プロセスの生成
UNIX(Linux)では、プロセスをfork()システムコールで生成します。実行中のプロセスの中でfork()を呼び出すと、今のプロセスに割り当てられているメモリごと複製したプロセスが作られます。そしてfork()関数から戻るところから、それぞれのプロセスに別れて続きを実行します。
fork()の中で複製される側を子プロセスと呼び、元のプロセスを親プロセスと呼びます。プロセスを複製することを、そのまま「フォークする」と呼ぶことがあります。UNIX(Linux)は、fork()により自分を複製することであらゆるプロセスを作り出します。
親プロセスと子プロセスはfork()から戻るときの戻り値がそれぞれ異なります。親プロセスの場合は、複製した子プロセスのPID(プロセスID)が返ります。子プロセスの場合は0が返ります。
次の例は、単純にfork()を呼び出して、その戻り値を表示しています。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
printf("PID=%d\n", pid);
exit(0);
}
この結果は、次のようにprintf()が2つ出力されます。最初の449119は親プロセスが自分のPIDを表示し、複製で作られた子プロセスが0を表示しています。そして、それぞれがexit()で終了します。
PID=449119
PID=0
上の例を少し変えて、getpid()により自分のPIDを表示させ、キー入力待ちで止めてみます。
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
printf("PID=%d %d\n", pid, getpid());
getchar();
exit(0);
}
PID=449571 449570
PID=0 449571
親プロセスのPIDは449570、子プロセスは449571であることがわかりました。そして、psコマンドでプロセス(forktestという名前でコンパイル)を確認してみると、どちらも確認できました。
449570 pts/2 S+ 0:00 ./forktest
449571 pts/2 S+ 0:00 ./forktest
子プロセスにとってfork()の次からがスタートですが、親プロセスが分身したかのように振る舞います。次の例は、子プロセスには親プロセスのそれまでのメモリ状態が引き継がれることをあらわしています。
int main(int argc, char *argv[])
{
pid_t pid;
char *s = "ABCD";
pid = fork();
printf("PID=%d %s\n", pid, s);
exit(0);
}
文字列ABCDの変数は、子プロセスにも引き継がれます。
PID=449224 ABCD
PID=0 ABCD
注意しなければならないのは、開いているファイルも引き継がれることです。親プロセスがファイルを開いたまま子プロセスを生成して、両方のプロセスからファイルを操作した場合、意図しない動きになるかもしれません。
子プロセスを生成したら子プロセスには別の処理を実行させるのが普通なので、ほとんど場合は、次のようにfork()の戻り値でそれぞれの処理に分岐させます。
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid == 0) {
// 子プロセスが実行する
puts("Child");
} else if (pid > 0) {
// 親プロセスが実行する
puts("Parent");
} else {
// エラー(親プロセス)
}
// どちらも実行する
printf("PID=%d\n", pid);
}
上の結果は次のようになります。この例では、プロセス分離の処理がある分、子プロセス側の表示の方が少し遅れたようです。
Parent
PID=452503
Child
PID=0
子プロセスを、分岐した処理ブロックの中で完結する場合はexit()で終了させます。
pid = fork();
if (pid == 0) {
// 子プロセスが実行する
puts("Child");
exit(0);
} else if (pid > 0) {
親プロセスが子プロセスの終了を待つ
親プロセスは、wait()またはwaitpid()により、子プロセスの終了を待ち合わせることができます。
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
親プロセスはwait()を呼び出すと、子プロセスが終了するまで待機します。子プロセスが終了すると、引数wstatusに子プロセスの終了ステータスが返り、戻り値にはそのPIDが返ります。次の例はwait()で子プロセスの終了を待機しています。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid, wpid;
int stat;
pid = fork();
if (pid == 0) {
puts("Child Start");
sleep(5);
exit(2);
}
puts("Wait");
wpid = wait(&stat);
printf("Child Exit pid=%d\n", wpid);
printf("stat=%d %d\n", WIFEXITED(stat), WEXITSTATUS(stat));
}
Wait
Child Start
Child Exit pid=464958
stat=1 2
wait()の引数に戻る子プロセスの終了ステータスは、次のマクロにより状態が確認できます。
WIFEXITED(status)
|
子プロセスがexit()やmain()からの戻りにより、通常に終了した場合0以外を返す。
|
WEXITSTATUS(status)
|
終了ステータスの値を返す。
|
waitpid()は、引数に指定するPIDの子プロセスを待機します。この例ではoptionはとりあえずデフォルトの0を指定します。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int stat;
pid = fork();
if (pid == 0) {
puts("Child Start");
sleep(5);
exit(2);
}
puts("Wait");
waitpid(pid, &stat, 0);
printf("Child Exit\n");
printf("stat=%d %d\n", WIFEXITED(stat), WEXITSTATUS(stat));
}
Wait
Child Start
Child Exit
stat=1 2
optionにWNOHANGを指定すると、子プロセスがまだ終了していない場合、ブロックして待たずにすぐに0を返します。次の例は、waitpid()を繰り返しながら子プロセスの終了を確認しています。
int main(int argc, char *argv[])
{
pid_t pid;
int stat;
pid = fork();
if (pid == 0) {
puts("Child Start");
sleep(5);
exit(2);
}
while(waitpid(pid, &stat, WNOHANG) == 0) {
sleep(1);
puts("Wait");
}
printf("Child Exit\n");
printf("stat=%d %d\n", WIFEXITED(stat), WEXITSTATUS(stat));
}
Child Start
Wait
Wait
Wait
Wait
Wait
Child Exit
stat=1 2
分離した子プロセスが終了したとき、何も実行していなくてもOSの中ではまだプロセスとして登録されたままになっています。この状態のプロセスを「ゾンビ」と呼んでいます。次の例のように、子プロセスが5秒で先に終了して、その後親プロセスがまだ動いている5秒間にpsコマンドで見ると、子プロセスがゾンビを表す「Z」となっていることが確認できます。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int stat;
pid = fork();
if (pid == 0) {
sleep(5);
exit(0);
// Zombie
}
sleep(10);
waitpid(pid, &stat, 0);
}
$ ps ax
465707 pts/0 S+ 0:00 ./forktest
465708 pts/0 Z+ 0:00 [forktest] <defunct>
wait()やwaitpid()は、子プロセスの待ち合わせと同時にプロセスとして完全に消滅させる(ゾンビを回収する)役割があります。ゾンビを放置したまま親プロセスが終了しても、OSが代わって回収してくれるのでゾンビが残ったままにはなりませんが、生成した親プロセスが自分で回収するべきです。
別のプログラムを実行するexec関数
exec関数は、今のプログラムを中断して別のプログラムを実行させます。今のプロセスがexec関数で指定された別のプログラムに成り代わって起動し、そのままexec関数からは返ってきません(エラー時には返ります)。
exec関数は、引数の与え方の種類がいつくがあり、以下が用意されています。
int execl(char *path, char *arg, ..., (char *)NULL);
int execlp(char *file, char *arg, ..., (char *)NULL);
int execle(char *path, char *arg, ..., (char *)NULL, char *envp[]);
int execv(char *path, char *argv[]);
int execvp(char *file, char *argv[]);
int execvpe(char *file, char *argv[], char *envp[]);
execに続くlvpeの文字には次の意味があります。
l
|
コマンドライン引数を可変長引数で与える。
|
v
|
コマンドライン引数を文字列の配列で与える。
|
p
|
パス検索する(フルパス指定しなくてもよい)。
|
e
|
環境変数を指定できる。
|
次の例は、catコマンドをexecl()で実行します。「l」なので可変引数で指定し、「p」が付いてないので、コマンドをフルパスで指定しています。execl()の中でcatコマンドに置き換わるので、最後の「exit」は表示されません。
int main(int argc, char *argv[])
{
execl("/usr/bin/cat", "cat", "/etc/hosts", NULL);
puts("exit");
}
127.0.0.1 localhost
次の例は、文字列の配列で引数を与える「v」が付いたexecv()の場合です。与えられたコマンドライン引数をそのまま表示させるだけのプログラムexectest1を起動し、引数の引き渡しを確認します。
int main(int argc, char *argv[])
{
char *cmd[] = {"exectest1", "100", "abc", NULL};
execv("exectest1", cmd);
}
exectest1
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for (i = 0; i < argc; i++) {
printf("arg%d=%s\n", i, argv[i]);
}
}
arg0=exectest1
arg1=100
arg2=abc
「e」が付いているものは、明示的に環境変数を引き渡すことができます。次の例は、現在の環境変数に、EXEC_ENVという環境変数を追加して引き渡しています。、
extern char** environ;
int main(int argc, char *argv[])
{
setenv("EXEC_ENV", "ABCD", 1);
execle("exectest2", "exectest2", NULL, environ);
}
exectest2
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
printf("env=%s\n", getenv("EXEC_ENV"));
}
env=ABCD
forkとexecを組み合わせる
exec関数は、大抵の場合fork()と共に使います。fork()で生成した子プロセスの処理でexecを実行することで、現在のプロセスの中からコマンドを実行するようなことができます。次の例は、プログラムの中からcatコマンドを起動しています。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
int stat;
pid = fork();
if (pid == 0) {
execlp("cat", "cat", "/etc/hosts", NULL);
// exec fail
exit(-1);
}
waitpid(pid, &stat, 0);
printf("stat=%d %d\n", WIFEXITED(stat), WEXITSTATUS(stat));
}
プログラムの中から別のプログラムを起動するような処理は、だいたいこのような形になります。これに似たものでsystem()という任意のコマンドを実行させる関数があります。これはfork〜execと同じことができますが、一旦シェルを起動してその上でコマンドを実行させています。