之前有学习过僵尸进程,它指的是“未老先衰”的子进程先于父进程去逝,而父进程未对它进行回收(wait)所产生的。
本文来谈谈,另一个对立的概念——孤儿进程,以及孤儿进程组。
如果一个进程,它的父进程先终止了,则该进程成为孤儿进程。此后,该进程的父进程变为 1 号 init 进程。
下面的代码可以产生一个孤儿进程。
int main() {
pid_t pid = fork();
if (pid == 0) {
while(1) sleep(1);
}
return 0;
}
上面的程序运行后,产生一个子进程,进入死循环,而父进程将会退出,此后子进程被 init 进程回收,成为孤儿进程。
需要注意的是,孤儿进程不像僵尸进程,它是无害的,也不需要回收。
整个进程组也可以成为孤儿。POSIX.1 将孤儿进程组定义为:
该进程组的每个成员的父进程要么是该组的成员,要么在其它会话中。
只要能够满足上面的定义,则此进程组就是孤儿进程组。可能读起来比较拗口,看图 1 中的例子可能会清楚点。
图 1 中,按照孤儿进程组的定义,进程组 1 不是孤儿进程组,因为进程组 1 中有一个进程的父进程不属于进程组 1,也不在另一个会话中。
进程组 2 是孤儿进程组,因为该组中的每个成员满足定义:每个成员的父进程要么在本组中,要么在其它会话中。
如果一个进程组包含一个或一个以上的停止的进程,当该进程组变成孤儿进程组时,每个处于停止状态的进程会接收到 SIGHUP 挂断信号,紧接着又会收到 SIGCONT 信号。(以上是 APUE 中的解释)
注意,在英文原版中是这样叙述的:
Since the process group is orphaned when the parent terminates, and the process group contains a stopped process, POSIX.1 requires that every process in the newly orphaned process group be sent the hang-up signal (SIGHUP) followed by the continue signal (SIGCONT).
上面的大意是说,刚产生的孤独进程组中的每一个进程,而不是每一个停止的进程,是每一个进程都能收到 SIGHUP 信号。中文翻译版本的确有点坑爹啊!!!
在 Ubuntu 和 Red Hat 中的测试结果也验证了原版中的解释,如果进程组存在停止状态的进程,当该进程组变成孤儿进程组时,该进程组中所有进程都会收到 SIGHUP 信号。其它系统中并未测试,如果你的结果和此不一样,请在下文留言。
下面的程序功能:进程组中存在停止的进程,该进程组成为孤儿进程组后,该进程组所有的进程会收到 SIGHUP 信号。另外,该程序还演示了后台进程组试图读控制终端会产生错误。
需要注意的是,父进程终止,子进程进入后台进程组。
// orphan.c
#include
#include
#include
#include
#include
void handler(int sig) {
printf("hello sighup, pid = %d\n", getpid());
}
void print(char *name) {
printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n",
name, getpid(), getppid(), getpgid(getpid()), tcgetpgrp(0));
fflush(stdout);
}
int main() {
char c;
pid_t pid;
print("parent");
pid = fork();
if (pid < 0) {
perror("fork");
}
else if (pid > 0) {
sleep(5);
}
else {
print("child");
signal(SIGHUP, handler);
kill(getpid(), SIGTSTP); // 让子进程暂停
print("child"); // 如果执行了此行,说明已经收到了 SIGHUP 信号
if (read(STDIN_FILENO, &c, 1) != 1) {
printf("read error, error number: %d\n", errno);
}
exit(0);
}
return 0;
}
$ gcc orphan.c
$ ./orphan
上面的结果正是预期的。
程序一开始让父进程打印信息,接着 fork 出子进程,让子进程打印信息,然后子进程进入停止状态。
等待父进程结束后,子进程所在的组变成了孤儿进程组,同时它也是后台进程组。接着所有子进程会收到 SIGHUP 信号,因为子进程对该信号进行了捕获,信号处理函数向控制终端输出 hello sighup, pid = 3080.
接下来子进程继续执行 print 向屏幕输出信息,注意此时的前台进程组 tpgrp = 3056,这是 bash 进程所在的进程组 id,也等于 bash 进程的 id.
最后,子进程试图读控制终端,于是收到 SIGTTIN 信号,read 返回出错,errno 被设置为 5,也就是 EIO,解释如下:
EIO I/O 错误。当进程位于后台进程组时,还试图从控制终端读,就会产生此错误。
为了验证其它未停止的进程也能收到信号,你可以多 fork 一个子进程来进行测试。
练习:产生一个孤儿进程组,该进程组中有停止的进程,在程序中捕获 SIGHUP 信号。