Programmer's High

epollインタフェースとsignalfd(1)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

「インターネットサーバでのPthreadとepoll」の記事(以下、前記事と呼びます)を書いた時点では、手元の環境がプアなためマルチプロセス/マルチスレッドを採用したサンプルプログラムの真価を発揮させられず、適切に比較できませんでしたが、その後デュアルコアマシンを借りることができたので、改めて比較してみました。 また、比較の際にサンプルプログラムに追加したダミー処理ではシグナルも使用したので、やはりLinuxに追加された signalfd(2) もepollによるイベントループで処理してみました。

本記事のサンプルコードは、以下のリンクよりダウンロードすることができます。記事と合わせてご参照ください。
[ サンプルコード ]

前記事のサンプルプログラム

前記事 ではHTTPサーバを例に並列性/多重性のサンプル実装を5種類提示しました。簡単に振り返ります。サンプルプログラムがデュアルコアシステム上で動作しており、HTTPリクエストがほぼ同時に3件届いた場合のインタリーブ例を挙げてみます。毎回必ずこの通りにインタリーブされるわけではなく、あくまでもそれぞれの動作の違いを示すための一例です( 図1.1 から 図1.5 )。 また、5epoll-multiはCPU数-1個の4epoll(相当)を子プロセスとして起動しますが、デュアルコアシステムでは子プロセスの4epollを1つしか起動せず、4epollとの比較がしにくくなるため、今回は測定用にCPU数分の4epollの子プロセスを起動することにします。

図1.1 インタリーブ例―1base

fig/epoll2-fig1.png
  • シングルスレッドで一セッションづつ処理する
  • 1セッションを完了しないと次のセッションへ進まない、同時に処理しない
  • CPU2を使用しない

図1.2 インタリーブ例―2pthread-unlimited

epoll2-fig2.png
  • スレッド数に上限を設けず、要求を受ける度に新規スレッドを作成し、1スレッドが1セッションを専門に処理する。上例では3スレッドを作成する
  • セッションA、Cを担当する2スレッドは同じCPU上で同時に実行される
  • セッションA、Bを担当する2スレッドはそれぞれ異なるCPU上で実行される

図1.3 インタリーブ例―3pthread-pool

epoll2-fig3.png
  • CPU 数分のスレッドをあらかじめ作成しておき、各スレッドが要求を受け付け逐次処理する。上例では2スレッドを作成済み
  • セッション A、B は並列化されるが、セッションCは逐次的に処理される

図1.4 インタリーブ例―4epoll

epoll2-fig4.png
  • シングルスレッドで epoll によるイベントループを用い I/O を多重化し、複数セッションを同時に処理する
  • 1セッションの完了を待たずに次のセッションの処理を開始する
  • 全セッションが同じ CPU 上で同時に処理される

図1.5 インタリーブ例―5epoll-multi(子プロセス数増加後)

epoll2-fig5.png
  • 上記4epoll.cをCPU数分の、スレッドではなく、子プロセスとして起動し、親プロセスで要求を受け付け、ソケットを子プロセスへ渡す。上例では2プロセスの4epollを起動済み
  • 1セッションの完了を待たずに次のセッションの処理を開始する
  • セッションA、Cは同じCPU上で同時に処理される
  • 2pthread-unlimited.cではシステムが三スレッドをスケジューリングした結果としてセッションA、CがCPU1、セッションBがCPU2上で処理されたが、5epoll-multi.cではそれぞれのCPU上で4epollが動作し、1プロセス内のI/Oの多重化により同時処理が実現されている点が異なる

構造をまとめると次の表になります( 表1.1 )。

表1.1 前記事サンプルプログラムの構造

サンプルプログラム スレッド or プロセス数 要求受付役 同時処理可能 セッション数
1base.c 1   1
2pthread-unlimited.c 無制限 マスタスレッド 無制限
3pthread-pool.c CPU数 各スレッド CPU数
4epoll.c 1   OPEN_MAX
5epoll-multi.c CPU数 + 1 親プロセス CPU数×OPEN_MAX
  • OPEN_MAX = 1 プロセスがオープン可能なファイル数。
  • 比較のため5epoll-multi.cには子プロセス数を増加してある。

人為的な処理追加

HTTPサーバとして前記事のサンプルプログラムを動作させると、その1セッションあたりの内容は若干数のシステムコールだけと言ってもよいものなので、サーバ側でのCPU使用率は user%sys% の内、 sys% ばかりが増加します。さらにサーバ側が負荷らしいレベルに達する前にHTTPクライアント役のCeleronの方が一杯になってしまい、充分な負荷を生成できません。

クライアント役には古いPentium 4も加え2台としましたが、サンプルプログラムにもう少し仕事をさせないと、まともな比較ができません。何もせずに時間だけを稼ぐ sleep(3) を加えようか、いやそれではやはりCPU負荷にはならないし、スケジューリングにもおかしな影響を与えてしまう。ではCPU負荷を与えるために単純な計算、例えば32ビット整数がオーバフローするまで1ずつ足し算をしようか。いやそれではCPUを必要以上に手放さなくなるので、やはりおかしなダミー処理だろう、などと考えた結果、子プロセスを作成し、 /tmpfind(1) することにしました。

