3-3 マルチプロセス


プロセスとは

課題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() したままであると、どちらのプロセスも同じ処理しかしないので、通常 fork() 後、それぞれのプロセスに自分が親か子かを調べさせ、それに応じて異なった処理をさせる。 例えば csh などのシェル・プログラムは、ユーザが指示したプログラムを実行するのに fork() を使っている。

子プロセスは別なプログラムを実行し、親プロセスはその終了を wait() システムコールで待つ。 wait() は、子プロセスが exit() システムコールで終了するまで、呼び出した親プロセスを一時停止させるためのシステムコールである。 exit() の引数の整数値が、wait() の返り値となる。


シグナル

親プロセスと子プロセスは、せっかく並行して動いているのである。 親プロセスが子プロセスの終了を検知するのに、wait() を呼んで、何もせずにじっと待つのは望ましくないときもある。

親プロセスが別の処理をやっている間も、子プロセスの終了を、適宜、検出できるように、シグナルという機構が用意されている。 これはハードウェア割り込みをまねた機構で、ある条件が成立したら、あらかじめ登録しておいた関数 (シグナル・ハンドラ) を OS に呼ばせる、という機構である。 シグナルが発生したとき実行されていた関数は一時停止し、シグナル・ハンドラが終了した後、再開される。

なおシグナル・ハンドラは、親プロセスの一部として実行される。 シグナル・ハンドラを呼ぶのに新しいプロセスが作られるわけではない。 それまで親プロセスが実行していた処理を一時強制的に中断し、シグナル・ハンドラを実行する。 中断された処理は、シグナル・ハンドラが return で終了するまで、中断したままで、途中で並行に動きだすことはない。

Web サーバを fork() でマルチプロセス化する場合には、子プロセスの終了をこのシグナル機構を使って検出する。


典型的なサーバプログラム

fork システムコールを使った典型的なサーバは、accept() でクライアントと接続した後、fork() システムコールを使って、新しいプロセスを生成する。 新しく作られたプロセス(子プロセス)は、元のプロセス(親プロセス)の完全なコピーである。メモリの内容、open しているファイル・ディスクリプタ、など全てコピーされる。 fork の終了後は、親プロセスも子プロセスもともに fork() の次の行から実行を続行する。

fork() 終了時点では、親プロセスも子プロセスもまったく同じである。 しかし、fork は返り値として、親プロセスには子プロセスの process ID を、子プロセスには0を返すので、これを用いて、その後は別々の処理をおこなわせることができる。

ここで注意しなければいけないことは、親プロセスと子プロセスはメモリを共有しない、ということである。fork() 直後のメモリの内容は同一だが、その後、例えば親プロセスがメモリの内容を更新したとしても、その更新は子プロセスのメモリには反映されない。

同時に複数のクライアントの相手をするためには、accept() で接続したクライアントの相手を子プロセスにまかせ、親プロセスは再び accept() を実行して、別なクライアントからの要求を待てばよい。処理の流れは次のようになるだろう。

子プロセスは read/write システムコールで I/O 待ちのためブロックするかもしれないが、その間は Unix のプロセス・スケジューラによって自動的に親プロセスが実行される。 このように fork() システムコールを使って、処理を複数のプロセスに割り当てるようにすると、I/O 待ちを意識して明示的に処理を切り替えなくても、自然に through put を高めることができる。

上の図で、accept() の後の親プロセス側の close() は、accpet() が返してきたファイル・ディスクリプタを close するためのものである。

fork() すると socket も二重化されるが、子プロセスの側で close しても親プロセスの側は close されない。 このため、親プロセスも明示的に close() しないと socket がいつまでたっても完全に消滅しないという状況に陥ってしまう。



fork() の使用例

fork() の使い方を簡単な例を使って説明する。細かい用法については man で調べてほしい。

[動作]

次の (1) から (2) を繰り返す。

[プログラム fork.c]


課題 4

fork システムコールを使って、課題3で作成した web server をマルチプロセス化せよ。

正しくマルチプロセス化できたかどうかは、 webclient.c を使って確かめよ。

とし、作成した web サーバから /index.html を 2 回読み出せるかどうか確かめよ。 マルチプロセス化できていないと、プログラムは途中で停止してしまう。




目次へ戻る

Copyright (C) 1999-2000 Shigeru Chiba

Email: chiba@is.tsukuba.ac.jp