【整理】SIGHUP问题梳理


本文梳理了网络上找到的一些和 SIGHUP 问题相关的资料,并根据自己的理解,对其中若干说法进行了修正。


原文出处:
1.《关闭终端后,后台作业退出的分析
2.《后台进程退出时,关于SIGHUP信号的讨论
3.《SIGHUP信号与控制终端
4.《为什么linux下sshd被kill会导致所有子进程被终止
5.《where is SIGHUP from? (sshd forks a child to create a new session, kill this child and all processes in the session dies)》


==== 我是火影两周年纪念分割线 ====


《关闭终端后,后台作业退出的分析》

先把结论写在上面:
  • 内核驱动发现终端(或伪终端)关闭,给终端对应的控制进程(bash)发 SIGHUP  
  • bash 收到 SIGHUP 后,会给各个作业(包括前后台)发送 SIGHUP,然后自己退出
  • 前后台的各个作业收到来自 bash 的 SIGHUP 后退出(如果程序会处理 SIGHUP,就不会退出)

PS: 这里所谓终端关闭就是指内核感知不到终端了,例如远程登录时的网络断开、sshd 挂掉、手动叉掉 ssh 登陆窗口之类的情况也算在内。
PPS: SIGHUP 会在以下情况发出
  • 终端关闭时,该信号被内核发送到 session 首进程(比如登陆 shell 进程,即上面说的 bash 进程)
  • session 首进程退出前,该信号被内核发送到该 session 中的前台进程组中的每一个进程(是内核发的?还是会话首进程发的?如果是内核发的,那么在 session 首进程为 bash 的情况下,前台进程组不是会收到两次 SIGHUP ?此问题后面作了回答
  • 若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到过 SIGSTOP 或 SIGTSTP 信号后被挂起),该信号会被发送到该进程组中的每一个进程(如果孤儿进程组中没有进程被挂起,难道就不发了?是发给每一个子进程,还是只发给被挂起的子进程?)。
PPPS: bash 收到 SIGHUP 后会转发给各个任务(job)


查看 bash 的手册,终于看到
The shell exits by default upon receipt of a SIGHUP. Before exiting, an interactive shell resends the SIGHUP to all jobs, running or stopped. Stopped jobs are sent SIGCONT to ensure that they receive the SIGHUP.
真相接近大白:原来是 bash 在收到内核 driver 发出的 SIGHUP 后,转发的 !


==== 我是火影两周年纪念分割线 ====


《为什么linux下sshd被kill会导致所有子进程被终止》

