mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Linux Programming、epollの話

お久しぶりです、初めての日本の夏に圧倒されているトールマエサカです。

今日はLinuxにおけるネットワークプログラミング関連のネタです。分散データベースサーバの開発過程で最近よくLinuxのepollというイベントハンドリング機能を使っています。これがまた優秀な機能なので紹介します。

このContextでいうイベントハンドラーはサーバがクライエントのリクエストを処理するためのメカニズムです。イベントの感知と通知は大雑把にいうと以下の三つの処理で構成されています:

  • 一つもしくは複数のディスクリプタを監視
  • ディスクリプタの準備が整うまでハチ公のごとくひたすら待ち続ける
  • 準備が整ったディスクリプタの通知

アプリケーションでの実装は一昔までselect(2)、もしくはpoll(2)というシステムコールで行われていました。二つとも役目は同じですがselect(2)の場合、kernelをいじらない限りディスクリプタセットのサイズは一定という制限があります。対してpoll(2)の場合、ディスクリプタを纏める配列のサイズはプログラマが設定するため、このような制限はないといわれています。 両方ともアプリケーションに呼ばれる度に監視下のディスクリプタセットをkernelに渡し、kernelは渡されたリストに対しstateを更新し送り返します。送り返されたディスクリプタ達からready状態のディスクリプタをピックアップするためにアプリケーションはディスクリプタ達を一つ一つ調べます。つまりディスクリプタの数が多くなればなるほど性能が劣化するという事ですね。

epollの違い

すでにkernelが知っている情報をアプリケーションで再計算する従来の手法に対しLinux Kernel 2.6から追加されたepollではディスクリプタのstateがkernel内で管理されます。select(2)やpoll(2)の様に呼ばれる度にディスクリプタセットをkernelに送る必要もありませんし無駄にディスクリプタセットをループする必要がなくなります。又、edge-triggeredインターフェイスとしても使えるため、kernelの負担を多少下げることも可能。パフォーマンスの方はselect(2)とpoll(2)のtime complexityがO(n)に対しepollはO(1)と無視のできない性能の差を実現しています。

性能のベンチマーク結果は様々なサイトで報告されていますが、libeventのチャートを見ると凄さが解っていただけると思います。epollは正義の味方のmemcachedの中でもlibevent経由で使われています。サンプルコードや詳しい内容はmanに書かれているので是非お時間のある時に読んでみてください。読んだ事がなければワクワクさせてくれる内容です。

epollのもう一つ素晴らしいところは使い方がとても簡単という事です。基本的な使い方をここで紹介しますが、ハードコアな見本はuserverという複数のイベントハンドラーが扱える小型のウェブサーバーやlibeventのソースコードがお勧めです。又、検索してみたらodz bufferさんによる簡潔な例があったのでこれも参考になるかと思います。

Linux以外の場合

epollのメカニズム自体はLinuxの特権ではなく、例えばBSDにはkqueueというメカニズムがあります。自分はあまり詳しくないのでここは触れないでおきますが、この分野に興味のある方はDan Kegel氏のThe C10K Problemを読むと楽しいですよ (お決まりのレコメンドですみません…) Solarisの /dev/poll の話も含まれています。

使い方

さて使い方ですが、epoll_create(2), epoll_ctl(2), epoll_wait(2)を使って扱います。基本的にステップは三つあります。 epollのディスクリプタを作る。

int epfd;
if((epfd = epoll_create(MAX_EVENTS)) < 0) {
    fprintf(stderr, "epoll_create()\\n");
    exit(1);
}

作成したディスクリプタをepollに追加。

struct epoll_event event;
int sock;

/* get_socket()系は自前で実装が必要 */
if((sock = get_socket()) < 0) {
    fprintf(stderr, "get_socket()\\n");
    exit(1);
} 

memset(&event, 0, sizeof(event));
ev.events  = EPOLLIN | EPOLLET; /* "man epoll" から拝借 */
ev.data.fd = sock;

/* ソケットをepollに追加 */
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &event) < 0) {
    fprintf(stderr, "epoll_ctl()\\n");
    exit(1);
}

後は待ち続ける。出番がきたらそれ相応の処理を行う。

int nfd, i;
struct epoll_event events[MAX_EVENTS];

while(1) {
    nfd = epoll_wait(epfd, events, MAX_EVENTS, EP_TIMEOUT);

    if(nfds < 0) {
        fprintf(stderr, "epoll_wait()\\n");
        exit(1);
    }
    for(i = 0; i < nfd; ++i) {
       /* ディスクリプタ達を処理。 */
    }
}

上記のコードはmanのlevel-triggeredインターフェイスでepollを使った場合のサンプルをフォローしていて、 for文の中身が気になる方はmanにもう少し詳しいコードが載っています。

ミクシィ開発部で使われている言語の99%がPerlと思われがちですが、弊社ではこういったコアに近いレベルの開発も実は行っています。

毎日mikio氏に色々と教わりながら面白おかしくハックしている今日この頃です。