KLabの技術系インターンに参加してきた話
- はじめに
- インターンに行くまで
- 目標
- micropsとは
- インターン(0日目)
- インターン(1日目)
- インターン(2日目)
- インターン(3日目)
- インターン(4日目)
- インターン(5日目)
- インターン(6日目)
- さいごに
はじめに
人生初のインターンということでKLabさんの技術系インターンに参加してきました。
www.klab.com
今回、期間中お世話になったメンターは山本雅也さんです。
twitter.com
進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。
インターンに行くまで
まず、何があったかというと昨年の9月頃にこんなことがありました。
というかけんつくんネットワーク周りに興味があるんだっけ?
— chikuwait (@chikuwa_IT) 2017年9月20日
TCP/IP のプロトコルスタックをゼロから作るインターンを知っているんだけど
— chikuwait (@chikuwa_IT) 2017年9月20日
pandaxさんネットワークおにいさんなのでけんつくんをつなげてみた
— chikuwait (@chikuwa_IT) 2017年9月20日
けんつくんもきっと煽りがいがあるはずなので
— chikuwait (@chikuwa_IT) 2017年9月20日
はい、つまりはそういうことです。
この前に、chikuwait君と話していて
「ネットワーク、興味あるから勉強したいんだよねぇ」
と、雑に話した途端にこれです。
というわけで、この後に山本さんに直接連絡を取りリモートで面談を行い
すごくあっさりと決まってしまいました。
ただ、ネットワーク周りを本当にやったことがなかったのでインターンまでに
「Linux カーネルモジュールで最速のEchoサーバを作る」
や
「DHCPの仕様をざっくりと理解してくる」
といったことを事前に行いました。
進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。
目標
今回のインターンでの目標はメンターである山本さんが作っているユーザ空間で動作するTCP/IPプロトコル・スタックである「microps」にDHCPクライアント機能を追加実装する。
ただ、それだけです。
micropsとは
micropsは前述の通り、ユーザ空間で動作するTCP/IPプロトコル・スタックで、Ethernetフレームの送受信を行います。
加えてカーネル内のプロトコル・スタックを使用していません。
僕が機能追加する前の段階でSocketライクなAPIを提供する機能もありました。
インターン(0日目)
飛行機代を抑えるために早朝の便をとり、なんと前日の昼前に東京着という意味不明なスケジュールで行動していました。
初の東京だったのでまずどこに行こうかと思い、やっぱりアキバにいってしまいました。
その後、紆余曲折を経てホテルに到着。初日からの戦闘に向けて美味しいものも食べてきました。
インターン(1日目)
この日は、10:30ごろにKLabさんが入っている六本木ヒルズ森タワーに行きました。
気合が入りすぎて5時ごろには起床していたので、何故か江東区から歩いていくという強行に出ました。
この日はまず
インターンのガイダンス説明から始まり
メンターさんの紹介やその他諸々の手続きを行い
その後すぐに会議室に移りmicropsの仕様とDHCPに関する説明を30分ほど受けました。
それからはひたすらに開発環境の構築やmicropsのビルドなどを行い、その日は終了。
インターン(2日目)
この日はDISCOVERパケットを構築すること、またそれを送信し返ってくるOFFERパケットを解析することが目標だった。
DHCPクライアントがどのようにIP等を手に入れているかというと、ざっくりと以下の4プロセスを踏む
- DISCOVER パケットをクライアントがブロードキャストで送信
- OFFER パケットが返ってくるので情報を解析
- 必要な情報を取り出しREQUEST パケットに組み込み送信
- ACK or NACKが返ってきて終了
2日目はこの前半2つを行った。
まずはRFCや事前資料からDISCOVERパケットを構築するところから始めた。
パケット自体は固定形式部分が234byte、可変形式のオプションが64byteという構成になっている。
またIPを取得していない状態でsocket等を使用するため、送受信は全てブロードキャストを使用する。
そのため、ブロードキャストフラグも使用し明示的にブロードキャストを指定。
オプションは4byteのマジックコードに始まり、パケットの種別を示すものが3byte続く。
その後はクライアントIDやMACアドレス、サーバ固有情報などが含まれ最終的には0xFFをStopperとしている。
Stopperから末尾まではデータが入っているはずがないので0x00で埋めてある。
ここまでがパケット構成となる。
その結果を以下に示す。
========== DHCP Message Debug Print ========== op: 1 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 0.0.0.0 siaddr: 0.0.0.0 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 01 option[3d] 01 08 00 27 0c a1 27 option:[ff] total 300 bytes (padding 47 bytes)
次に構築したパケットを送信、レスポンスを受信する処理を追加した。
ここで使用したものはmicropsに始めから搭載されていたudp関連のsocketライクなAPI。
レスポンスデータは形式こそオプション以外では変化が無いもののオペコードは必ず2になって(送信時は1を指定)返ってくる。
またオプション部分でマジックコードの次もOFFERパケットを示すものに変わっている。
以下が受信したOFFERパケットの情報となる。
========== DHCP Message Debug Print ========== op: 2 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 02 option[36] c0 a8 00 01 option[33] 00 00 02 58 option[01] ff ff ff 00 option[03] c0 a8 00 01 option[06] c0 a8 00 01 option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c option:[ff] total 304 bytes (padding 0 bytes)
二日目はここで終了。
インターン(3日目)
3日目はOFFERパケットから必要な情報を取り出し、REQUESTパケットを構築し送信
返ってくるACK(or NACK)を解析して、micropsを使用したEchoサーバの設定に必要な情報を組み込むところまでが目標。
まずは、yiaddrから使えるIPを取り出してREQUESTパケットとして構築。
構築したパケット自体は2日目に行ったものとほとんど同様なので概要は省略。
========== DHCP Message Debug Print ========== op: 1 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 03 option[3d] 01 08 00 27 0c a1 27 option[32] c0 a8 00 66 option:[ff] total 300 bytes (padding 41 bytes)
次にACK(or NACK)を受け取る。
これもOFFERパケットの受信と同様に行った。
========== DHCP Message Debug Print ========== op: 2 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 05 option[36] c0 a8 00 01 option[33] 00 00 02 58 option[01] ff ff ff 00 option[03] c0 a8 00 01 option[06] c0 a8 00 01 option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c option:[ff] total 304 bytes (padding 0 bytes)
ここまでは前日に行っていたこととあまり大差なかったのですんなりいった。
問題はこのあと、実際にACKから必要なIPアドレス、サブネットマスク、デフォルトゲートウェイを取り出して設定として組み込む点だった。
IPアドレスなどはすぐに見つかり、RFCなどにオプション種別も記載されていたので比較的容易に発見できた。
それを組み込む際にip_init 関数が存在するがこいつを単に呼び出すとethernetフレームにipプロトコルが二重に追加されてしまう問題があった。
またルーティングテーブル周りの初期化や、このmicropsを利用する人が任意でDHCPクライアント機能を利用できるようにするなど
やることが山積みだった。
このあたりはメンターの山本さんの力を借りて(もちろんここまで来るのに絶大な支援があった)、実装することができた。
ここで3日目が終了。
インターン(4日目)
この日は、最低限の処理しか追加していなかったコードにオプション解析などを組み込みデータがちゃんと正しいものか、こちらが想定しているものかチェックする処理などを追加
その後はひたすら鬼のペアプロコードリファクタリングを行った。(多分これが一番きつかった)
インターン(5日目)
この日は最終成果発表に向けて資料の準備とPRの作成を行った。
期間中、多大なご迷惑をお掛けした分プルリクがマージされた時はすごく嬉しかったというか、やったぞという気持ちしかなかった。
インターン学生の @lrf141 くんが僕の作ってる TCP/IP プロトコルスタック「microps」に DHCP クライアント機能を追加してくれました!実質三日で仕上げてくれたので優勝です🎉https://t.co/BnUoBgYmwf
— YAMAMOTO Masaya (@pandax381) 2018年3月30日
その後の成果発表も無事に終わり、Klabの方々とご飯を食べに行きこの日は終了。
そしてインターンも終了。
インターン(6日目)
帰りの飛行機は例によって飛行機代を抑えるためにほぼ最終便をとっていた。
そのため、馬鹿みたいに時間があったので大門->スカイツリー->浅草->上野、アキバと観光してきた。
余談ではあるが、人生経験の一環としてメイドカフェに行こうか本気で迷った。
さいごに
というわけで長かったようで短く感じた怒涛の初インターンは終了した。
インターンでやったことあったことを雑にまとめてみたが、実際にはスケジュールがかつかつで8時間はコードを書いている日の連続だった。
技術的な面でも未熟であったことがわかったが、その他にも色々と自分には知らないことが多かった。
成果発表で、色々なエンジニアの方に今回何をやったかということを報告し「わからない事にわからないなりに挑戦し、完成させたという点は評価できる」との評価を頂いたが
やはり実際に作っている時などに、「これはきいたことがあるぞ」「これは知っているかもしれない」と思っていたこともあったがそれをいざ実行しようとなると「お、なんか上手くいかないぞ」といったことが多くあり
わかっている事とできる事は全く別物であると痛感した。
また、自分は今までコードばかりを書いていてそれが楽しかったからというのもあるが
もっと自分が作るものの基盤的な知識を身につけなければいけないということも実感した。
そういった、自身の弱点のようなものを見続けたので今回のインターンはすごく辛く感じる面が多かった。
しかし、それ以上にものすごいレベルのエンジニアの方たちに囲まれて
今回のようにコードを書いて、何かを実現してという環境に身を置けたのはすごく楽しかった。
辛いと感じることもあったが、それでもわからないことをわからないと認めてその上で色々と試行錯誤を繰り返し一つの問題を解決していく。そういった時間を過ごせたことはこの上なく幸せな時間だった。
学ぶべきことが多く分かった反面で、学んだことも非常に多かった。
一週間ではあったが自分自身が以前に比べて成長できたことを感じることができたのでインターンに参加して本当によかったと思った。
最後に、今自分がいる大学ではあまり情報系に熱心な同期がいない。
そして家でもコードを書いて、それを楽しいと5年も言い続けている自分はそんな普通な人からみるとキモいという対象らしい。
そんな人たちに流されて普通の大学生活を送ろうとしていたが、今回のインターンでお昼休みなどに山本さんとキャリアについて話していて
自分のやりたいことがはっきりと見えたので周りを気にしないで残りの学生生活を自分なりに有効活用しようと思う。
けんつくんは僕が紹介したインターンで邪念を捨てたらしいです
— chikuwait (@chikuwa_IT) 2018年4月10日
本当に今回インターンに参加できてよかったとおもう。
今回、お世話になったKLabの皆さん、そしてなによりメンターとして1週間指導してくださった山本雅也さん
本当にありがとうございました。
無限インターン行きたい!!
プログラマのためのSQL第4版を読んで。〜 データベース VS ファイルシステム 〜
はじめに
訳あって今、カーネルからWebまでという非常に広い範囲を日常的に触っている。
しかし、一日の時間は24時間と決まっており自分にはコードを書く事以外にもしなければならないことがある。
だが、コードを書かないという日はなるべく作りたくない。
我儘を言うなら、限られた時間でC言語で低レイヤーをやったりPHPを使って高レイヤーをいじったり、ScalaやJavaを使ってその中間も色々やってみたい。
そう思った時に、その全てを叶えるのがデータベースだと思った。
NoSQLであるRedisはC言語ベースで作られていてソースコードはGithub上で公開されている。
PHPではサーバサイドとして実際にDBを操作する時がある。
Scala/Javaにはjdbcなどがある。
つまり、先ほどの我儘を全て叶えてくれる非常に魅力的な領域だった。
というわけで、そんなふわふわした理由のもとにデータベースを勉強してみようと思った。
単にデータベースを勉強するといっても、実際にDBを使うことに始まり
それらを利用するためのドライバやDB自体をソースコードレベルで理解することまでを目標にする。
その上でまずDBの使い方、特にSQLについて深く勉強したいと思ったので「プログラマのためのSQL」を購入した。
ここではその書籍を読んで勉強したことをまとめていく。
データベース VS ファイルシステム
データベース、特にRDBMSにおいては普段利用するプログラミング言語が操作するファイルシステムとはかなり勝手が違う。
データベースを操作するのにはSQLを使用し、SQLはプログラミング言語と異なり独自のI/Oシステムを持たずデータの宣言、操作、制御のみを行う。
しかし、プログラミング言語にもSQLにもモデルは存在する。
そのモデルを理解することで対象を理解することが容易になる。
プログラミング言語におけるモデルとは、あまり詳しくないのではっきりとは言えないが「いくつかのプログラミング言語は数学をベースとしているため数学で実際に使用する記号群や構成を使用することができる。」といったことを指す。
SQLにおけるモデルは数学でいうところの集合(Set)としてのデータである。
数学でいう集合は、それに含まれる要素は特定のタイプに属していて順序をもたない。そして、集合に対する操作は一度に全ての要素に対して行われる。
わかりやすい例が参考文献に記述してあった。
正の整数の集合から奇数の部分集合を求める場合、答えはすべての奇数を含む単一の集合として得られる。奇数を1つずつ調べて順番に集合を作っていくようなことはしない。ただ、奇数を「2で割ったときの余りが1になる」という条件を満たす数として定義するだけだ。
このような背景があるために、分類の条件を変更したとしてもテスト可能で、かつ分類そのものも可能である。
それ故、集合を対象とする集合指向モデルは並列処理に向いている。
SQLは3つのサブ言語から構成されている
- DDL: データ宣言言語(Data Declaration Language)
- DML: データ操作言語(Data Manipulation Language)
- DCL: データ制御言語(Data Control Language)
DDLはデータベースに含まれる中身を定義し、そこに含まれるデータの整合性を確保する。
前述のファイルシステムでは整合性やデフォルト値、他のテーブルとの関連性は一切定義されない上に厳密にデータの論理的一貫性を保つ機能はない。
しかしデータベースにおいてはDDLがそれらの役割持ち、DMLとDCLと共に動作するためSQLは統合された全体として扱われファイルの様に分離された一部分とは扱われない。
DMLはSQLを一度でも書いたことがある人ならわかる。
SQLにおける次の操作を示す。
- SELECT
- INSERT
- UPDATE
- DELETE
DCLはここではまとめない。かなり深い概念らしくあまり解説されていなかったから。
ここまでのまとめ
エンティティとしてのテーブル
データベース、特にリレーショナルデータベースにおいてエンティティとは属性によって定義される。そしてそのインスタンスはテーブルにおける1行1行を指す。
属性とはテーブルでいうところの列であり、値はそれ以上分解不能な原始的な値であるスカラ値をもつ。
よってエンティティとしての役割をデータベースのテーブルは持つことになる。
完全に同じ構成のテーブルが2つあるとき集合的な観点からみるとそれらは同じ種類の要素を持つ集合でしかない。
これはファイルであれば許されることである。なぜならファイルはそれら自体は物理的に分類された単位であるために同一情報を異なるファイルとして保持することが許容されている。
しかし、前述の内容よりSQLにおいては集合であり区別が付かないのでそのような物は一つにまとめてしまうのがよい。
関連としてのテーブル
これは非常に簡単なことで、テーブルにおける関連とは列が一つ以上のエンティティテーブルを参照することによって成立している。
これはファイルとフィールドにはない特性になっている。
行 VS レコード
行はレコードではない。レコードはそれを読み込むアプリケーション側で定義されるものであって、それ自体はスキーマで定義される。
フィールドの名前はアプリケーション側で定義されるのに対して、行はデータベーススキーマ中で定義されるということになる。
ここで空のファイルを考える。
空のファイルは0バイト長という状態であるが、空のテーブルに関しては行は空としても列を持っていて理論的制約などを保持している。
つまり数学的意味合いの集合における空集合とは少し異なり、上記の構造をとるためたとえ空であっても別の集合として扱うべきである。
次の行にある特徴としてはテーブル内の全ての行は構造上同一な形式を取る点であり、これはファイルにはない。
ファイルであれば内部に含まれるデータの構造などは全て一致しているわけでもなく文字列であったり数値であったり情報もサイズもバラバラにしてしまうこともできるからだ。
列 VS フィールド
レコード内のフィールドはそれを読み込むアプリケーション側で定義されるが行における列はデータベーススキーマで定義される。
そして列が保持するデータ型は前述の通りスカラ値を持つ。
そして、SQLでは列は列名によってのみアクセスされる。
厳密に列名によってのみというわけではなく
SELECT * INSERT INTO <table name>
の様な省略形は存在する。しかしこれは列名のリストを単にテーブル定義で列が定義された物理順序で展開しているだけにすぎないので内部的には列名でのみアクセスされる。
SQLにおけるNULLの使用もこれに近いものとなっている。
列があるが故に、ファイルとの違いがある。
ファイルは受け入れる内容や吐き出す内容に特に制約は無いがSQLにはその制約が存在する。
それにより、SQLが扱うものには一定の整合性が保証される。
つまりはそれが指す関係の整合性を示すことになる。
これはファイルごとに独立しているファイルシステムには無い特徴となっている。
おわりに
SQLとしての基盤的な事象をファイルシステムと比較してまとめてあったのはSQLを理解する上で非常に大事な情報であると感じた。
次はDBMSが内部的に行っているトランザクションと同時実行制御に関する章を読んでまとめていきたい。
参考文献
- プログラマのためのSQL第四版
- スキーマとは-IT用語辞典 スキーマとは - IT用語辞典
- エンティティ(entity)-「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 http://wa3.i-3-i.info/word11594.html
最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part4
はじめに
前回、実装したはずの機能で特にacceptでコケる問題がなかなか解決出来なかったので
毎回おなじみになりつつあるこのサイト
linux/include - Elixir - Free Electrons
さらに今回は、この本も参考にして書いていく
www.oreilly.co.jp
まず色々、調べたりしたことをまとめてその後に本題に入ろうと思う
色々やる前のメモ
実際に動いたであろうサンプルソースを参考にああだこうだ考えながら書いていたがacceptでコケる。
省略していた待ち行列が関係してるのではと思っている。
そもそも、エラーコード返ってきてるのにそれを見てなかったから見てみる。
見てみた。
$dmesg .... .... [524524.095520] Fastest Echo Server Start!! [524524.095544] accept error no:-11 $
なんか、acceptでエラーが返ってきている。
それを上記のサイトで調べる。
まずはacceptの実装から。
acceptの実装は次のようになっていた。
static int accept(struct socket *sock, struct socket *new_sock, int flags) { struct sock *sk = sock->sk; struct sk_buff *buf; int res; lock_sock(sk); if (sock->state != SS_LISTENING) { res = -EINVAL; goto exit; } while (skb_queue_empty(&sk->sk_receive_queue)) { if (flags & O_NONBLOCK) { res = -EWOULDBLOCK; goto exit; } release_sock(sk); res = wait_event_interruptible(*sk_sleep(sk), (!skb_queue_empty(&sk->sk_receive_queue))); lock_sock(sk); if (res) goto exit; } buf = skb_peek(&sk->sk_receive_queue); res = tipc_create(sock_net(sock->sk), new_sock, 0, 0); if (!res) { struct sock *new_sk = new_sock->sk; struct tipc_sock *new_tsock = tipc_sk(new_sk); struct tipc_port *new_tport = new_tsock->p; u32 new_ref = new_tport->ref; struct tipc_msg *msg = buf_msg(buf); lock_sock(new_sk); /* * Reject any stray messages received by new socket * before the socket lock was taken (very, very unlikely) */ reject_rx_queue(new_sk); /* Connect new socket to it's peer */ new_tsock->peer_name.ref = msg_origport(msg); new_tsock->peer_name.node = msg_orignode(msg); tipc_connect2port(new_ref, &new_tsock->peer_name); new_sock->state = SS_CONNECTED; tipc_set_portimportance(new_ref, msg_importance(msg)); if (msg_named(msg)) { new_tport->conn_type = msg_nametype(msg); new_tport->conn_instance = msg_nameinst(msg); } /* * Respond to 'SYN-' by discarding it & returning 'ACK'-. * Respond to 'SYN+' by queuing it on new socket. */ if (!msg_data_sz(msg)) { struct msghdr m = {NULL,}; advance_rx_queue(sk); send_packet(NULL, new_sock, &m, 0); } else { __skb_dequeue(&sk->sk_receive_queue); __skb_queue_head(&new_sk->sk_receive_queue, buf); } release_sock(new_sk); } exit: release_sock(sk); return res; }
この中で、一通り関係しそうなものをerror.hとかerror-base.hから探してみる。
そうすると
#define EWOULDBLOCK EAGAIN
が返っていることがわかり今度はこのEAGAINを調べた。
すると、こいつが例の11番のエラーを返していた。
ただ
#define EAGAIN 11 //try again
とあり、何をtry againすればいいのかわからなかった。
そこでaccept関数の実装をみて、どこでこのエラーが返ってきてるか調べてみることにした。
するとここの処理が影響していることがわかった。
while (skb_queue_empty(&sk->sk_receive_queue)) { if (flags & O_NONBLOCK) { res = -EWOULDBLOCK; goto exit; } release_sock(sk); res = wait_event_interruptible(*sk_sleep(sk), (!skb_queue_empty(&sk->sk_receive_queue))); lock_sock(sk); if (res) goto exit; }
skb_queue_emptyが返す値が真であるならO_NONBLOCKをflagsとして渡しているので
今回のgotoで該当の処理がスキップされエラーとなってしまうためskb_queue_emptyが返す値が偽である必要がある。
skb_queue_empty関数に渡しているものは
&sk->sk_receive_queue
という構造体の値である。
そもそもこのskが何かというとaccept関数の先頭で宣言、初期化されている次のものである。
struct sock *sk = sock->sk;
右辺のsockはaccept関数に渡している第一引数である。
そして問題のskb_queue_empty関数は次のようになっている。
static inline int skb_queue_empty(const struct sk_buff_head *list) { return list->next == (struct sk_buff *)list; }
これはソケットバッファに関するキューの様でリストの次の要素と先頭要素が等価であることを調べている(っぽい)。
これとはまた別にO_NONBLOCK、ノンブロッキングIOについてもよくわかっていなかったのでそれも別途記述していく。
ノンブロッキングI/O
カーネルモジュールで、TCPソケットを使おうとしたらほとんどのソースにO_NONBLOCKが出てきてなんだこれはと調べると
次のサイトに色々まとまっていた。
ノンブロッキングI/OではI/O対象のファイルディスクリプタの準備完が了していないことをアプリケーション側に伝えるため即座にエラーが返る(errnoにEGAINが格納されて返ってくる)。一般に、O_NONBLOCKフラグを利用してノンブロッキングモードを宣言するが、この時プロセスはブロック状態にならず、CPUを他の処理に回すことができるためI/O待ち時間を有効活用できる。
ノンブロッキングI/Oはソケットなどのネットワークに用いられるI/OモデルでディスクI/Oには使わない。
これに関連していたのがO_NONBLOCKを使用していたacceptであった。
次のManPageを参照すると色々わかった。
Man page of ACCEPT
キューに保留となっている接続要求がなく、 かつソケットが非停止になっていないときは、 accept() は接続が発生するまで呼び出し元を停止 (block) する。 ソケットが非停止になっていて、 待ち状態の接続要求がキューに無いときは、 accept() はエラー EAGAIN か EWOULDBLOCK で失敗する。
これが最も自分を苦しめたあれである。
そもそも今回は、このキューを使っていなかった。
そのため、待ち状態の接続要求がキューになく(そもそもキューを用意していなく)、acceptがEAGAIN,EWOULDBLOCKを返していた・
おわりに
そんなこんなで、tcpクライアントからメッセージを受け取り表示するところまではできた。
コードは以下のレポジトリにある。
github.com
使ったtcpクライアントはpythonで書いたものを使用した。
#!/usr/bin/env python # -*- coding: utf-8 -*- import socket target = "localhost" port = 8888 #using IPv4 and TCP/IP client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #connecting client_socket.connect( (target, port) ) #data send client_socket.send("hello,world") #get data response_data = client_socket.recv(1024) print response_data
ただ、データの送信が出来ないことに加えて、時折メッセージを受信出来なかったりするのでそのあたりのバグとリファクタリングは今後も行っていく必要がある
最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part3
はじめに
前回まで
rabbitfoot141.hatenablog.com
rabbitfoot141.hatenablog.com
この二回でカーネルモジュールで標準出力や、カーネルスレッドを動かすところまでやった。
今回はこれをTCPサーバー化するのを目標にやっていく。
ネットワーク周り
さくっと実装したいが、カーネルモジュールでのネットワーク周りがわからないので
そこあたりから色々調べていく。
ソケット周り
これが使えないとネットワーク通信ができないのでこれがどういう仕様で実装されているか調べてみる。
見つかった。
http://elixir.free-electrons.com/linux/v3.4/source/include/linux/net.h#L138
これは
#include <linux/net.h>
というヘッダーにあることがわかった。
これの138行目にsocketという名前の構造体があり。
こいつがBSDのソケットとして使えるようなので使うことにする。
/** * struct socket - general BSD socket * @state: socket state (%SS_CONNECTED, etc) * @type: socket type (%SOCK_STREAM, etc) * @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc) * @ops: protocol specific socket operations * @file: File back pointer for gc * @sk: internal networking protocol agnostic socket representation * @wq: wait queue for several uses */ struct socket { socket_state state; kmemcheck_bitfield_begin(type); short type; kmemcheck_bitfield_end(type); unsigned long flags; struct socket_wq __rcu *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; };
色々なコードや、文献を漁っているとこの中でも次に重要になるのが
const struct proto_ops *ops;
という構造体。
この構造体の実装は次のようになっている。
struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #ifdef CONFIG_COMPAT int (*compat_ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); #endif int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); int (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); #ifdef CONFIG_COMPAT int (*compat_setsockopt)(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen); int (*compat_getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); #endif int (*sendmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len); int (*recvmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len, int flags); int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags); ssize_t (*splice_read)(struct socket *sock, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags); void (*set_peek_off)(struct sock *sk, int val); };
bind,listen,acceptなどなど、どこかで見たことのあるような単語が並んでいる。
実際にtcpサーバを構築するときは、これらの関数に必要な情報を渡して呼び出す必要がある。
なんやかんや、C言語でネットワークプログラミングをする時と同じ様なあれこれはあるので
それと似たような感じでコードを書けそう。
この後で実装するコードは、カーネルスレッドを多用するので
kernel_lock関数を使った方がいいとかはまた別途考える必要がある。
文字列周り
文字列周りの扱いはどうしようかと調べていたら、linux/socket.hにちょうどいい構造体があった。
/* * As we do 4.4BSD message passing we use a 4.4BSD message passing * system, not 4.3. Thus msg_accrights(len) are now missing. They * belong in an obscure libc emulation or the bin. */ struct msghdr { void * msg_name; /* Socket name */ int msg_namelen; /* Length of name */ struct iovec * msg_iov; /* Data blocks */ __kernel_size_t msg_iovlen; /* Number of blocks */ void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */ __kernel_size_t msg_controllen; /* Length of cmsg list */ unsigned msg_flags; };
kernelでネットワークのレスポンスを扱うにはなにやらこれがいいらしい。
実装の前に
全体の流れを整理する。
- 必要なsocket類を構造体などで宣言する。
- listenをするスレッドを作る
- 上記のスレッド内でsocketを作成、bind、listenなどを行う
- それが完了したら更にacceptを行うスレッドを作る
- 文字列が送信されてきたら、それを送り返す
実装する
実装してみた、結果はこちら。
ヘッダーファイルに直書きしているのは後で直す。
ただし、実行してみるとaccept errorと表示されている。
[ 689.884212] accept_thread the csock is :474605440,474606080 [ 689.884216] accept start [ 689.884220] accept error
なるはやで解決しないといけないものは
accept errorと表示されるので、上記のopsを見なおしてなぜそこでコケたのか調べる。
あとrmmodを実行した際に
rmmod: ERROR: ../libkmod/libkmod-module.c:793 kmod_module_remove_module() could not remove 'fastecho': Device or resource busy rmmod: ERROR: could not remove module fastecho.ko: Device or resource busy
といったエラーが出てinsmodするために再起動しないといけない。
それはだるいのでどうにかしたい。
以上、学生アドベントカレンダー25日目の記事でした。
全てのプログラマに捧げるScala入門
はじめに
Qiitaの方に次のようないくつかのScala入門記事を掲載していた。
java経験者のScala入門メモ [基礎知識] - Qiita
java経験者のScala入門メモ [関数、クラス] - Qiita
java経験者のScala入門メモ [ケースクラス、オブジェクト] - Qiita
java経験者のScala入門メモ [トレイト、メソッド] - Qiita
しかし、これらは自身がScalaを学習し始めた直後に書いたもので
内容の正確性と充実性がいまいちなのでこれを書き直すことにした。
対象は、Javaを使っていてScalaの使用を考えている人。を想定しているが
Java以外の言語を使っている人や単に興味がある人にも楽しんで貰えればと思っている。
対応するバージョンは2.11.xと2.12.xとなる。
ただし、この記事群はあくまで入門編なので
理解が難しい点や現状で解説しても混乱を招くことが想定されるものは除外しているので
その点はご理解のほどをお願いしたい。
Scala入門
Scalaがある程度使えるぐらいまででいいという方はコレクションまで
もう少し使ってみたいという方は最後まで読んで頂けるとある程度の入門にはなるだろう。
以下の記事は本アドベントカレンダー用の記事として作成した。
- Scalaの基本
- クラスとオブジェクト、ケースクラス
- トレイト
- ラムダと高階関数、カリー化
- コレクション
- パターンマッチ
- 暗黙の型変換
- 型パラメータ
- 例外
これら3つに関してはまた別の機会に書くことにする。
参考文献
- Scala スケーラブルプログラミング 第3版
- Scala 公式ドキュメント
A Scala Tutorial for Java Programmers | Scala Documentation
- Scalaメモ
Scalaメモ(Hishidama's Scala Memo)
- Scalaでリスト処理
- 第18章 Scalaのパターンマッチ
さいごに
この記事は Muroran Institute of Technology Advent Calendar 2017 24日目の記事となる。
今回は特に書くことがなかったのでScalaの入門記事を更新することにした。
いくらアドベントカレンダーといえど7個も記事書くのしんどかった。
全てのプログラマに捧げるScala入門 パターンマッチ
はじめに
前回はコレクションについてさらっと触れたが今回は、パターンマッチについて触れる。
しかし、例によってパターンマッチは用途がかなり多いのでここでは基礎的なことにのみ限定して紹介する。
パターンマッチ
match文というのがScalaには存在する。
例えば、次のコードを例に考えてみよう。
scala> def judge(n: Int): String = n match { | case 1 => "one" | case 2 => "two" | case _ => "error" | } judge: (n: Int)String scala> (1 to 5).toList.foreach( num => println(judge(num)) ) one two error error error
match文は直前に渡される変数に束縛され実行する。実際に今回はnという変数に束縛されている。
今回は定数パターンでのみ分類したが最初の2つがnが1 or 2のとき最後のものがそれ以外の時を指す。
この程度であれば、if式などを使った方が断然早く楽に感じる。
しかし、このパターンマッチは単に値を格納した変数だけでなく型やその他もろもろのものに対して適用できるのでif式などよりも汎用性が高い。
これ以降はほかにどんな状況が想定できるか解説していく。
型パターン
型についてもパターンマッチを適用できる。
scala> val list = List(1, "hello", true, 1.0) list: List[Any] = List(1, hello, true, 1.0) scala> list.foreach(value => value match { | case number: Int => println(number) | case str: String => println(str) | case bool: Boolean => println(bool) | case _ => println("error") | }) 1 hello true error
このように、case後の変数名後にデータ型を書くことができて
それについて処理を分けることもできる。
ただし、最後のcaseで指定しているワイルドカードパターンがなければ
リストの末尾にある、Double型の変数がマッチしないためにエラーが発生する。
つまりパターンマッチは、なにかしらのパターンにマッチさせなければいけないということになる。
変数パターン
これは先程のものと同じかと思うが少し違う。
scala> val num = 2 num: Int = 2 scala> val res = num match { | case num => num * 2 | } res: Int = 4
これは計算できるかぎりの全てのパターンにマッチする。
そしてマッチしたものは変数に束縛させて処理を行っている。
シーケンスパターン
お次はシーケンス型のパターンにマッチするものである。
scala> val list = List(1,2,3,4) list: List[Int] = List(1, 2, 3, 4) scala> val res = list match { | case List(1, a, _*) => a | case _ => -1 | } res: Int = 2
先頭要素が1で要素が2個以上のシーケンス型のコレクションにマッチするものに成る。
タプルパターン
これは個人的に、ネットで情報を探していて一番おもしろかったので紹介する。
有名なFizzBuzzを解くプログラムを例にする。これはタプルを上手に使った面白い解法だった。
object Sample{ def main(args: Array[String]):Unit = { (1 to 30).foreach( num => println(judge(num)) ) } def judge(n: Int): String = (n%3, n%5) match { case (0, 0) => "FizzBuzz" case (0, _) => "Fizz" case (_, 0) => "Buzz" case _ => n.toString } }
割ったあまりをタプルとして格納して、それを元にマッチさせている非常にスマートな解法だった。
パターンガード
これは定数パターンに通じるものだが、定数や変数を扱う際にそのマッチに更に条件をつけることができるものである。
scala> val n: Int = 5 n: Int = 5 scala> val res = n match { | case n if n%2 == 0 => true | case n if n%2 != 0 => false | } res: Boolean = false
おわりに
この他にもまだコンストラクタパターンなどがあるが実際にライブラリ等を使用した際によく使うものであるので
興味があれば他のパターンマッチについても学習して欲しい
全てのプログラマに捧げるScala入門 コレクション
はじめに
この記事ではScalaのコレクションについて解説する。
しかし、コレクションをすべて隅から隅まで紹介するにはこの一つの記事では難しいため
特によく使うものに厳選して解説する
コレクション
コレクションにはmutable(可変)とimmutable(不変)のものが存在するため
まずそれらについて解説し、その後にそれらを踏まえた性能の特性について解説する。
特に、immutableであるコレクションを使う際にはそれぞれの性能特性を理解しておくことが
パフォーマンス的に重要になってくる。
不変と可変
不変と可変はこれ以前の記事で解説した、valとvarに似たとようなものである。
しかし、コレクションとこれらを併用することで微妙な差が出てくる。
ここでは特にそれを紹介する。
コレクションにおける不変と可変
次のようなimmutableなリストを例として挙げる。
val list: List[Int] = List(1,2,3,4,5)
このリストに対して、インデックスを使用して値を参照する場合は
applyメソッドか()でインデックスを指定する。
list(0) //return -> 1 list.apply(1) //return -> 2
インデックスを取得できるなら値の更新ができるはずだが、immutableリストではそうならない。
scala> list(0) = 10 <console>:9: error: value update is not a member of List[Int] list(0) = 10 ^
エラーにも出ているがimmutableなコレクション、今回の場合で言えば特にイミュータブルリストは
updateという値の更新をするメソッドが存在しないために更新できない。
immutableとは内部の値も更新できない、つまりは内部状態が不変なコレクションと言える。
それでは値を更新したい場合はどうするのか。
解決策は至って簡単でListを作りなおすことである。
先頭に要素を追加する場合と末尾に要素を追加する場合の2つを紹介する。
scala> 0 :: list res4: List[Int] = List(0, 1, 2, 3, 4, 5) scala> list :+ 6 res5: List[Int] = List(1, 2, 3, 4, 5, 6)
このように::と:+を使うことでそれぞれ先頭追加、末尾追加が行える。
そして、値を追加した後、新しいリストが返ってきている。
しかし、これを次のように再代入はできない。
scala> 0 :: list res4: List[Int] = List(0, 1, 2, 3, 4, 5) scala> list :+ 6 res5: List[Int] = List(1, 2, 3, 4, 5, 6)
というのも、listという変数は今valで宣言されており
値を追加したimmutableなリストは新しいListオブジェクトとなっているので
valにおいては参照するオブジェクトが変更されることに等しいので再代入はできない。
なので、実際に上記のコードのように再代入したいのなら変数はvarで宣言する必要がある。
それでは次にmutableリストについて解説する。
immutableリストを解説したあとだとこちらはわかりやすいかもしれない。
まずmutableリストを使うにはcollection.mutableパッケージをインポートする必要がある。
import scala.collection.mutable.ListBuffer
mutableなリストはMutableListが存在するが実際にはListBufferを使うことが多いと思うので
今回はそちらをインポートする。
そして先ほどと同様にリストを宣言する。
scala> val list: ListBuffer[Int] = ListBuffer(1,2,3,4,5) list: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4, 5)
valで宣言していことを覚えておきながら値の更新を行う。
//index = 0の値を2に更新 scala> list.update(0, 2) scala> list.foreach(println _) 2 2 3 4 5
更新が正常に終了している。
valで宣言したmutableコレクションの値を更新することが出来たのは
コレクション、今回ならListBufferのオブジェクト自体に変更はなく
変更されたのはコレクションの内部状態であるからvalでも変更ができた。
これと同様に、先頭や末尾への追加がおこなえる。
性能特性
性能特性に関しては、いちからまとめるより公式のドキュメントがよくまとめているのでそちらを紹介する。
特に気をつけたいのは線形と定数となっている部分である。
例えばimmutableなListでは末尾への追加が線形になっていて先頭への追加が定数となっている。
これが意味することは末尾への追加はその要素数分だけコストがかかり、先頭への追加は一定のコストしかかからないということである。
もし要素数が多いリストに値を追加するなら最初から先頭追加にして、
追加が完了した段階でreverseなどをつかって逆順にすることの方が賢い方法と言える。
シーケンス
ここでは特によく使うシーケンスのコレクションについてのみ解説する。
シーケンスとは要素が順序をもっておりインデックス等で要素を指定できるコレクションのことである。
先ほど紹介したListもシーケンス型のコレクションに含まれている。
リスト
Scalaにおけるリストは次のような構造になっている
List(1,2,3, ..., n)
これをもっと掘り下げると次のような構造になっている。
1 :: 2 :: 3 :: ... :: n :: Nil
先頭追加のメソッドが連続した状態で、末尾がかならずNilになっている。
データ型については前章を見てもらうとわかるが型推論でIntになっている。
これが例えばIntとDoubleが混在したリストだとどうなるだろうか?
List(1,1.0) res4: List[Double] = List(1.0, 1.0)
IntはDoubleにキャストできるためDoubleのリストとして解釈され、Intの要素もDoubleに変換されている。
では次にIntとStringの混在リストはどうなるだろうかやってみよう。
scala> List(1, "hello") res5: List[Any] = List(1, hello)
すべてのクラスの親クラスであるAny型に推論されている。
推論はされているが、実際はこのようにいくつかの型をまとめるようなデータ構造は避けるべきである。
次にリストのなかでよく使うメソッドを一気にコードとして紹介する。
//Listでは無いものをListに変換する scala> val list = (1 to 10).toList list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) //先頭要素を求める scala> list.head res0: Int = 1 //先頭以外を求める scala> list.tail res1: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10) //サイズを求める、sizeでもいい scala> list.length res2: Int = 10 //空かどうかを求める scala> list.isEmpty res3: Boolean = false //末尾要素を求める scala> list.last res4: Int = 10 //末尾以外を求める scala> list.init res5: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9) //先頭からn分だけリストにする scala> list.take(5) res6: List[Int] = List(1, 2, 3, 4, 5) //末尾からn分だけリストにする scala> list.takeRight(5) res7: List[Int] = List(6, 7, 8, 9, 10) //先頭の要素n個を除いたものを求める scala> list.drop(5) res8: List[Int] = List(6, 7, 8, 9, 10) //末尾の要素n個を除いたものを求める scala> list.dropRight(5) res9: List[Int] = List(1, 2, 3, 4, 5) //引数が含まれているか求める scala> list.contains(5) res11: Boolean = true //逆順にする scala> list.reverse res12: List[Int] = List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1) //最大値 scala> list.max res13: Int = 10 //最小値 scala> list.min res14: Int = 1
配列
リストだけでなく当然配列も存在する。
配列は要素のシーケンスを保持することができて、任意の要素に対して一定のコストでアクセスできる。
一般的な配列はScalaだと次のようにかける。
//要素数5で作成 val array = new Array[Int](5)
もちろんListのようにパラメータでも初期化できる。
メソッドに関してもほとんどListと同じで大きな違いといえば性能面になる
バッファ
最初に紹介したListBufferと同様に、mutableな配列やシーケンスは大抵****Bufferの名前え存在する。
これは内部状態がミュータブルであるが、利用できるメソッドとして大きな違いはない。
コレクションと関数
ここではリストの要素に関数を適用して新しいリストを求める場合について行う。
前の記事で紹介したラムダ式を多用することになる。
map
これは新しいリストを返す制御構造のなかで特に使うものであり次のように使用する。
scala> val list = 1 to 10 toList list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) scala> list.map(num => num * 2) res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
関数にたいして要素を一つずつ適用し、返ってきた値を新しいリストとして生成しているのがわかる。
この程度であればわざわざ明示的にラムダを書く必要がなくワイルドカードを使用して簡潔にかくことができる。
scala> val list = 1 to 10 toList warning: there was one feature warning; re-run with -feature for details list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) scala> list.map(_ * 2) res0: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
filter
これは評価式をもつ関数を渡し、それに対して要素が真であるmのをリストとして返すものである。
使い方自体はmapと同じになる。
scala> list.filter(num => num % 2 != 0) res1: List[Int] = List(1, 3, 5, 7, 9)
foreach
これはリストの要素に対して、繰り返し処理をする時に使う。
よく使うのは要素の表示など
scala> list.foreach(println _) 1 2 3 4 5 6 7 8 9 10
ただし、新たなコレクションが返ってくるわけではない。
おわりに
ここではまだ紹介していないMapやSet、Tuppleなどのコレクションや
畳み込みなどのメソッドがあるので、Scalaをもっと使いこなすにはそれらへの理解も必要になるだろう。
次はパターンマッチについての紹介になる。