shell作业控制的两个问题:组长叛变与SIGHUP信号
前言
对创建了进程组的进程成为进程组首进程,其特点是其pid等于进程组的pgid。
对创建了会话的进程称为会话首进程,该进程一般为shell进程,同时其本身又是一个进程组的首进程。
这首先引出第一个问题:在Linux系统中,为什么进程组首进程(包括会话首进程)既不能以其自身创建新进程组同时作为两个进程组的组长,又不能脱离原会话创建新会话呢?
其次,在用户认为作业结束,断开终端(比如关闭ssh窗口),系统究竟给哪个进程发送SIGHUP信号?
这两个问题涉及到linux内核的做法。
问题1:组长为什么不能叛变?
首先:方便应用程序判断进程是否是首进程。
内核维护会话与进程组严格的层次关系(进程组的组长一定代表其所在的进程组、进程组之间互斥组成会话)。这导致应用程序判断一个进程是否为进程组或会话首进程的唯一方式是比对pid、pgid与sid。不可能一个进程组的一个成员是另外一个进程组的组长(一个进程组包含另一个进程组)。同理,不可能一个会话包含另一个会话。
其次:保证作业控制的条理清晰
进程组之间不互斥的相互包含的关系会导致作业控制非常复杂, 例如:用户希望通过shell停止一个任务(进程组),但该任务下居然包含一个子任务(子进程组), shell不知道发信号的对象究竟包不包含该子任务。与linux的设计哲学相悖。
总结:保证结构清晰(树形结构)、不复杂。
补充:
进程组、会话是作业控制的单位,
而父子进程的关系是wait的基础。
两者独立互不干扰。
问题2:谁会收到SIGHUP信号?
下述程序一个为进程本身与其子进程建立了SIGHUP的信号处理器,另一个只是为子进程建立SIGHUP的处理器。这两个程序将代替shell本身运行的程序,拥有shell的会话首进程与控制终端的控制进程的双重身份。
程序:
#define _GNU_SOURCE /* Get strsignal() declaration from <string.h> */
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
_Noreturn void errExit(const char *p){
perror(p);
exit(EXIT_FAILURE);
}
static void /* Handler for SIGHUP */
handler(int sig)
{
printf("PID %ld: caught signal %2d (%s)\n", (long) getpid(),
sig, strsignal(sig));
/* UNSAFE (see Section 21.1.2) */
}
int
main(int argc, char *argv[])
{
pid_t parentPid, childPid;
int j;
struct sigaction sa;
if (argc < 2 || strcmp(argv[1], "--help") == 0){
fprintf(stderr, "%s {d|s}... [ > sig.log 2>&1 ]\n", argv[0]);
exit(EXIT_FAILURE);
}
setbuf(stdout, NULL); /* Make stdout unbuffered */
parentPid = getpid();
printf("PID of parent process is: %ld\n", (long) parentPid);
printf("Foreground process group ID is: %ld\n",
(long) tcgetpgrp(STDIN_FILENO));
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGHUP, &sa, NULL) == -1)
errExit("sigaction");
for (j = 1; j < argc; j++) { /* Create child processes */
childPid = fork();
if (childPid == -1)
errExit("fork");
if (childPid == 0) { /* If child... */
if (argv[j][0] == 'd') /* 'd' --> to different pgrp */
if (setpgid(0, 0) == -1)
errExit("setpgid");
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGHUP, &sa, NULL) == -1)
errExit("sigaction");
break; /* Child exits loop */
}
}
/* All processes fall through to here */
alarm(60); /* Ensure each process eventually terminates */
printf("PID=%ld PGID=%ld\n", (long) getpid(), (long) getpgrp());
for (;;)
pause(); /* Wait for signals */
}
另一个程序除了不为父进程建立处理器之外,与其一模一样。
结果
为父进程建立处理器的程序,通过
$ exec ./程序名 s s d d > log 2>&1
运行后关闭终端,会发现log中输出如下内容
PID of parent process is: 5133
Foreground process group ID is: 5133
PID=6962 PGID=5133
PID=6963 PGID=5133
PID=6964 PGID=6964
PID=5133 PGID=5133
PID=6965 PGID=6965
PID 5133: caught signal 1 (Hangup)
可以发现,只有父进程收到了内核发来的SIGHUP信号。这是由于,当会话首进程(即终端的控制进程)为SIGHUP建立了处理器,内核便认为该会话首进程有能力处理该信号(bash等shell的做法是将SIGHUP信号发送给会话的所有进程组以通知终端断开),此程序使用exec成为了会话首进程,也建立了信号处理器,但信号处理器并没有发送SIGHUP给会话中的其余进程,内核也不会发送,那么自然而然只有该进程才能收到SIGHUP信号了。
而不为父进程建立处理器的程序,输出结果如下:
PID of parent process is: 9994
Foreground process group ID is: 9994
PID=10131 PGID=9994
PID=10132 PGID=9994
PID=9994 PGID=9994
PID=10134 PGID=10134
PID=10133 PGID=10133
PID 10132: caught signal 1 (Hangup)
PID 10131: caught signal 1 (Hangup)
可见父进程被SIGHUP终止后,其同一进程组的子进程亦收到了SIGHUP信号,但是后台进程组没有收到信号。此时这些信号都是内核发送的。