epoll, エッジトリガー, EPOLLRDHUP

このエントリーは社内勉強会用の解説記事です。 普通の人は便利な libevlibuv を使っておきましょう。

epoll はいわゆる C10K 問題に対応するための Linux カーネルが提供する仕組みです。といってもこの問題、32bit 時代のメモリが潤沢ではなかった時代のものなので、いまどき並大抵のプログラムであれば、普通に1コネクション1スレッドで問題ないでしょう。@kazuho 先生がそうおっしゃっていました

まあ御託はさておいて、epoll のマニュアル読んでもちょいわからないところを補足しておきます。

レベルトリガーとエッジトリガー

マニュアルに書いてありますが、簡単に言うとレベルトリガーは指定した状態が満たされるかチェックする方式です。つまりソケットの受信バッファにデータがあれば、epoll_wait を何度呼び出しても必ず read ready が返ってきます。

エッジトリガーは read ready をチェックするのではなく、「read ready に今なった」というイベントを通知するものです。つまり、レベルトリガーは状態を検査するのに対して、エッジトリガーは状態の変化を通知します。受信バッファにデータがある場合、エッジトリガーでは一度しか通知されず、次の通知は受信バッファが一度空になったあと、再度データが溜まったときとなります。

エッジトリガーはなにが嬉しいのか

性能・・・よりは、以下の点でしょう。

  1. 複数スレッドで [epoll_wait] したときに、一つのスレッドだけが通知を受け取れる

    c.f. Is level triggered or edge triggered more performant?

  2. EPOLLOUT で書き込み可能フラグをいちいち add/del しなくてよい

    読み込みと違って、ソケットはたいていの場合書き込み可能なので、レベルトリガーだとフラグをいちいちつけ外しする必要があります。 エッジトリガーならフラグつけっぱなしでも大丈夫。

TCP でピアがシャットダウンしたことを検出

マニュアルを見ると、EPOLLHUP というそれっぽいのがありますが、これはエラーが発生したときにあがるフラグです。通常の TCP FIN で終わる場合、エラーではないのでこのフラグは立ちません。

それならと思ってマニュアルを見ると、EPOLLRDHUP というのがあります:

EPOLLRDHUP (since Linux 2.6.17)
Stream socket peer closed connection, or shut down writing half of connection. (This flag is especially useful for writing simple code to detect peer shutdown when using Edge Triggered monitoring.)

これは、悪魔が仕掛けた罠です。

確かにこのフラグを立てると、TCP FIN パケットが飛んできた瞬間に検出できます。でも、思い出してください。TCP FIN は本来先行したはずのデータパケットよりも先に来ることがあり得るってことを。ネットワークが空いているうちは、EPOLLRDHUP でちゃんと検出できているように見えます。でも、混んでいる状況ではピアが送ったデータを全部受け取るよりも前に、FIN を検出してしまうことがあるのです。

正解は、EPOLLINrecv が 0 を返すのをチェックすることです。単純ですね。

epoll を 100 倍使いやすくする仲間たち

ソケットの類に加えて、以下のシステムコールを組み合わせればほとんどの用が足りるでしょう。

  • timerfd

    指定した時刻が経過したらイベントがあがります。

  • eventfd

    誰かが書き込みとイベントがあがります。pipeより軽い。

  • signalfd

    シグナルを受け取るとイベントがあがります。シグナルの情報は read で入手できます。 普通のシグナルハンドラーより便利。

以上。