Unixのプロセス
UnixではプロセスはプロセスID (PID) という数字で管理している。プロセスには、必ず親プロセスがあり、そのプロセスを作ったプロセスのPID (Parent PID、PPIDと呼ぶ) がプロセスごとに記録されている1。
Unixでプロセスを作るには、fork(2)やvfork(2)というシステムコールを使う。Linuxにはclone(2)というシステムコールもあり、fork(2)やvfork(2)はそれぞれ特殊な引数をつけたclone(2)の呼び出しと実質的に等価である。
親プロセスの役割は、プロセスを看取ることである。プロセスがexit(2)を呼び出すと、そのプロセスは終了し、親プロセスにシグナルSIGCHLDが送られる。親プロセスは、wait(2)など (waitpid(2)とかwait4(2)のようなバリエーションがある) のシステムコールで子供の終了コードを取得することができる。原則として、プロセスの終了コードを取得する方法はこれだけである。余談だが、親がwait(2)などで看取ってくれないと、終了したプロセスの終了コードなどをカーネルが保持し続ける必要があり、このような状態をゾンビと呼ぶ。
子プロセスを持つプロセスが終了する (親プロセスが子プロセスより先に終了する) と、子プロセスの親はPID 1のプロセスにさし変わる。PID 1のプロセスはinitと呼ばれ、カーネル初期化後に最初に起動されるプロセスである (伝統的にはカーネルが自ら起動する唯一のプロセスでもあった)。initは、各種デーモンやコンソールログインを司るgetty(8)などを起動する。現在のLinuxではsystemdがinitとして起動されるのが主流である。PID 1のプロセスが終了する (あるいは起動できない) と、カーネルは異常終了する。
以上は、Unixにおける通常の動作であって、現在のLinuxでは特殊な例が存在する。
プロセスを作ったプロセスが親にならない例
clone(2)のflagとしてCLONE_PARENTを指定すると、生成される子プロセスの親プロセスは、clone(2)を発行したプロセスではなくその親プロセスとなる (親プロセスの情報が、親から子へコピーされる)。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#define STACK (4096)
int
childfunc(void *arg)
{
prctl(PR_SET_NAME, "parenttest:C");
printf(":child... sleeping for 10sec.\n");
sleep(10);
printf(":child... sleep done; exiting.\n");
return 128;
}
int
main(void)
{
int pid;
void *childstack;
printf("before clone; pid is %d\n", getpid());
prctl(PR_SET_NAME, "parenttest:P");
childstack = mmap(NULL, STACK,
PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (childstack == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
pid = clone(childfunc, childstack+STACK, CLONE_PARENT|SIGCHLD, NULL);
if (pid > 0) {
int wstatus, r;
printf("parent... child is %d.\n"
"waitpid ECHILD might be expected below\n", pid);
r = waitpid(pid, &wstatus, 0);
if (r < 0) {
perror("waitpid");
sleep(10);
printf("parent... sleep done; exiting.\n");
} else {
printf("waitpid returns %d.\n"
" exit code %d.\n", r, WEXITSTATUS(wstatus));
}
return 0;
} else if (pid == 0) {
printf("??? clone(2) returned 0\n");
exit(EXIT_FAILURE);
} else {
perror("clone");
exit(EXIT_FAILURE);
}
}
実行すると、PIDを表示した後clone(CLONE_PARENT)を発行する。clone()を発行したプロセス (parenttest:P) も、clone()で生成されたプロセス (parenttest:C) も、10秒後に終了するはずだが、その間にpstreeでプロセスを観察すると、シェルの子供が2ついることがわかる。
% ./parenttest &
[1] 999975
% before clone; pid is 999975
parent... child is 999976.
waitpid ECHILD might be expected below
waitpid: No child processes
:child... sleeping for 10sec.
% pstree -p $$
zsh(11686)─┬─parenttest:C(999976)
├─parenttest:P(999975)
└─pstree(999979)
% parent... sleep done; exiting.
:child... sleep done; exiting.
[1] + done ./parenttest
clone()からCLONE_PARENTを外す (スタックとエントリポイントを除けばfork()と同等) と、
zsh(11686)─┬─parenttest:P(??????)───parenttest:C(??????)
└─pstree(??????)
のようになり、parenttest:Pはwaitpid(2)によりparenttest:Cの終了コードも取れるはず。
CLONE_PARENTのとき、zshにとっては、parenttest:Cは知らないうちにできた子であるが、ちゃんと看取ってくれるようだ。シェルによっては誤動作するかもしれない。
親が先に終了した時に、祖父の子供になる例
prctl(2)のCHILD_SUBREAPERという機能を使うと、孫がある状態で、子供が先に終了した場合、親は孫の親プロセスとなる。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#define STACK (4096)
int
grandchildfunc(void *arg)
{
prctl(PR_SET_NAME, "subreapertest:G");
printf("::grandchild... sleeping 10sec.\n");
sleep(10);
printf("::grandchild... sleep done; exiting.\n");
return 129;
}
int
childfunc(void *arg)
{
int pid;
void *childstack;
prctl(PR_SET_NAME, "subreapertest:C");
printf(":child... cloning\n");
childstack = mmap(NULL, STACK,
PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (childstack == MAP_FAILED) {
perror("child mmap");
exit(EXIT_FAILURE);
}
pid = clone(grandchildfunc, childstack+STACK, SIGCHLD, NULL);
if (pid == 0) {
printf(":??? clone(2) returned 0\n");
exit(EXIT_FAILURE);
} else if (pid < 0) {
perror(":clone");
exit(EXIT_FAILURE);
}
printf(":child: grandchild is %d, exiting.\n", pid);
return 128;
}
int
main(void)
{
int pid;
void *childstack;
printf("before clone; pid is %d\n", getpid());
prctl(PR_SET_NAME, "subreapertest:P");
childstack = mmap(NULL, STACK,
PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (childstack == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
if (prctl(PR_SET_CHILD_SUBREAPER, 0) < 0) {
perror("prctl(PR_SET_CHILD_SUBREAPER)");
exit(EXIT_FAILURE);
}
pid = clone(childfunc, childstack+STACK, SIGCHLD, NULL);
if (pid > 0) {
int child, r, wstatus;
printf("parent... child is %d.\n"
" I might be a subreaper\n", pid);
for ( ; ; ) {
r = wait(&wstatus);
if (r > 0) {
printf("child %d exited with status %d\n",
r, WEXITSTATUS(wstatus));
}
else if (r < 0 && errno == ECHILD)
break;
}
printf("parent: all child exited\n");
} else if (pid == 0) {
printf("??? clone(2) returned 0\n");
exit(EXIT_FAILURE);
} else {
perror("clone");
exit(EXIT_FAILURE);
}
return 0;
}
実行すると、子 (subreapertest:C) と孫 (subreapertest:G) が生えるが、subreapertest:Cはすぐに終了する。subreapertest:Gは10秒寝る。
./subreapertest &
[1] 1003968
% before clone; pid is 1003968
parent... child is 1003969.
I might be a subreaper
:child... cloning
:child: exiting.
child 1003969 exited with status 128
::grandchild... sleeping 10sec.
% pstree -p $$
zsh(11686)─┬─pstree(1003974)
└─subreapertest:P(1003968)───subreapertest:G(1003970)
% ::grandchild... sleep done; exiting.
child 1003970 exited with status 129
parent: all child exited
[1] + done ./subreapertest
subreapertest:Cが終了した後、subreapertest:Pがsubreapertest:Gの親となっていて、終了コードも取得できることがわかる。
prctl(PR_SET_CHILD_SUBREAPER)の引数1を0にすると、subreapertest:Cが終了するとsubreapertest:GのPPIDは1 (init) になる。subreapertest:Pとsubreapertest:Gに親子関係はないため、subreapertest:Gがいてもすべての子供が終了したとしてsubreapertest:Pは終了する。
何に使うの?
何だかよくわからない機能ではあるが、runc が駆使しているのは確認した。
-
多くのUnix (*BSDを含む) ではstruct procの中にp_ppidというメンバー変数があり、そこにPPIDが保存されている。Linuxではstruct task_structの中に、親プロセスのstruct task_structへのポインタがある。 ↩