最初に、プログラムが操作する対象を識別する

開いたファイルや通信接続には番号が付く

プログラムがファイルを開いたり、ネットワーク接続を作ったりすると、Linuxカーネルはその対象を管理します。プログラムには、対象を指定するための小さな整数が返されます。この番号をファイルディスクリプタファイルディスクリプタプロセスが、開いたファイルや通信接続を指定するために使う整数。と呼びます。

ネットワーク通信の接続口としてカーネルが管理する対象をsocketと呼びます。read(fd, buffer, size)は、fdで対象を指定し、受け取ったデータをbufferで示したメモリ領域へ書き込みます。戻り値は、実際に読み込んだバイト数です。

ファイル

fd 3

保存済みの内容を読み書きする対象を示します。

ネットワーク接続

fd 4

通信相手から届いたデータを読み、送信する対象を示します。

待ち受けsocket

fd 5

新しい接続を受け付ける対象を示します。

待ち受けsocketをサービスへ引き継ぐsocket activationを見る

04.1
データがまだ用意されていない場合

readを呼んだスレッドは、データが届くまで待つ

通信接続から読む場合、相手がまだデータを送っていなければ、すぐには結果を返せません。結果が得られるまで呼び出し元のスレッドを待機させる読み書きをblocking I/Oと呼びます。待機中のスレッドはCPUへ割り当てる候補から外れるため、CPUはほかの実行可能なスレッドに使われます。

アプリのスレッド待機中read(fd, buffer, size)
Linuxカーネルsocketにデータが届くまで待つこのスレッドはCPUの割り当て対象から外れる
通信相手データは未到着ネットワークの処理は別に進む
待機中

CPUは別のスレッドへ割り当てられる

データが届くまで、このスレッドは実行可能な状態から外れます。待っているあいだもCPUを使い続けるわけではありません。

データが到着すると、カーネルは待っていたスレッドを実行可能な状態へ戻します。スケジューラがそのスレッドをCPUへ割り当てた後、read()の処理が続き、アプリへ結果が返ります。

04.2
一つのスレッドで多数の接続を扱う

読み書きを進められる対象だけを知りたい

1000本の接続

現在データが届いている接続は一部だけ

一つの接続でread()を待ち続けると、そのスレッドはほかの接続を処理できません。そこで、複数のファイルディスクリプタのうち、現在I/Oを進められるものをカーネルへ問い合わせます。この状態をI/O readinessI/O readinessreadやwriteなどのI/O操作を、現在は待たずに進められる状態。と呼びます。

select

番号の集合を毎回渡す

監視するファイルディスクリプタを集合に入れてselect()へ渡します。LinuxではFD_SETSIZEによる1024本の上限があり、man pageは現代のアプリにpollまたはepollを勧めています。

poll

監視項目の配列を毎回渡す

ファイルディスクリプタと待ちたいイベントをpollfdの配列にしてpoll()へ渡します。selectの固定上限はありません。

epoll

監視対象を登録し、準備できた対象を受け取る

epoll_ctl()で監視対象を登録します。カーネルは監視対象と、現在I/Oを進められる対象を別のlistで管理し、epoll_wait()で後者を返します。

04.3
epollがカーネル内で保持する二つのlist

監視対象と、現在処理できる対象を分ける

epollepoll多数のファイルディスクリプタから、現在I/Oを進められる対象を受け取るLinuxのAPI。では、interest listが監視対象を、ready listが現在I/Oを進められる対象への参照を保持します。アプリは毎回すべての監視対象を渡し直すのではなく、ready listに入った対象をepoll_wait()から受け取ります。

interest list

epoll_ctl()で登録

fd 4fd 7fd 9

アプリが監視したいファイルディスクリプタを保持します。

ready list

カーネルが更新

fd 7

現在I/Oを進められる対象への参照が入ります。

アプリへ返す

epoll_wait()

fd 7

ready listにある対象をまとめて受け取ります。

上の接続を選ぶと、読み込み可能な状態を切り替えられます。

level-triggered

読み取れるデータが残る間は通知対象になる

epollの標準動作です。一度にすべてを読まなくても、読み込み可能な状態が続く間は、次のepoll_wait()でも通知されます。

edge-triggered

状態が変化した時点を通知する

EPOLLETを指定すると、状態の変化を契機に通知されます。通常は、データを待たずにEAGAINを返すnonblocking I/Oに設定し、read()がEAGAINを返すまで読み進めます。

04.4
準備完了の通知から、処理の依頼と完了へ

epollとio_uringが受け渡す情報は異なる

epollが返すのは、「このファイルディスクリプタなら、現在は読み書きなどを進められる」という通知です。通知を受けたアプリは、その後にread()やwrite()を呼びます。

io_uringio_uringI/O操作の依頼と完了結果を、共有メモリ上の二つのqueueで受け渡すLinuxのAPI。では、アプリが読み込みや書き込みなどの操作をSubmission Queueへ登録します。カーネルは処理結果をCompletion Queueへ登録します。複数の操作をまとめて依頼でき、完了した順に結果を受け取れます。

ユーザー空間

アプリ

読み込み、書き込み、接続受付などの要求を記述します。

Submission Queue
SQE 0SQE 1SQE 2

アプリがI/O要求を追加

Completion Queue
CQE 0CQE 1CQE 2

カーネルが結果を追加

カーネル空間

Linuxカーネル

要求を処理し、完了した順に結果を返します。

epoll_wait()はI/Oを進められるファイルディスクリプタを返します。io_uringは、I/O操作の依頼と完了結果を二つのqueueで受け渡します。