課題3で作成した web server は、browser からの要求を一度にひとつづつ accept() で受け付け処理した。 しかしネットワーク I/O は比較的遅い処理なので、これでは CPU が I/O の終了を待って何もしない時間がけして短くない。
そこで CPU が I/O 待ちをしている間は、別の browser からの要求を処理するようにすれば、全体としての thoughput を向上させることができる。 このような処理は、select() システムコールを使えば実現できるが、プログラムが非常に複雑になってしまう。 同時に複数の要求を受け付けるようなサーバプログラムは、今回説明する fork() システムコールを使って書くとよい。
プロセス (process) とは Unix の用語で、実行中の個々のプログラムのことである。 Unix は一度に複数のプログラムを、自動的に切替えながら、並行して実行できるので、同じプログラムをふたつ同時に実行することもできる。 このため、プログラムと、実行中の「それ」を区別するために、プロセスという言葉が使われる。
ではプロセスとは具体的に何なのだろうか。 プロセスとは、実行されているプログラムの他、そのプログラムを実行するために OS によって割り当てられたメモリ領域や、現在接続中のソケットや読書き中のファイルなど、I/O 装置の利用状況の集合体である。 OS はプロセスごとに、これらの情報を管理して、実際に CPU を使って走らせるプロセスを適宜切りかえ、全てのプロセスが全体として並行に動作するようにしている。
OS がプロセスを切り替えるタイミングのひとつは、read(), write() システムコールを実行したときである。 このとき、I/O 処理の完了を待って、しばらく CPU が何もすることがなくなると、OS はプロセスを切り替える。 もうひとつの主要なプロセス切り替えのタイミングは、ひとつのプロセスが一定時間連続して動き続けたときである。 このときも OS は別なプロセスに強制的に切り替えて、並行実行を達成しようとする。
新しくプロセスを作るには fork() システムコールを使う。 このシステムコールは、fork() を実行したプロセスのコピーを作り、元のプロセスと合わせてふたつのプロセスを並行して走らせる。 ここで大切なことは、新しいプロセスを作って別なプログラムを先頭から実行するのではなく、プロセスのコピーを作る点である。
コピーであるので、fork() 実行直後には、同じプログラムの同じ場所を実行中のプロセスがふたつできることになる。 fork() を実行したプロセスを親プロセス、fork() によって生成されたプロセスを子プロセスと呼ぶ。
fork() したままであると、どちらのプロセスも同じ処理しかしないので、通常 fork() 後、それぞれのプロセスに自分が親か子かを調べさせ、それに応じて異なった処理をさせる。 例えば csh などのシェル・プログラムは、ユーザが指示したプログラムを実行するのに fork() を使っている。
↓ fork() ------- | | 親プロセス 子プロセス | | wait() 指示されたプログラムを実行 : | : exit(1) : | : <---------- ↓
子プロセスは別なプログラムを実行し、親プロセスはその終了を wait() システムコールで待つ。 wait() は、子プロセスが exit() システムコールで終了するまで、呼び出した親プロセスを一時停止させるためのシステムコールである。 exit() の引数の整数値が、wait() の返り値となる。
親プロセスが別の処理をやっている間も、子プロセスの終了を、適宜、検出できるように、シグナルという機構が用意されている。 これはハードウェア割り込みをまねた機構で、ある条件が成立したら、あらかじめ登録しておいた関数 (シグナル・ハンドラ) を OS に呼ばせる、という機構である。 シグナルが発生したとき実行されていた関数は一時停止し、シグナル・ハンドラが終了した後、再開される。
なおシグナル・ハンドラは、親プロセスの一部として実行される。 シグナル・ハンドラを呼ぶのに新しいプロセスが作られるわけではない。 それまで親プロセスが実行していた処理を一時強制的に中断し、シグナル・ハンドラを実行する。 中断された処理は、シグナル・ハンドラが return で終了するまで、中断したままで、途中で並行に動きだすことはない。
Web サーバを fork() でマルチプロセス化する場合には、子プロセスの終了をこのシグナル機構を使って検出する。
fork システムコールを使った典型的なサーバは、accept() でクライアントと接続した後、fork() システムコールを使って、新しいプロセスを生成する。 新しく作られたプロセス(子プロセス)は、元のプロセス(親プロセス)の完全なコピーである。メモリの内容、open しているファイル・ディスクリプタ、など全てコピーされる。 fork の終了後は、親プロセスも子プロセスもともに fork() の次の行から実行を続行する。
fork() 終了時点では、親プロセスも子プロセスもまったく同じである。 しかし、fork は返り値として、親プロセスには子プロセスの process ID を、子プロセスには0を返すので、これを用いて、その後は別々の処理をおこなわせることができる。
ここで注意しなければいけないことは、親プロセスと子プロセスはメモリを共有しない、ということである。fork() 直後のメモリの内容は同一だが、その後、例えば親プロセスがメモリの内容を更新したとしても、その更新は子プロセスのメモリには反映されない。
同時に複数のクライアントの相手をするためには、accept() で接続したクライアントの相手を子プロセスにまかせ、親プロセスは再び accept() を実行して、別なクライアントからの要求を待てばよい。処理の流れは次のようになるだろう。
------- | | | ↓ | accept() | | | fork() ------- | | | | 親プロセス ↓ | | 子プロセス | close() | | | read() ------- | write() | close() | exit() プロセスの終了
子プロセスは read/write システムコールで I/O 待ちのためブロックするかもしれないが、その間は Unix のプロセス・スケジューラによって自動的に親プロセスが実行される。 このように fork() システムコールを使って、処理を複数のプロセスに割り当てるようにすると、I/O 待ちを意識して明示的に処理を切り替えなくても、自然に through put を高めることができる。
上の図で、accept() の後の親プロセス側の close() は、accpet() が返してきたファイル・ディスクリプタを close するためのものである。
fork() すると socket も二重化されるが、子プロセスの側で close しても親プロセスの側は close されない。 このため、親プロセスも明示的に close() しないと socket がいつまでたっても完全に消滅しないという状況に陥ってしまう。
fork() の使い方を簡単な例を使って説明する。細かい用法については man で調べてほしい。
次の (1) から (2) を繰り返す。
(1) 標準入力から整数を読む。
(2) 子プロセスを作る。子プロセスは、(1)で指定された秒数後に終了 (exit) する。
#include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <stdio.h> void sigchld_handler(int); int main() { int sec; signal(SIGCHLD, sigchld_handler); /* シグナルハンドラの登録 */ for (;;) { int pid; scanf("%d", &sec); /* 秒数を読む */ pid = fork(); /* 子プロセスを作る */ if (pid < 0) { /* fork失敗 */ perror("fork"); exit(1); } else if (pid == 0) { /* * 子プロセスの動作。sec秒sleepするだけ。 */ fprintf(stderr, "hello\n"); sleep(sec); fprintf(stderr, "bye\n"); exit(0); /* 子プロセスを終了する */ /* ここでexitするのを忘れないこと */ } /* 子プロセスは上でexitするので、この部分に */ /* たどりつくのは、親プロセスだけ */ } } /* * SIGCHLDのためのシグナルハンドラ * * 子プロセスは終了すると、シグナル SIGCHLD を親プロセスに送り、 * 終了を通知する。親プロセスはこのシグナルを処理する手続きを * 用意しなければならない。この手続きの中では、システムコール wait() * または waitpid() を呼んで、子プロセスの終了コード (exit() の引数) * を受けとならければならない。これをやらないと、子プロ * セスは永遠に終了せず、システムの資源を浪費してしまう。 */ void sigchld_handler(int x) { int chld; /* * 死んだ子プロセスの後始末をする。SIGCHLD はプロセスの状態が変化し * たとき送られることになっており、SIGHCHLD を受け取っても、子プロ * セスが死んでいるとはかぎらない。そこで WNOHANGを指定して、ブロック * しないようにしている。 */ chld = waitpid(-1, NULL, WNOHANG); if (chld == -1) { /* wait失敗 */ perror("wait"); exit(1); } /* * OS によってはシグナルハンドラを再登録しないと、次にシグナルを * 受けたときにシグナルハンドラが呼ばれないことがある。その場合は * 最後に次のようにシグナルハンドラの再登録をおこなう。 * * signal(SIGCHLD, sigchld_handler); */ }
fork システムコールを使って、課題3で作成した web server をマルチプロセス化せよ。
正しくマルチプロセス化できたかどうかは、 webclient.c を使って確かめよ。
% gcc -o webclient webclient.c % ./webclient hostname port-number
とし、作成した web サーバから /index.html を 2 回読み出せるかどうか確かめよ。 マルチプロセス化できていないと、プログラムは途中で停止してしまう。
Copyright (C) 1999-2000 Shigeru Chiba
Email: chiba@is.tsukuba.ac.jp