10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Linuxの親プロセス

Posted at

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)を発行したプロセスではなくその親プロセスとなる (親プロセスの情報が、親から子へコピーされる)。

parenttest.c
#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という機能を使うと、孫がある状態で、子供が先に終了した場合、親は孫の親プロセスとなる。

subreapertest.c
#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 が駆使しているのは確認した。

  1. 多くのUnix (*BSDを含む) ではstruct procの中にp_ppidというメンバー変数があり、そこにPPIDが保存されている。Linuxではstruct task_structの中に、親プロセスのstruct task_structへのポインタがある。

10
10
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?