こちらは「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() している) ので、カーネルは孫プロセスの終了時にプロセスを片付けてくれる。