これは何?
ソケットにおけるノンブロッキングI/OについてPumaのコードを絡めて調べてみたまとめです。
PumaはRubyで書かれたOSSのWebサーバーです。
ソケットに限らずノンブロッキングI/Oという言葉をよく聞きます。ですがイマイチなにを意味しているのかわからなかったので調べてみました。
ブロッキングI/Oとは
ユーザーモードからシステムコールをカーネルに発行し、結果が帰ってくるまでに待ちが発生するI/O処理のことです。ファイル全般に言えることであり、ソケットもファイルなので当然ブロッキング発生します。
ソケットにおけるブロッキングI/Oは以下のようなものがあります。
-
accept(2)
でlistenキューからソケットを取り出すときに接続が確立されていない場合(TCPハンドシェイク中など)は確立されるまでブロックされる。 - ソケットに対して
read(2)
やwrite(2)
を実行するとソケットバッファにデータが送信し終わるまでブロックされる。
ノンブロッキングI/Oとは
ブロッキングが起きるのはシステムコールを発行した後「ソケットの準備ができるまで待つ」ことが原因です。そこでノンブロッキングI/Oではこの「待つ」ということをせずシステムコールを発行して準備ができていなければエラーとともにプロセスに制御を直ちに返してもらいます。これにより「ソケットの準備ができるまで待つ」時間がなくなります。
とはいっても何度も成功するかわからないノンブロッキングI/Oを試すのは大変です。事前にI/Oの準備ができているかを確認しておきたいです。
このために生まれた手法がI/Oの多重化です。
I/Oの多重化
複数のファイルディスクリプタ(プロセスとファイルの接続)をチェックして準備完了になったファイルディスクリプタを返すことです。ソケットもファイルなのでこの方法を使うことができます。
RubyではIO.select
を用いてselect(2)
を発行することでソケットを複数チェックし、listen中のソケットなら接続が確立しているかどうか、確立されていればソケットバッファにデータが到着しているかどうかをあらかじめ確認できます。
I/Oの多重化を行うことで複数のソケットから準備ができているものだけに限定できるので、何度もノンブロッキングI/Oを試してエラーを発生させるという事をしなくてすむようになります。
実際のPumaのコード
抜粋して載せています。
ノンブロッキングでaccept(2)
しているコード
while @status == :run
begin
ios = IO.select sockets #1
ios.first.each do |sock|
if sock == check
break if handle_check
else
begin
if io = sock.accept_nonblock #2
client = Client.new io, @binder.env(sock)
#1
ではIO.select
を使ってselect(2)
を発行しています。複数のソケットをチェックし接続の確立を確認しています。
#2
ではIO.accept_nonblock
を使ってノンブロッキングなaccept(2)
を行っています。#1
でI/Oの多重化を行って接続が確立しているソケットを確認しているので、ソケットがとじてしまっている他はエラーは帰ってこない…ハズ。
ノンブロッキングでread(2)
しているコード
PumaではReactor
というクラスでリクエストをモニターしてselectを行っています。1
def run_internal
monitors = @monitors
selector = @selector
while true
begin
ready = selector.select @sleep_for #3
#3
でのselector
はnio4rというライブラリのNIO::Selector
というselect(2)
周りを請け負うクラスのインスタンスです。複数のソケットを内部でモニターしています。
# NIO::Selector#selectの内部
ready_readers, ready_writers = Kernel.select(readers, writers, [], timeout)
実際にノンブロッキングでのread(2)
を行っているコードは以下です。
https://github.com/puma/puma/blob/6a39d41094823c8929f4a31613a2f6a53804997f/lib/puma/client.rb#L344
begin
chunk = @io.read_nonblock(want) #ノンブロッキングI/O
rescue Errno::EAGAIN
return false
rescue SystemCallError, IOError
raise ConnectionError, "Connection error detected during read"
end
このようにaccept(2)
やread(2)
どちらのケースでもI/Oの多重化とノンブロッキングI/Oが行われていることがわかります。
参考
https://github.com/puma/puma
nio4r Getting Started
Working With TCP Sockets
Rustで始めるネットワークプログラミング
Rubyで学ぶWebサーバーアーキテクチャ(Preforking, ThreadPool, イベント駆動モデル)
-
Reactorは他にもHTTPヘッダーとボディが全て到着したかどうかのチェックも行っています。リクエストが処理可能になるとtodoとして処理を待つキューに入れられます。https://github.com/puma/puma/blob/master/docs/architecture.md ↩