プロセスは別のプロセスから生まれる
プロセスは、動いているプログラム1つ分の単位です。Linux では、シェルで打ったコマンドも、別のプロセスが fork で自分を複製して作ります。
fork、exec、wait の3つの呼び出しを知ると、プロセスが木の形に並ぶ理由と、終了ステータスの意味が分かります。
このレッスンのゴール
メモ
forkとexecとwaitで、プロセスが別のプロセスを始める流れを説明できる。
重要な言葉
| ことば | いみ |
|---|---|
| プロセス | 動いているプログラム1つ分の単位 |
| fork | 呼んだプロセスを複製して、子を作る呼び出し |
| exec | 今のプロセスを、別のプログラムに置き換える呼び出し |
| wait | 子が終わるのを待ち、終了ステータスを受け取る呼び出し |
| 終了ステータス | 終わるときに返す0から255の数。0は成功 |
| ゾンビ | 終わったが、親がまだ wait していない子 |
学生が入力するセルは、このプレビューでは空白です。
forkは自分をもう1つ作る
fork を呼ぶと、プロセスがそっくり複製されます。元が親で、複製が子です。戻り値は、子では0、親では子の番号です。
親が wait を呼ぶと、子が終わるまで待ち、終了ステータスを受け取ります。子が return 3 で終われば、親は3を受け取ります。
forkの戻り値
| 誰が受け取るか | 戻り値 |
|---|---|
| 親プロセス | 子の番号。正の数 |
| 子プロセス | 0 |
| 作れなかったとき | -1 |
学生が入力するセルは、このプレビューでは空白です。
子が終わるのを親が待つ
printf '%s\n' 'int main(void){pid_t p = fork(); if (p == 0) {puts("child"); return 3;} int st; wait(&st); printf("exit status %d\n", WEXITSTATUS(st)); return 0;}' | gcc -include stdio.h -include unistd.h -include sys/wait.h -x c - -o /tmp/fk && /tmp/fkここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
3人の子の終了ステータスを足す
printf '%s\n' 'int main(void){for (int i = 0; i < 3; i++) if (fork() == 0) return i; int st, sum = 0; for (int i = 0; i < 3; i++) {wait(&st); sum += WEXITSTATUS(st);} printf("status sum %d\n", sum); return 0;}' | gcc -include stdio.h -include unistd.h -include sys/wait.h -x c - -o /tmp/kids && /tmp/kidsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
forkを2回呼ぶと4つになる
printf '%s\n' 'int main(void){fork(); fork(); puts("hi"); return 0;}' | gcc -include stdio.h -include unistd.h -x c - -o /tmp/four && /tmp/four | wc -lここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
execは今のプログラムを別のものに置き換える
exec は、今のプロセスの中身を、別のプログラムに入れ替えます。プロセスの番号は変わりません。成功すると、元のプログラムには戻りません。
シェルは、fork で子を作ります。子が exec でコマンドになり、親が wait で終わりを待ちます。
シェルが ls を動かす流れ
| 順 | 誰が | 何をする |
|---|---|---|
| 1 | シェル | fork で子を作る |
| 2 | 子 | exec で ls に変わる |
| 3 | ls | 一覧を出して、ステータス0で終わる |
| 4 | シェル | wait で、終わりとステータスを受け取る |
学生が入力するセルは、このプレビューでは空白です。
execのあとの行は実行されない
printf '%s\n' 'int main(void){execl("/bin/echo", "echo", "hello from exec", NULL); puts("not reached"); return 0;}' | gcc -include stdio.h -include unistd.h -x c - -o /tmp/ex && /tmp/exここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
シェルは直前のステータスを覚えている
sh -c 'exit 5'; echo $?ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
waitされない子はゾンビになる
子が終わっても、親が wait するまで、終了ステータスの記録が残ります。この状態の子がゾンビです。
ゾンビは何も実行しません。ただ、親が wait しないままだと、記録が溜まります。
よくある間違い
注意
fork のあと、親と子は同じ変数を共有する、と思いがちです。実際は、メモリごと複製されます。子が変数を書き換えても、親の変数は変わりません。同じ値が見えるのは、複製された瞬間だけです。
子が書き換えても親の変数は変わらない
printf '%s\n' 'int main(void){int x = 1; if (fork() == 0) {x = 2; return 0;} wait(NULL); printf("%d\n", x); return 0;}' | gcc -include stdio.h -include unistd.h -include sys/wait.h -x c - -o /tmp/cp && /tmp/cpここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
さらに挑戦: 木の深さを求める
課題が通ったら、depth(tree, pid) を書きましょう。番号1が深さ0で、その子が深さ1です。
自分の親を探すには、木を全部調べます。上のテストの TRACE では、番号4の深さは2です。