TCPServer#accept_nonblockの実装を少し追った

こんにちは。お金がないので、まともに年が越せるのか心配なきたけーです。

ちょっと前まで、社内の読書会でWorking With TCP Socketsという本を読んでいました。Rubyを使ってTCPSocketを使ったプログラミングについて学ぶことができる楽しい本なのですが、ちまちまと読みなおしています。

その中に、「Non-blocking IO」という章があって、「read, write, accept, connect」をノンブロッキングに行うメソッドについて紹介されています。今回の記事では特にaccept_nonblockメソッドに着目して、Rubyがそれをどのように実装しているのか追ってみます。

acceptを用いたプログラム

TCPServer#acceptを用いる。接続を確立するまでブロックする。

require 'socket'

server = TCPServer.new(4481)

loop do
  connection = server.accept
  # ...
end

straceした結果

rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
accept(3,

accpetシステムコールを呼び出して、ブロックしている。

accept_nonblockを用いたプログラム

TCPServer#accept_nonblockを用いる。接続が確立できなかったら、例外が発生する。

require 'socket'

server = TCPServer.new(4481)

loop do
  begin
    connection = server.accept_nonblock
  rescue Errno::EAGAIN
    retry
  end
end

straceした結果

fcntl(ファイルディスクリプタを操作するシステムコール)で、O_NONBLOCKフラグを立てているのが分かります。

fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
fcntl(3, F_GETFL)                       = 0x802 (flags O_RDWR|O_NONBLOCK)
accept(3, 0x7fff7a9c8720, [128])        = -1 EAGAIN (Resource temporarily unavailable)
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
fcntl(3, F_GETFL)                       = 0x802 (flags O_RDWR|O_NONBLOCK)
fcntl(3, F_GETFL)                       = 0x802 (flags O_RDWR|O_NONBLOCK)
accept(3, 0x7fff7a9c8720, [128])        = -1 EAGAIN (Resource temporarily unavailable)

accept_nonblockの実装を追う

Rubyソースコードを読んでみましょう。対象のRubyは2.0.0-p353です。

TCPSocket#accept_nonblockの実装は ext/socket/tcpserver.c#L92 にあるのがわかります。

実際にファイルディスクリプタを操作する処理は、rsock_s_accept_nonblock でおこなっていそうです。
...rsock_s_accept_nonblockの実装は ext/socket/init.c#L518にありました。
ここでもファイルディスクリプタの操作はおこなっていなさそうです。

rb_io_set_nonblock(fptr);

に当たりをつけて、さらに潜り込みます。実際に操作をおこなっているのは rb_io_set_nonblock のようですね。io.c#L2351

rb_io_set_nonblockで検索をかけると、connect_nonblockのようなaccept_nonblock以外のノンブロッキング関連の複数メソッドがこれを利用していることが分かります。

例外について

rsock_s_accept_nonblockに戻ると、cloexec_acceptで実際にacceptシステムコールを呼び出し、その返り値をチェックしていることが分かります。システムコールがCの世界で「エラーを数値」として扱っているのに対して、それをどのようにRubyの世界の「例外」にしているのでしょうか? おなかもすいてきたので、これは次の機会に( error.c#L1899eval.c#L527 の周辺を追っかければよさそう)。

まとめ

accept_nonblockconnect_nonblockなどのいわゆるノンブロッキング関連のメソッドは、ファイルディスクリプタを操作するためにrb_io_set_nonblock を共通に利用しているのでした。
RubyはCのシステムコールやライブラリなどをオブジェクト指向の扱いやすいインターフェイスで提供してくれていますが、それがどのように実現しているのか知るのは面白いです。少し話がズレますが、ActiveRecordRDBMSの違いを吸収して、汎用的なインターフェイスを提供しています。便利な素晴らしいインターフェイスをただ使うのではなく、興味を持ったところから裏側で何が行われているのか知ることで、そのライブラリやツールを自信を持って扱うことができるのではないでしょうか。