カーネル/VM Advent Calendar 一日目: eventfd, timerfd, signalfd

ひ、日付? な、なんのことです…!?

最初はfutex()とかカーネルの変更でpostgresqlが遅くなったとかの話をからめようかと思ってたんですがfutex()調べてたらどんどん大きくなってしまったので今回はスキップして(そのうちあるであろう)2回目にまわしとくことに。

ということで、今回はfutex()から似たところ?で、 epoll, kqueue, *fdあたりを簡単にまとめてみようと思います。

select

まずはselect()のおさらいを。

こんなふうにサーバにクライアントが5つぶらさがっています。クライアントは常時接続はしていますが、ずっとサーバと通信しているわけではありません。時々サーバにリクエストを投げ、サーバは適宜リクエストに応答します。サーバはsocketを作ってクライアントと通信をしています。

言うまでもないことですが、こんなコードではうまく動きません。

for(;;){
  for(i=0; i < NCLIENTS; i++){
    recv(sock[i], buf, len, 0);
    ...
  }
}

クライアント0 からのリクエストを待ってblockしている間に、他のクライアントからのリクエストが来ても全く応答することができません。

かといって、MSG_DONTWAIT を使ってnon-blockにしてsleepするようにしても効率はよくありません。

for(;;){
  for(i=0; i < NCLIENTS; i++){
    recv(sock[i], buf, len, MSG_DONTWAIT);
    ...
    sleep(1);
  }
}

そこで select() を使います。

fd_set rfds;
FD_ZERO(&rfds);
for(i=0; i < NCLIENTS; i++)
  FD_SET(sock[i], &rfds);
while(select(nfds, &rfds, NULL, NULL, NULL) > 0){
    ...
}

rfdsというファイルデスクリプタの集合にsocketを登録し、select()を呼び出すと(この場合は) 読みこみができる状態になったファイルデスクリプタの数を返してくれます。

すぐに読みこみ可能なファイルデスクリプタが存在しない場合には、どれかが読みこみ可能になるまでblockします。なので、select()が返ってきたところで適宜クライアントのリクエストに応答していけばいいわけです。

epoll

さて、select()ではファイルデスクリプタの集合をユーザランド側で管理しています。これを毎回 select()でカーネルに渡し、カーネル側では呼び出されるごとに、このファイルデスクリプタの集合をスキャンしています。 この実装ではファイルデスクリプタの数が増えていくと、どんどんとselect()の処理は重くなっていきます。

そこでLinuxでは epollというシステムコールを使います。

int epollfd = epoll_create(10);
ev.events = EPOLLIN;
ev.data.fd = sock[i]
epoll_ctl(epollfd, EPOLL_CTL_ADD, sock[i], &ev);
while(epoll_wait(epollfd, events, 10, -1) > 0){
    ...
}

epollでは epoll_createを使ってカーネル側に監視するファイルデスクリプタを管理するためのデータを作らせます。 そして返り値として、後の操作に使うepoll用のファイルデスクリプタを受け取ります。 このepollfdをepoll_ctl()に渡して監視対象とするファイルデスクリプタを登録します。カーネル側が監視すべきファイルデスクリプタを知っているので、select()のようにファイルデスクリプタを全てスキャンしなおす必要はなく、パフォーマンスが改善されています。

timerfd

さてはて、ファイルやソケット以外にも知りたい「外からの通知」というものはあります。 たとえばタイマー・シグナルなどです。これも epollで監視できたら便利ですね。 というわけで、 tiemrfd, signalfd, eventfdといったシステムコールでファイルデスクリプタ経由で「通知」をうけとることができるようになります。

まずはタイマーの通知をうけとるための timerfdです。 timerfd_create()でファイルデスクリプタを作成し、timerfd_settime()で「最初にタイマー通知が行なわれる時刻」と「その後タイマーが次に通知されるまでのインターバル」(もしくは0を設定してタイマーをくりかえさない)が設定できます。 両方ともに0が設定されるとタイマーがオフになります。

このファイルデスクリプタをread()すると前回のread()またはタイマーを設定した時から、タイマーに設定した時間が過ぎた回数が返ってきます。 もしも、まだ設定した時間が経過していない時には、その時刻までread()はblockします。もちろん、これを epollやselectで監視するファイルデスクリプタとして設定できます。

signalfd

signalfd()を使うと指定したシグナルを監視するファイルデスクリプタを作ることができます。これをread()すると、struct signalfd_siginfoの(配列の)形式でシグナルの情報が読みとれます。 これもselect, epollに指定することができます。

ただし、事前にsigprocmask()でデフォルトの動作が起こらないようにしておかないとよくわからない挙動になるかもしれません。詳しくはこのへんを unix のシグナル処理あれこれ - Emacs ひきこもり生活

eventfd

eventfdはシンプルなイベントの通知を行なうことができるファイルデスクリプタです。たとえば、一方がpipeをreadしてblockし、もう一方のプロセスがそのpipeにwriteすることでblockが解除されて、readしていたプロセスが動き出すようなコードを考えます。pipeに書いているデータには特に意味はなく、相手のプロセスを起こすことにしか使われていません。これだけのためにpipeを使うのは効率が悪いのでシンプルなeventfdを使うとよいという感じです。

eventfdには64bitの整数が関連つけられていて、read()した時にこの値が0であるとblockします。write()では同じく64bitの正数を書いて、その値がeventfdに関連つけられている整数に加算されます。この時、read()でblockしていたプロセスは、write()されていた値を受け取り、eventfdの整数は0に戻ります。

eventfdは様々なカーネルからの通知を受け取るのにも使うことができます。たとえば、eventfdを作り、cgroupのmemory.usage_in_bytesを開いて、cgroup.event_controlに" "を書くと、メモリ使用量がこのthresholdをクロスするたびに(thresholdより上から下になったり、その逆になるごとに)eventfdにイベントが通知されます。

ほかにもeventfdへのイベント通知で、KVMのguestにinterruptが起こるような設定をすることもできます。

kqueue

わぁ、いろんな *fdを使うことでいろんなものをepollで効率よく監視できるようになってちょー便利ですね。 でも、epollはLinuxでしかありません。 FreeBSDではどうするの? kqueue() を使います。

kqueue()では単体で以下のイベントが監視できます

  • ファイルデスクリプタがread()できるようになった
  • ファイルデスクリプタがwrite()できるようになった
  • ファイルが(削除された|書かれた|サイズが増えた|属性が変わった|リンクがふえた|名前が変わった|revokeされた)
  • プロセスが(exitした|forkした|execした)
  • シグナルをうけた
  • タイマーイベント
  • その他ユーザレベルのイベント

うわぁ…便利……