当会话首进程打开了一个控制终端之后它同时也成为了该终端的控制进程;当一个控制进程失去其终端连接后,内核会向其发送一个SIGHUP信号来通知它这一事实(还会发送一个SIGCONT信号以确保当该进程之前被一个信号停止时重新开始该进程)。一般来讲,这种情况可能会在下面两个场景中出现:
- 当终端驱动器检测到连接断开后,表明调制解调器会在终端行上信号丢失
- 当工作站上的终端窗口被关闭时。发生这种情况是因为最近打开的与终端窗口关联的伪终端的主侧的文件描述符被关闭了
SIGHUP信号的默认处理方式是终止进程。如果控制进程处理了或者忽略了这个信号,那么后继尝试从终端中读取数据的请求就会返回文件结束的错误。
SUSv3 声称如果终端断开发生的同时还满足调用 read()时抛出 EIO 错误的条件的话,那么调用 read()既有可能返回文件结束,也有可能返回 EIO 错误。可移植的程序必须要处理好这两种情况
向控制进程发送SIGHUP信号会引起一种链式反应,从而导致SIGHUP信号发送给很多其他进程。这个过程可能会以下面两种方式发生:
- 控制进程是一个shell。shell建立了一个SIGHUP信号的处理器,这样在进程终止之前,它能够将SIGHUP信号发送给由它所创建的各个任务。在默认情况下,这个信号会终止那些任务,但如果它们捕获了这个信号,就能知道shell进程已经终止了
- 在终止终端的控制进程时,内核会解除会话中所有进程与该控制终端之间的关联关系以及控制终端与该会话的关联关系(因此另一个会话首进程可以请求该终端成为控制终端了),并且通过向该终端的前台进程组的成员发送 SIGHUP 信号来通知它们控制终端的丢失
SIGHUP 信号也可以用作他用:
- 当一个进程组成为孤儿进程组时会生成 SIGHUP 信号。
- 此外,手工发送 SIGHUP 信号通常用来触发 daemon 进程重新初始化自身
或重新读取其配置文件。(根据定义,daemon 进程没有控制终端,因此无法从内核接收 SIGHUP信号。)
在shell中处理SIGHUP信号
在登录会话中,shell通常是终端的控制进程。大多数shell程序在交互式运行时会为SIGHUP信号建立一个处理器。这个处理器会处理shell,但在终止之前会向由shell创建的各个进程组(包括前台和后台进程组)发送一个 SIGHUP 信号。(在SIGHUP信号之后可能会发送一个SIGCONT信号,这依赖于shell本身以及任务当前是否处于停止状态)。至于这些组中的进程如何响应SIGHUP信号则需要根据应用程序的具体需求,如果不采取特殊的动作,那么默认情况下会终止进程。
- 一些任务控制shell在正常退出(如登出或在 shell 窗口中接下 Control-D)时也会发送SIGHUP 信号来停止后台任务。bash 和 Korn shell 都采取了这种处理方式
- nohup(1)命令可以用来使一个命令对SIGHUP信号免疫------即执行命令时将SIGHUP信号的处理设置为 SIG_IGN。bash 内置的命令 disown 提供了类似的功能,它从 shell 的任务列表中删除一个任务,这样在 shell 终止时就不会向该任务发送 SIGHUP 信号了。
下面演示了 shell 接收 SIGHUP 信号并向其创建的任务发送 SIGHUP 信号的过
程。
- 这个程序的主要任务是创建一个子进程,然后让父进程和子进程暂停执行以捕获 SIGHUP信号并在收到该信号时打印一条消息。
- 如果在执行程序时使用了一个可选的命令行参数(它可以是任意字符串),那么子进程会将其自身放置在一个不同的进程组中(在同一个会话中)。
- 这个
功能对于说明 shell 不会向不是由它创建的进程组发送 SIGHUP 信号,即使该进程组与 shell 位于同一个会话中来讲是非常有用的。(由于程序中最后一个 for 循环是一个无限循环,因此这个程序使用了 alarm()设置一个定时器来发送 SIGALRM 信号。如果一个进程没有终止的话,那么当它接收到 SIGALRM 信号而不做处理时会导致进程终止。
static void handler(int sig)
{
}
int main(int argc, char *argv[])
{
pid_t childPid;
struct sigaction sa;
setbuf(stdout, NULL); /* Make stdout unbuffered */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGHUP, &sa, NULL) == -1){
perror("sigaction");
exit(1);
}
childPid = fork();
if (childPid == -1){
perror("fork");
exit(1);
}
if (childPid == 0 && argc > 1)
if (setpgid(0, 0) == -1) /* Move to new process group */
{
perror("setgid");
exit(1);
}
printf("PID=%ld; PPID=%ld; PGID=%ld; SID=%ld\n", (long) getpid(),
(long) getppid(), (long) getpgrp(), (long) getsid(0));
alarm(60); /* An unhandled SIGALRM ensures this process
will die if nothing else terminates it */
for (;;) { /* Wait for signals */
pause();
printf("%ld: caught SIGHUP\n", (long) getpid());
}
}
运行:
第一个命令会导致创建两个进程,这两个进程属于由 shell 创建的进程组。第二个命令创建了一个子进程,子进程将自身放置在了一个不同的进程组中。
当查看 samegroup.log 时会发现其中包含了下面的输出,表明两个进程组的成员都收到了shell 发送的信号
当查看 diffgroup.log 时会发现下面的输出,表明 shell 在收到 SIGHUP 时不会向不是由它创建的进程组发送信号。
SIGHUP 和控制进程的终止
如果因为终端断开引起的向控制进程发送的 SIGHUP 信号会导致控制进程终止,那么
SIGHUP 信号会被发送给终端的前台进程组中的所有成员(见 25.2 节)。这个行为是控制进程终止的结果,而不是专门与 SIGHUP 信号关联的行为。如果控制进程出于任何原因终止,那么前台进程组就会收到 SIGHUP 信号。
在 Linux 上,SIGHUP 信号后面会跟着一个 SIGCONT 信号以确保在进程组之前被一个信号停止的情况下恢复该进程组。但 SUSv3 并没有指定这种行为,并且在这种情况下大多数其他UNIX 实现不会发送 SIGCONT 信号
下面程序演示了控制进程的终止导致向终端的前台进程组的所有成员发送 SIGHUP
信号。这个程序为每个命令行参数都创建了一个子进程。如果相应的命令行参数是 d,那么子进程会将自身放置在自己的(不同的)进程组中;否则的话子进程加入到父进程所在的进程组中。为确保它们能够在进程终止事件不发生的情况下正常终止,父进程和子进程都调用了 alarm()设置一个定时器以在 60 秒之后发送一个SIGALRM 信号。最后所有进程(包括父进程)打印出了它们的进程 ID 和进程组 ID⑥,接着循环等待信号的到达。当发出信号之后,处理器会打印出进程的进程 ID 和信号数值
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)
usageErr("%s {d|s}... [ > sig.log 2>&1 ]\n", argv[0]);
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));
for (j = 1; j < argc; j++) { /* Create child processes */
childPid = fork();
if (childPid == -1){
perror("fork");
exit(1);
}
if (childPid == 0) { /* If child... */
if (argv[j][0] == 'd') /* 'd' --> to different pgrp */
if (setpgid(0, 0) == -1){
perror("setpgid");
exit(1);
}
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGHUP, &sa, NULL) == -1){
perror("sigaction");
exit(1);
}
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 ./disc_SIGHUP d s s > sig.log 2>&1
exec命令是一个shell内置命令,它会导致shell执行一个exec()来使用指定的程序取代自己。由于shell是终端的控制进程,因此现在这个程序已经成为了控制进程并且在终端窗口被关闭时会收到SIGHUP 信号。在关闭终端窗口之后,在sig.log 文件中会看到下面的输出。
关闭终端窗口会导致SIGHUP信号被发送给控制进程。(父进程),进而导致该进程的终止。从上面可以看出,两个子进程与父进程位于同一个进程组中(终端的前台进程组),它们都收到了 SIGHUP 信号,但位于另一个进程组(后台)中的子进程并没有收到这个信号
总结
SIGHUP 信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联. 系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
SIGHUP会在以下3种情况下被发送给相应的进程:
1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程);
2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程;
3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。
例如:在我们登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。 比如xinetd超级服务程序。
当xinetd程序在接收到SIGHUP信号之后调用hard_reconfig函数,它将循环读取/etc/xinetd.d/目录下的每个子配置文件,并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程讲给该子服务进程发送SIGTERM信号来结束它。如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。