问题描述:
使用 SecureCRT 远程登陆,打开两个终端,在其中一个(终端A)运行测试程序 a.out;在另一个(终端B)中使用 pstree 可以看到
|-sshd(2555)-+-sshd(16568)---bash(16572)---pstree(16862)
        |            `-sshd(16635)---bash(16637)---a.out(16860)---a.out(16861)
如果在终端 B 中运行 kill -9 16635 后,则标红的这些进程全会终止掉;
使用 logout、exit 命令也会断开连接,但是 a.out(在后台进程组)会被 init 收管,并不会挂掉。
请教各方高人,这是为何?

PS: 粗略看了一下 sshd 的实现代码,和 sshd 自身应该没什么关系,父进程自己都挂了怎么会来得及处理子进程, 莫非在 kill -9 16635 的时候,内核会检测将要结束的进程是 sshd ,然后干掉他的子进程?

费解啊!费解


自己来结贴
google 上找了好久,又看了看 APUE,大致得出了结论
  1. ssh 远程登录时,实际上是伪终端(网络连接断开、kill 掉 sshd 实际上都被内核感知为关闭终端);
  2. 终端被关闭后,内核会给与之对应的会话首进程发送 SIGHUP ,会话首进程如果挂了,内核会给该会话下的所有前台进程组发送 SIGHUP(前台进程组只会有一个,这里用“所有”不太好),而后台进程组,如果没有进程处于停止状态,是不会收到来自内核的 SIGHUP 的(这里的说法保留意见);
  3. bash 在收到 SIGHUP 后会退出,但在退出前,会给各个作业(jobs,即前后台进程组)发送 SIGHUP 信号;
所以上面 kill -9 16635 后:
  1. 内核发现连接断开(伪终端关闭) 
  2. 给对应的控制进程(bash)发 SIGHUP 
  3. bash 收到 SIGHUP 后,给各个作业发送 SIGHUP ,然后自己退出
  4. a.out 收到来自 bash 的 SIGHUP,退出


==== 我是火影两周年纪念分割线 ====


where is SIGHUP from? (sshd forks a child to create a new session, kill this child and all processes in the session dies)

ssh (along with terminal emulators, screen, tmux, script, and some other programs) uses a thing called a "pseudo-tty" (or "pty"), which behaves like a dialup modem connection. I describe it that way because that's the historical origin of this behavior: if you lost your modem connection for some reason, the tty (or pty) driver detected the loss of carrier and sent SIGHUP ("Hangup") to your session. This enables programs to save their state (for example, vi/vim will save any files you had modified but not saved for recovery) and shut down cleanly. Similarly, if the network connection goes away for some reason (someone tripped over the power or network cable? ...or sssh dumped core for some odd reason), the pty sends SIGHUP to your session so it gets a chance to save any unsaved data.

Technically, the tty/pty driver sends the signal to every process in the process group attached to the terminal (process groups are also related to shell job control, but this was their original purpose). Some other terminal signals are handled the same way, for example Ctrl + C sends SIGINT and Ctrl + \ sends SIGQUIT (and Ctrl + Z sends SIGTSTP, and programs that don't handle SIGTSTP by suspending themselves are sent SIGSTOP; this double signal allows vim to set the terminal back from editing mode to normal mode and in many terminal emulators swap to the pre-editing screen buffer).


==== 我是火影两周年纪念分割线 ====


《后台进程退出时,关于SIGHUP信号的讨论》


问题的提出:
  我一直纠结于一个问题。在 Linux 上,我用“command &”启动了一个后台进程。如果这个后台进程既没有 nohup, 又没有 setsid,也没有 disown 的话(如下程序),那么这个进程在终端被关闭时,会被某个信号 kill 。 我的问题是: 该进程会被谁 kill; 谁发的信号;
[root@xxx ~]# cat test.sh

#!/bin/bash

sleep 300

[root@xxx ~]# 
[root@xxx ~]# ./test.sh &

APUE (中文第二版 9.6节)
如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂掉信号发送给控制进程(会话首进程)。

(原文中给出是简版,下面是完整版)
[root@Betty ~]# man bash
...
INVOCATION
       A login shell is one whose first character of argument zero is a -, or one started with the --login option.


       An  interactive  shell  is  one  started  without  non-option  arguments and without the -c option whose standard input and error are both connected to terminals (as determined by
       isatty(3)), or one started with the -i option.  PS1 is set and $- includes i if bash is interactive, allowing a shell script or a startup file to test this state.
...
SIGNALS
       When  bash  is interactive, in the absence of any traps, it ignores SIGTERM (so that kill 0 does not kill an interactive shell), and SIGINT is caught and handled (so that the wait
       builtin is interruptible).  In all cases, bash ignores SIGQUIT.  If job control is in effect, bash ignores SIGTTIN, SIGTTOU, and SIGTSTP.


       Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent.  When job control is not in effect,  asynchronous  commands  ignore
       SIGINT  and  SIGQUIT in addition to these inherited handlers.  Commands run as a result of command substitution ignore the keyboard-generated job control signals SIGTTIN, SIGTTOU,
       and SIGTSTP.


       The shell exits by default upon receipt of a SIGHUP.  Before exiting, an interactive shell resends the SIGHUP to all jobs, running or stopped.  Stopped jobs are  sent  SIGCONT  to
       ensure that they receive the SIGHUP.  To prevent the shell from sending the signal to a particular job, it should be removed from the jobs table with the disown builtin (see SHELL
       BUILTIN COMMANDS below) or marked to not receive SIGHUP using disown -h.


       If the huponexit shell option has been set with shopt, bash sends a SIGHUP to all jobs when an interactive login shell exits.


       If bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes.  When bash is  waiting
       for  an  asynchronous  command  via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status
       greater than 128, immediately after which the trap is executed.
...
APUE 和 Bash manpage 肯定是权威资料。第三条资料是某网友的分析,也很好。如下是该网友的结论(纯属转载):
  • 内核驱动发现终端(或伪终端)关闭,给对应终端的控制进程(bash)发 SIGHUP  
  • bash 收到 SIGHUP 后,会给各个作业(包括前后台)发送 SIGHUP,然后自己退出


==== 我是火影两周年纪念分割线 ====


《SIGHUP信号与控制终端》

unix 中进程组织结构为(一个)session 中(可能会)包含一个前台进程组及一个或多个后台进程组,一个进程组(可能)包含多个进程。
一个 session 中(最终)可能会包含 session 首进程(也可能不包含,即已经退出),而一个 session 可能会拥有一个控制终端。
一个进程组中可能会包含进程组组长进程(也可能不包含,即已经退出)。进程组组长进程的进程 ID 与该进程组 ID 相等。
与终端交互的进程是前台进程,否则便是后台进程。

SIGHUP 会在以下 3 种情况下被发送给相应的进程:
  1. 终端关闭时,该信号被发送到 session 首进程以及作为 job 提交的(后台)进程(即用 & 符号提交的进程)
  2. session 首进程退出时,该信号被发送到该 session 中的前台进程组中的每一个进程
  3. 若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到 SIGSTOP 或 SIGTSTP 信号后被挂起),该信号会被发送到该进程组中的每一个进程(这里说的有问题,应该是发送给被挂起的进程)。
系统对 SIGHUP 信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。

下面观察几种因终端关闭导致进程退出的情况,在这儿进程退出是因为收到了 SIGHUP 信号。login shell 是 session 首进程。
首先写一个测试程序,代码如下:
#include <stdio.h>
#include <signal.h>
char **args;
void exithandle(int sig)
{
    printf("%s : sighup received\n",args[1]);
}
int main(int argc,char **argv)
{
    args=argv;
    signal(SIGHUP,exithandle);
    pause();
    return 0;
}
程序中捕捉 SIGHUP 信号后打印一条信息,pause() 使程序暂停。
编译后的执行文件为 sigtest 。


1、
   命令: sigtest front > tt.txt

   操作: 关闭终端
   结果: tt文件的内容为 front : sighup received
   原因:  sigtest是前台进程,终端关闭后,根据上面提到的第1种情况,
          login shell作为session首进程,会收到SIGHUP信号然后退出,
          根据第2种情况,sigtest作为前台进程,
          会收到login shell发出的SIGHUP信号。

2、
   命令:sigtest back > tt.txt &

   操作: 关闭终端
   结果: tt文件的内容为 back : sighup received
   原因:  sigtest是提交的job,根据上面提到的第1种情况,
          sigtest会收到SIGHUP信号
3、
   写一个 shell,内容为

./sigtest &
   执行该 shell
   操作:关闭终端
   结果:ps -ef | grep sigtest 会看到该进程还在,tt文件为空
   原因: 执行该shell时,sigtest作为job提交,然后该shell退出,
         致使sigtest变成了孤儿进程,(从这里开始就是错误的)不再是当前session的job了,
         因此sigtest既不是session首进程也不是job,不会收到SIGHUP
         同时孤儿进程属于后台进程,因此login shell退出后不会发送SIGHUP
         给sigtest,因为它只将该信号发送给前台进程(一直错到这里)。
         第3条说过若进程组变成孤儿进程组的时候,若有进程处于停止状态,
         也会收到SIGHUP信号,但sigtest没有处于停止状态,
         所以不会收到SIGHUP信号  

4、
   命令:nohup sigtest > tt

   操作: 关闭终端 
   结果: tt 文件为空
   原因:  nohup 可以防止进程收到 SIGHUP 信号


至此,我们就清楚了何种情况下终端关闭后进程会退出,何种情况下不会退出。
要想终端关闭后进程不退出有以下几种方法,均为通过 shell 的方式:
1、 编写 shell,内容如下
trap "" SIGHUP  #该句的作用是屏蔽SIGHUP信号,trap可以屏蔽很多信号
sigtest
2、nohup sigtest 可以直接在命令行执行, 若想做完该操作后继续别的操作,可以 nohup sigtest &
3、编写 shell,内容如下
sigtest &
其实任何将进程变为孤儿进程的方式都可以,包括 fork 后父进程马上退出

另一个人的回答:
在这个环境下 GNU bash, version 3.2.25 ,前台进程,终端关闭后,进程会收到 2 次 SIGHUP 信号,一次为 bash 退出前由 bash 自己发送,一次为会话首进程(同样是 bash)退出后由 kernel 发送。

你可能感兴趣的:(SIGHUP)