epoll のサンプルを読んだ

epoll について勉強した。nginx や node.js (libuv) の内部でも普通に使われているらしい (知らなかった)。 具体的にはシステムコールとして提供されている epoll_create, epoll_wait, epoll_ctl について、 epoll の man (https://linuxjm.osdn.jp/html/LDP_man-pages/man7/epoll.7.html) に載っているサンプルにコメントを書いて理解した。 因みに epoll は Linux に特有であり, BSD では kqueue というものが使われているらしい。

#define MAX_EVENTS 10

// listen_sock は listen socket を参照するfdであり、socket(2) の戻り値。たくさんのリクエストを受け付ける。
// 今は省いているが、事前にポートに bind(2) しておく必要がある。
int listen_sock;

// ready になった listen_sock を accept(2) して得られるfd。
// peer からのデータの読み込みや、サーバーからのデータ書き込みは `listen_sock` でなくこれを通して行う。
int conn_sock;

// ブロックが解除された時点での、ready な fd の数であり、後の epoll_wait(2) の戻り値がバインドされる。
// 同時点での `events` のサイズと同じだと思われる。
int nfds;

// `epoll_create1` で内部的に作成された epoll インスタンスを参照する fd。
int epfd = epoll_create1(0);

// listen_sock に対応づいたイベント。
// "listen_sock が読み込み可能になる" をイベントとする。
// また、listen_sock はブロッキングにする。つまり listen_sock が読み込み ready にならない間は、epoll_wait(2) で停止するようにする。
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;

// epoll_event 構造体の配列。listen_sock や conn_sock の Ready イベントがごちゃ混ぜになって入っていく。
// これは多分 epoll_wait(2) の内部だけで変更される。
struct epoll_event events[MAX_EVENTS];

// epoll_ctl は epfd が参照する epoll インスタンスをコントロールする関数。
// 今回は listen_sock を epoll の監視対象に"加える"。以降、listen_sock はずっと epoll の監視下にある
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev)

for (;;) {
    // ここでのみ、events が破壊的に変更される。
    // 具体的には、epoll の監視下にある fd の中で ready なものすべてがキューイングされているが、
    // それが events にダンプされる。(`events` 内の順番は多分保証されてない)
    nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);

    // ready な fd を舐めるループ。
    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {  // listen_sock の ready イベント。
            conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);
            // conn_sock はノンブロッキングにしないと、エッジトリガーの場合に後の epoll_wait(2) がハングする可能性がある
            //  (c.f. https://linuxjm.osdn.jp/html/LDP_man-pages/man7/epoll.7.html)。
            setnonblocking(conn_sock);
            // 読み込み可能をイベントとする。また、エッジトリガーにすることで、conn_sock が "ready になった瞬間" にのみ、
            // 後の epoll_wait(2) が解除される。
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            // conn_sock を監視対象に加える
            epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev)
        } else {
            // do_use_fd() はコネクト済み、かつ読み込み ready になった fd を実際に使う関数。しかし、注意が必要。
            // 例えば fd から read(2) するとき、read(2) が EAGAIN (もうソケットのバッファが空で、次もう一回呼ぶとブロックするぞというエラー値) 
            // を返すまでは繰り返し read(2) すべき。つまり、イベントは確実に消化すべき。そうしないと、peer は自身がすでに送りつけた
            // データ (ソケットバッファに残っているものを含む) に基いて応答を期待している場合に、次の epoll_wait(2) でハングする。
            do_use_fd(events[n].data.fd);
        }
    }
}