こちらは「C 言語コンプレックスを払拭したいシリーズ」の派生記事である。
fork (2) の簡単なサンプル
Linux では fork (2)
を使ってプロセスを複製できる。fork()
のプロトタイプは次の通りだ。
#include <unistd.h>
pid_t fork(void);
このシステムコールを実行すると、プロセスの情報が複製される。システムコールをコールしたプロセスは親と呼ばれ、複製されたプロセスは子と呼ばれる。fork()
の返り値は親プロセスの場合は子プロセスの ID であり、子プロセスの場合は 0
である。
fork()
を使うもっとも簡単なサンプルは次のようなものだろう。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if (pid == 0) {
printf("Hey, I'm a child process\n");
} else {
int status;
waitpid(pid, &status, 0); // 親は子を待つ必要がある
printf("I spawned PID=%d, and it's done!\n", pid);
}
exit(0);
}
これの実行結果は次のようになる。
$ gcc my-fork.c
$ ./a.out
Hey, I'm a child process
I spawned PID=12997, and it's done!
このサンプルでは親プロセスが waitpid()
で子プロセスの終了を待っている。子プロセスの仕事が終了しているにも関わらず、親プロセスが wait()
もしくは waitpid()
しない場合、子プロセスは終了ステータスを保持し続ける必要があるため、ゾンビプロセスとして残り続けることになる。このサンプルではすぐに親プロセスが終了するので waitpid()
をコールする必要はないが、子を待つコードは常に入れ込むようにしておく方がよい。
fork (2) と exec (2)
よく fork()
と一緒に使われるシステムコールに exec()
がある。これはプロセスを別のプログラムで上書きするシステムコールである。fork()
で作成した子プロセスで別なプログラムを exec()
する、という使われ方をする。
exec()
は成功すると処理が戻らない。失敗した場合は -1
が返る。
先程のサンプルに exec()
で適当な処理をさせてみよう。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if (pid == 0) {
printf("Hey, I'm a child process\n");
execl("/bin/cat", "cat", "/etc/timezone", NULL);
// execl() が戻るときは失敗
perror("/bin/cat");
exit(1);
} else {
int status;
waitpid(pid, &status, 0);
printf("I spawned PID=%d, and it's done!\n", pid);
}
exit(0);
}
これの実行結果は次のようになる。
$ gcc my-fork2.c
$ ./a.out
Hey, I'm a child process
Etc/UTC
I spawned PID=13163, and it's done!
fork() とゾンビプロセス
前述のように、fork()
後に親プロセスが wait()
しない場合、子プロセスが死にきれずゾンビになる。
ところで、親が wait()
する以外にもゾンビを避ける方法がある。三世代でプロセスを管理するのだ。ふつうの Linux プログラミング 第2版では、これを「ダブル fork」と呼んでいる。
「ダブル fork」は次のように機能する。まず親が子プロセスを作り、子プロセスは孫プロセスを作った後ですぐに exit()
する。そうすると、孫の処理終了時にはステータスを返却する先のプロセスが存在しない (すでに exit()
している) ので、カーネルは孫プロセスの終了時にプロセスを片付けてくれる。