LoginSignup
28
12

More than 3 years have passed since last update.

PumaのコードからみるソケットへのノンブロッキングI/O

Last updated at Posted at 2019-12-12

これは何?

ソケットにおけるノンブロッキング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でのselectornio4rというライブラリの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, イベント駆動モデル)


  1. Reactorは他にもHTTPヘッダーとボディが全て到着したかどうかのチェックも行っています。リクエストが処理可能になるとtodoとして処理を待つキューに入れられます。https://github.com/puma/puma/blob/master/docs/architecture.md 

28
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
12