ちょうど、手元の環境の /tmpdf -i した時に15個のinodeを使っていることが目に止まり、これを find するとごく短時間で終了するシステムコールばかりを発行することになり、HTTPサーバが動的コンテンツとして純粋な外部モジュールを起動し、ディレクトリリストを得る動作っぽいかなという程度の着想です(そんなモジュールが現実に存在するかは知りませんが)。

1セッションごとに毎回fork/execするのはあまり現実的とは言えませんし、また一般的に考えてもこういう人為的な操作はよろしくないとは思いますが、目的は比較測定と割り切り、この条件を測定環境/方法として固定することにします( 図1.6 )。

図1.6 ダミー処理

epoll2-fig6.png

fork(2) 後の部分(図中では"...")は別のCPU上で実行されることもありますが、HTTPリクエスト読み取りとファイル送信の順序が変るわけではなく、サンプルプログラムの基本的構造に変化はありません。

子プロセス生成により実際にはすべてのサンプルプログラムがマルチプロセス化し、スケジューリングに影響を与えますが、ここでは追及しないことにします。

1base

1baseへのダミー処理の追加は比較的容易です。また、先のインタリーブ例も基本的には変化しません。セッションを同期的に処理するため、ダミーの子プロセスの終了も単にその場で待ち合わせます。先のインタリーブ例( 図1.1 )と比較のため、 図1.7 を示します。

もちろんダミーの子プロセスが別のCPU上で実行されることもあり得ますが、ここではシングルスレッドで同期的に待ち合わせる点に注目してください。

図1.7 ダミー処理の追加―1base

epoll2-fig7.png

2pthread-unlimited

同様にダミー処理を容易に追加できます。ソースコード上の変更は 1baseと変らず、やはり同期的に子プロセスの終了を待ち合わせるにも関わらず、マルチスレッド構造が効果を発揮し、複数のセッションを同時に処理できます。インタリーブを考えると子プロセスの存在が影響を与えますが、前述のようにここでは追及せず、2pthread-unlimitedのソースコード上は同期的に待ち合わせるようになっていても、マルチスレッドのおかげで複数のセッションを同時に処理できる点に注目してください。同様に比較インタリーブ例を示します( 図1.8 )。

図1.8 ダミー処理の追加―2pthread-unlimited

epoll2-fig8.png

(CPU2 上のセッション B は割愛)

3pthread-pool

ソースコード上の変更は1baseと変らず、ダミー処理をやはり容易に追加できます。マルチスレッド構成ですが、CPU数ぶんしかスレッドを作成しないため、それ以上の数のセッションを同時には処理できません。そのため先のインタリーブ例から変化せず、 図1.3図1.7 が合体した形になります。CPU数以上のセッション、すなわち上例のセッションCはセッションAまたはBが終了しないとやはり開始されません。

ソースコードの変更 (1)

1base.c、2pthread-unlimited.c、3pthread-pool.cのソースコード変更は本質的に変らないため、ここでは1base.cのみを説明します。まず、ダミー処理の子プロセスを作成し、 waitpid(2) により同期的に待ち合わせる関数 dummy_busy() を追加します。実際にはepollによるイベントループにも対応する処理がありますが、これについては後述します。もっとも単純な wait(2) に代わり、 waitpid(2) を用いるのもイベントループ対応の一環です。

dummy_busy() 要約 (1)

dummy_busy(sock, do_wait)
{
    pid = fork();
    if (pid > 0){
        /* 親プロセス */
        /* epollの場合 ... 後述 */

        if (do_wait)
            /* 子プロセスの終了待ち合わせ */
            waitpid(pid, &status, 0);
    } else {
        /* 子プロセス―ダミー処理 */
        exec "find /tmp > /dev/null 2>&1";
    }
}

1base.cではHTTPリクエスト読み取り後にdummy_busy()をコールするだけです。詳細は付属サンプルコードを参照してください。

1base.cへのダミー処理追加要約

main()
{
    /*リスニングポート作成... */

    while(1) {
        accept();                 /* 接続受付 */
        read();                   /* HTTPリクエスト読み取り */
+       dummy_busy(/* wait */1);  /* ダミー処理 */
        write();                  /* HTTP レスポンスヘッダ送信 */
        sendfile();              /* 静的ファイル送信 */
        close();                 /* 接続切断*/
    }
}

>> (2)につづく

(初出: H22/2)

Copyright © 2010 SENJU Jiro

本稿で取り上げた内容は、記事の執筆時の情報に基づいており、その後の実装の進展、変更等により、現在の情報とは異なる場合があります。 本連載「プログラマーズ・ハイ」では読者からのご意見、ご感想を切望しています。今後取り上げるテーマなどについてもご意見をお寄せください。

アーカイブ