标签归档:signal_interrupt

使用strace定位jvm退出的原因范围

今天遇到的一个tomcat启动过程中jvm退出的问题,不是jvm crash的情况,用户日志配置的不正确导致一些信息没有展现出来,只看到pandora执行了shutdownhook的信息。这可能是启动时的逻辑有触发System.exit,或被系统或人为kill掉了。

根据以往的经验,排除了oom killer或ulimit -t设置不当导致被内核给kill掉的情况,OS级别的signal通常不留机会给jvm执行shutdownhook的。如此一来singal的范围应该就是SIGTERM, SIGINT, SIGHUP这3种(参考这里)。

虽然singal范围缩小,但依然不能确定是因为代码里调用了System.exit还是人为(或被其他进程)kill引起的。直接上大招用systemtap需要安装kernal debuginfo,没有权限的话,还要找到对应的人去做;如果现象较容易重现的话,可以先通过strace命令进一步缩小问题的范围,究竟是因为jvm内部执行了System.exit还是外界的kill引起的。

这里通过启动一个scala的repl来模拟java进程,通过strace attach到jvm进程上,然后观察,如果是外界的kill所致,可以看到下面的信息:

$ sudo strace -p 1947
Process 1947 attached - interrupt to quit
futex(0x7fb7635959d0, FUTEX_WAIT, 1948, NULL) = ? ERESTARTSYS (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
futex(0x7fb762762360, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn(0x7fb762762360)            = 202
futex(0x7fb7635959d0, FUTEX_WAIT, 1948, NULLPANIC: attached pid 1947 exited with 143
 <unfinished ... exit status 143>

里面的关键信息是SIGTERMexit status 143(即SIGTERM的code)

如果是kill -2或ctrl-c终止repl,可以看到有关SIGINT的信息

$ sudo strace -p 1813
Process 1813 attached - interrupt to quit
futex(0x7fb24d15a9d0, FUTEX_WAIT, 1814, NULL) = ? ERESTARTSYS (To be restarted)
--- SIGINT (Interrupt) @ 0 (0) ---
futex(0x7fb24c327360, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn(0x7fb24c327360)            = 202
futex(0x7fb24d15a9d0, FUTEX_WAIT, 1814, NULLPANIC: attached pid 1813 exited with 130
 <unfinished ... exit status 130>

如果是jvm自身执行了System.exit比如:

scala> System.exit(0)

那么在跟踪的信息里,是看不到signal的:

$ sudo strace -p 2131
Process 2131 attached - interrupt to quit
futex(0x7fc14adb49d0, FUTEX_WAIT, 2132, NULLPANIC: attached pid 2131 exited with 0
 <unfinished ... exit status 0>

至此我们可以判断出到底是外部还是内部引起的了,如果是内部就不必麻烦Systemtap了,可以从源码去找。

shell脚本里后台进程忽略SIGINT信号问题排查

这篇blog是同事涧泉在和我一起分析tomcat进程意外退出时,针对`SIGINT`的疑惑,他的一些记录,我这里转载一下。

问题简述

写一个简单的 C 语言程序 “a.c”, 用一个死循环 hold 住.

int main() {
    for (;;) { }
    return 0;
}

写一个简单的 makefile 文件. 为了方便调试, 添加 -g 参数生成调试信息.

a: a.c
    gcc -g a.c -o a

编译程序:

[observer.hany@v125205215 ~/tmp]$ make
gcc -g a.c -o a

在当前 shell 下后台运行程序, 使用 kill 命令向进程发 SIGINT 信号, 可看到进程正常中断退出.

[observer.hany@v125205215 ~/tmp]$ ./a & 
[1] 32533
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241    32533  9744 98 11:37 pts/1    00:00:05 ./a

[observer.hany@v125205215 ~/tmp]$ kill -SIGINT 32533
[observer.hany@v125205215 ~/tmp]$ jobs
[1]+  中断                    ./a
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD

编写如下简单 sh 脚本 “a.sh”.

#!/bin/bash
./a &

使用脚本启动后台进程, 再使用 kill 向进程发送 SIGINT 信号, 进程却不会退出.

[observer.hany@v125205215 ~/tmp]$ ./a.sh 
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241    12448     1 81 11:50 pts/1    00:00:03 ./a

[observer.hany@v125205215 ~/tmp]$ kill -2 12448
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241    12448     1 98 11:50 pts/1    00:37:24 ./a

但直接 kill (默认发送 SIGTERM 信号) 可以使进程退出.

[observer.hany@v125205215 ~/tmp]$ kill 12448
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD

确认进程是否收到信号

尝试用 strace 命令查看两种情况下程序运行的差异.

第一种情况, shell 下直接运行程序:

[observer.hany@v125205215 ~/tmp]$ ./a &
[1] 5309
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241     5309  9744 99 12:51 pts/1    00:00:03 ./a

使用 strace 监控进程:

[observer.hany@v125205215 ~]$ sudo strace -p 5309
Process 5309 attached - interrupt to quit

发送 SIGINT 信号, 可以看到进程退出了.

[observer.hany@v125205215 ~/tmp]$ kill -SIGINT 5309
[observer.hany@v125205215 ~/tmp]$ jobs
[1]+  中断                    ./a
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD

strace 监控到完整输出如下:

[observer.hany@v125205215 ~]$ sudo strace -p 5309
Process 5309 attached - interrupt to quit
--- SIGINT (Interrupt) @ 0 (0) ---
Process 5309 detached

第二种情况, 使用脚本在后台运行程序:

[observer.hany@v125205215 ~/tmp]$ ./a.sh 
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241     9512     1 99 12:55 pts/1    00:00:03 ./a

使用 strace 监控进程:

[observer.hany@v125205215 ~]$ sudo strace -p 9512
Process 9512 attached - interrupt to quit

发送 SIGINT 信号, 进程没有退出.

[observer.hany@v125205215 ~/tmp]$ kill -SIGINT 9512
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241     9512     1 98 12:55 pts/1    00:02:07 ./a

strace 可以看到进程确实收到了 SIGINT 信号, 但进程没有退出.

[observer.hany@v125205215 ~]$ sudo strace -p 9512
Process 9512 attached - interrupt to quit
--- SIGINT (Interrupt) @ 0 (0) ---

直接 kill (默认发送 SIGTERM 信号), 进程才退出.

[observer.hany@v125205215 ~/tmp]$ kill 9512
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD

完整 strace 输出如下:

[observer.hany@v125205215 ~]$ sudo strace -p 9512
Process 9512 attached - interrupt to quit
--- SIGINT (Interrupt) @ 0 (0) ---
--- SIGTERM (Terminated) @ 0 (0) ---
Process 9512 detached

确认进程信号处理逻辑

查了一下 gnu libc Termination-Signals 文档和 linux man 7 signal 文档,
默认情况下进程对 SIGINT 和 SIGTERM 信号的处理过程都是 Term, 终止进程.

strace 只能看到进程收到了信号, 看不到进程对信号的处理过程.
猜想是不是可能两者的处理逻辑并不完全一致,
SIGINT 做了什么特殊判断, 在某种特殊情况下不终止程序.

尝试用 systemtap 监控 probe::signal.handle, 看是否能找到两种信号处理程序的差异.
因为测试机上 linux 内核和 stap 版本太低, 无法监控 “signal.handle”.
要调试具体信号处理逻辑, 需要调试到 linux 信号处理的内核代码,
目前难以做到, 放弃了这种方式.

查看进程信号处理 handler

因为不用 shell 启动进程时, 如 ubuntu 下 “Alt + F2” 运行程序,
进程也是会被 SIGINT 正常终止的.
怀疑是 shell 修改了进程的默认信号处理器.
尝试用 systemtap 监控 “syscall.sigaction” 系统调用,
测试机上的 linux 内核和 stap 版本也不支持监控 “syscall.sigaction”.
尝试监控了一下 probe::signal.do_action, 输出信息太多, 没有找到有用信息.

修改程序代码 “a.c”, 让进程启动时自己查询对应的信号处理器, 重新编译程序.

#include <stdio.h>
#include <signal.h>

struct sigaction hdl_int;
struct sigaction hdl_term;

int main() {
    sigaction(SIGINT, NULL, &hdl_int);
    sigaction(SIGTERM, NULL, &hdl_term);
    for (;;) { }
    return 0;
}

第一种情况, 直接在 shell 启动程序:

[observer.hany@v125205215 ~/tmp]$ ./a &
[1] 29385
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241    29385  9744 99 13:50 pts/1    00:00:04 ./a

使用 gdb attach 到进程 (省略了一些冗余输出):

[observer.hany@v125205215 ~/tmp]$ gdb a 29385
GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-37.el5)
... ...
Loaded symbols for /lib64/ld-linux-x86-64.so.2
main () at a.c:10
10      for (;;) { }

可以看到进程中断到第 10 行死循环的位置, 使用 gdb 查看获取的两个信号处理器信息:

(gdb) p hdl_int
$1 = {__sigaction_handler = {sa_handler = 0, sa_sigaction = 0}, sa_mask = {__val = {0, 
      0, 140526787057128, 1, 0, 1, 210456656896, 140733374433184, 0, 210455544752, 
      140526787062592, 4476012448, 4294967295, 140526787064672, 6293608, 0}}, 
  sa_flags = 0, sa_restorer = 0x31002302d0 <__restore_rt>}
(gdb) p hdl_term
$2 = {__sigaction_handler = {sa_handler = 0, sa_sigaction = 0}, sa_mask = {__val = {0, 
      0, 140526787057128, 1, 0, 1, 210456656896, 140733374433184, 0, 210455544752, 
      140526787062592, 4476012448, 4294967295, 140526787064672, 6293608, 0}}, 
  sa_flags = 0, sa_restorer = 0x31002302d0 <__restore_rt>}

看到两个信号处理器都是 “0”, 从信号处理头文件中可找到如下常量定义:

/* Fake signal functions.  */
#define SIG_ERR ((__sighandler_t) -1)           /* Error return.  */
#define SIG_DFL ((__sighandler_t) 0)            /* Default action.  */
#define SIG_IGN ((__sighandler_t) 1)            /* Ignore signal.  */

可知 “0” 表示默认信号处理程序, 但实际的信号处理逻辑函数地址是看不到的.

向进程发送 SIGINT 信号, 进程终止:

(gdb) signal SIGINT
Continuing with signal SIGINT.

Program terminated with signal SIGINT, Interrupt.
The program no longer exists.

第二中情况, 使用脚本启动后台进程.

[observer.hany@v125205215 ~/tmp]$ ./a.sh 
[observer.hany@v125205215 ~/tmp]$ ps -C a -f
UID        PID  PPID  C STIME TTY          TIME CMD
54241    13670     1 99 14:08 pts/0    00:00:03 ./a

gdb 重新 attach 到新进程:

(gdb) attach 13670
Attaching to program: /home/observer.hany/tmp/a, process 13670
Reading symbols from /usr/local/snoopy/lib/snoopy.so...done.
Loaded symbols for /usr/local/snoopy/lib/snoopy.so
Reading symbols from /lib64/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/libdl.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/libdl.so.2
Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
main () at a.c:10
10      for (;;) { }

查看信号处理器信息:

(gdb) p hdl_int
$3 = {__sigaction_handler = {sa_handler = 0x1, sa_sigaction = 0x1}, sa_mask = {__val = {
      0, 0, 140457683965416, 1, 0, 1, 210456656896, 140733569554304, 0, 210455544752, 
      140457683970880, 4671133568, 4294967295, 140457683972960, 6293608, 0}}, 
  sa_flags = 0, sa_restorer = 0x31002302d0 <__restore_rt>}
(gdb) p hdl_term
$4 = {__sigaction_handler = {sa_handler = 0, sa_sigaction = 0}, sa_mask = {__val = {0, 
      0, 140457683965416, 1, 0, 1, 210456656896, 140733569554304, 0, 210455544752, 
      140457683970880, 4671133568, 4294967295, 140457683972960, 6293608, 0}}, 
  sa_flags = 0, sa_restorer = 0x31002302d0 <__restore_rt>}

这时看到 SIGINT 的信号处理器 hdl_int 是常量 “1”,
从头文件常量定义可查到即是 SIG_IGN, 忽略信号.

问题终于水落石出了, 原来是第二种情况, 在 shell 脚本运行后台程序时,
SIGINT 的信号处理器被设置成了 SIG_IGN, 忽略信号.

发送 SIGINT 信号, 可看到程序继续运行不会退出,
发送 SIGTERM 信号后, 程序终止.

(gdb) signal SIGINT
Continuing with signal SIGINT.
^C
Program received signal SIGINT, Interrupt.
main () at a.c:10
10      for (;;) { }
(gdb) signal SIGTERM
Continuing with signal SIGTERM.

Program terminated with signal SIGTERM, Terminated.
The program no longer exists.

确认是进程信号处理器被修改,
重新修改监控 probe::signal.do_action 的 stap 脚本,
缩小监控范围, 只监控信号为 SIGINT (常量 “2”) 并且用户 pid 为当前测试用户的调用.

function time_str() {
    return ctime(gettimeofday_s() + 8 * 60 * 60);
}

probe begin {
    printdln(" ", time_str(), "BEGIN");
}

probe end {
    printdln(" ", time_str(), "END");
}

probe signal.do_action {
    if (sig != 2
        || uid() != 54241
        // || execname() != "a"
        // || execname() != "bash"
        ) {
        next;
    }
    printdln(" ", time_str(), execname(), name,
        sig_name, sa_handler,
        "")
}

启动 stap 监控脚本:

[observer.hany@v125205215 ~]$ sudo stap -g trace-sig.stp
[sudo] password for observer.hany: 
Tue Feb 11 14:26:43 2014 BEGIN

再次执行 ./a.sh 脚本, 果然监控到如下输出:

Tue Feb 11 14:29:50 2014 bash do_action SIGINT 4491008 
Tue Feb 11 14:29:50 2014 bash do_action SIGINT 4491008 
Tue Feb 11 14:29:50 2014 bash do_action SIGINT 0 
Tue Feb 11 14:29:50 2014 a.sh do_action SIGINT 0 
Tue Feb 11 14:29:50 2014 a.sh do_action SIGINT 0 
Tue Feb 11 14:29:50 2014 a.sh do_action SIGINT 0 
Tue Feb 11 14:29:50 2014 a.sh do_action SIGINT 1 
Tue Feb 11 14:29:50 2014 bash do_action SIGINT 4491008 
Tue Feb 11 14:29:50 2014 bash do_action SIGINT 4491008 
Tue Feb 11 14:29:50 2014 bash do_action SIGINT 4703312 
Tue Feb 11 14:29:50 2014 a do_action SIGINT 0 

确认是 “a.sh” 脚本进程执行了将 SIGINT 的处理器设置成 “1”, 即 SIG_IGN, 的操作.

非交互式 shell 问题

后来找到 Get rid of SIGINT 这篇文章 (需要翻墙才能访问),
讲到这是非交互式 shell 的一个问题.

shell 命令行下是交互式模式 (interactive),
运行是脚本时是非交互式模式 (non-interactive).
非交互式 shell 默认禁用了 job control,
这时启动后台进程时 shell 会设置后台进程忽略 SIGINT 等信号.
大概行为伪码如下:

if(!fork()) {
  /* child */
  signal(SIGINT, SIG_IGN);
  signal(SIGQUIT, SIG_IGN);

  execve(...cmd...);
}

execve 启动进程时会继承信号处理器:

  • POSIX.1-2001 specifies that the dispositions of any signals that are
    ignored or set to the default are left unchanged.

因此默认情况下 shell 脚本启动的后台进程会忽略 SIGINT 等信号.
可以在 shell 脚本中设置 set -m 打开 job control, 避免这个问题.

tomcat进程意外退出的问题分析

节前某个部门的测试环境反馈tomcat会意外退出,我们到实际环境排查后发现不是jvm crash,日志里有进程销毁的记录,从pause到destory的整个过程:

org.apache.coyote.AbstractProtocol pause
Pausing ProtocolHandler
org.apache.catalina.core.StandardService stopInternal
Stopping service Catalina
org.apache.coyote.AbstractProtocol stop
Stopping ProtocolHandler
org.apache.coyote.AbstractProtocol destroy
Destroying ProtocolHandler

从上面日志来可以判断:

1) tomcat不是通过脚本正常关闭(viaport: 即通过8005端口发送shutdown指令)

因为正常关闭(viaport)的话会在 pause 之前有这样的一句warn日志:

    org.apache.catalina.core.StandardServer await
    A valid shutdown command was received via the shutdown port. Stopping the Server instance.
    然后才是 pause -> stop -> destroy 
2) tomcat的shutdownhook被触发,执行了销毁逻辑

而这又有两种情况,一是应用代码里有地方用System.exit来退出jvm,二是系统发的信号(kill -9除外,SIGKILL信号JVM不会有机会执行shutdownhook)

先通过排查代码,应用方和中间件团队都排查了System.exit在这个应用中使用的可能。那就只剩下Signal的情况了;经过一番排查后,发现每次tomcat意外退出的时间与ssh会话结束的时间正好吻合。

有了这个线索之后,银时同学立刻看了一下对方测试环境的脚本,简化后如下:

$ cat test.sh
#!/bin/bash
cd /data/server/tomcat/bin/
./catalina.sh start
tail -f /data/server/tomcat/logs/catalina.out

tomcat启动为后,当前shell进程并没有退出,而是挂住在tail进程,往终端输出日志内容。这种情况下,如果用户直接关闭ssh终端的窗口(用鼠标或快捷键),则java进程也会退出。而如果先ctrl-c终止test.sh进程,然后再关闭ssh终端的话,则java进程不会退出。

这是一个有趣的现象,catalina.sh start方式启动的tomcat会把java进程挂到init(进程id为1)的父进程下,已经与当前test.sh进程脱离了父子关系,也与ssh进程没有关系,为什么关闭ssh终端窗口会导致java进程退出?

我们的推测是ssh窗口在关闭时,对当前交互的shell以及正在运行的test.sh等子进程发送某个退出的Signal,找了一台装有systemtap的机器来验证,所用的stap脚本是从涧泉同学那里copy的:

function time_str: string () {
    return ctime(gettimeofday_s() + 8 * 60 * 60);
}

probe begin {
    printdln(" ", time_str(), "BEGIN");
}

probe end {
    printdln(" ", time_str(), "END");
}

probe signal.send {
    if (sig_name == "SIGHUP" || sig_name == "SIGQUIT" || 
        sig_name=="SIGINT" || sig_name=="SIGKILL" || sig_name=="SIGABRT") {
        printd(" ", time_str(), sig_name, "[", uid(), pid(), cmdline_str(), 
                "] -> [", task_uid(task), sig_pid, pid_name, "], ");
        task = pid2task(pid());
        while (task_pid(task) > 0) {
            printd(" ", "[", task_uid(task), task_pid(task), task_execname(task), "]");
            task = task_parent(task);
        }
        println("");
    }
}

模拟时的进程层级(pstree)大致如下,tomcat启动后java进程已经脱离test.sh,挂在init下:

|-sshd(1622)-+-sshd(11681)---sshd(11699)---bash(11700)---test.sh(13285)---tail(13299)

经过内核组伯俞的协助,我们发现

a) 用 ctrl-c 终止当前test.sh进程时,系统events进程向 java 和 tail 两个进程发送了SIGINT 信号
SIGINT [ 0 11  ] -> [ 0 20629 tail ] 
SIGINT [ 0 11  ] -> [ 0 20628 java ] 
SIGINT [ 0 11  ] -> [ 0 20615 test.sh ] 

注pid 11是events进程
b) 关闭ssh终端窗口时,sshd向下游进程发送SIGHUP, 为何java进程也会收到?
SIGHUP [ 0 11681 sshd: hongjiang.wanghj [priv] ] -> [ 57316 11700 bash ] 
SIGHUP [ 57316 11700 -bash ] -> [ 57316 11700 bash ]
SIGHUP [ 57316 11700 ] -> [ 0 13299 tail ] 
SIGHUP [ 57316 11700 ] -> [ 0 13298 java ] 
SIGHUP [ 57316 11700 ] -> [ 0 13285 test.sh ] 

不过伯俞很忙没有继续协助分析这个问题(他给出了一些猜测,但后来证明并不是那样)。

确定了是由signal引起的之后,我的疑惑变成了:

1) 为什么SIGINT (kill -2) 不会让tomcat进程退出?
2) 为什么SIGHUP (kill -1) 会让tomcat进程退出?

我第一反应可能是jvm在某些参数下(或因为某些jni)对os的信号处理会不同,看了一下应用的jvm参数,没有看出问题,也排除了tomcat使用apr/tcnative的情况。

我们看一下默认情况下,jvm进程对SIGINTSIGHUP是怎么处理的,用scala的repl模拟一下:

scala> Runtime.getRuntime().addShutdownHook(
            new Thread() { override def run() { println("ok") } })

对这个java进程分别用kill -2kill -1发现都会导致jvm进程退出,并且也触发shutdownhook。这也符合oracle对hotspot虚拟机处理Signal的说明,参考这里SIGTERM,SIGINT,SIGHUP三种信号都会触发shutdownhook

看来并不是jvm的事,继续猜测是否与进程的状态有关?catalina.sh脚本里并没有使用start-stop-daemon之类的方式启动java进程,start参数的执行方式简化后脚本相当于:

eval '"/pathofjdk/bin/java"' 'params' org.apache.catalina.startup.Bootstrap start '&'

就是简单的把java放到后台执行。当catalina.sh自身进程退出后,java进程的ppid变成了1

花了很多的时间猜测可能是OS层面的原因,后来发现并没有关系。春节后回来让少明和涧泉也一起分析这个问题,因为他们有c的背景,对系统底层知道的多一些,用了大半天时间,不断猜测和验证,最后确认了是Shell的原因。

SIGINT (kill -2) 不会让后台java进程退出的原因

为了简便,我们用sleep来模拟进程,当我们在交互模式下:

$ sleep 1000 & 

$ ps -opid,pgid,ppid,stat,cmd -C sleep
  PID  PGID  PPID STAT CMD
 9897  9897  9813 S    sleep 1000   

注意,进程sleep 1000的pid与pgid(进程组)是相同的,这时我们用kill -2是可以杀掉sleep 1000进程的。

现在我们把sleep进程放到一个脚本里后台执行:

$ cat a.sh
#!/bin/sh
sleep 4400 &
echo "shell exit"

运行a.sh脚本之后,sleep 4400进程的pid与pgid是不同的,pgid是其父进程的id,即已经退出了的a.sh进程

$ ps -opid,pgid,ppid,comm -p 63376
  PID  PGID  PPID COMM
63376 63375     1 sleep

这时我们用kill -2是杀不掉sleep 4400进程的。

到了这一步,已经非常接近原因了,一定是shell对后台进程signal_handler做了什么手脚。少明实现了一个自定handler的命令看看是否对kill -2有效:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void my_handler(int sig) {
    printf("handler aaa\n");
    exit(0);
}

int main() {
    signal(SIGINT, my_handler);
    for(;;) { }
    return 0;
}

我们把编译后的a.out命令在脚本里以后台方式运行:

$ cat a.sh
#!/bin/sh
/tmp/a.out &

这次再尝试用kill -2去杀a.out进程,是可以的。这说明shell对signal_handler做手脚是在执行用户逻辑之前,也就是脚本在fork出子进程的时候就设置了。按照这个线索我们google后了解到: shell在非交互模式下对后台进程处理SIGINT信号时设置的是IGNORE

交互模式与非交互模式对作业控制(job control)默认方式不同

为什么在交互模式下shell不会对后台进程处理SIGINT信号设置为忽略,而非交互模式下会设置为忽略呢?还是比较好理解的,举例来说,我们先某个前台进程运行时间太长,可以ctrl-z中止一下,然后通过bg %n把这个进程放入后台,同样也可以把一个cmd &方式启动的后台进程,通过fg %n放回前台,然后在ctrl-c停止它,当然不能忽略SIGINT

为何交互模式下的后台进程会设置一个自己的进程组ID呢?因为默认如果采用父进程的进程组ID,父进程会把收到的键盘事件比如ctrl-c之类的SIGINT传播给进程组中的每个成员,假设后台进程也是父进程组的成员,因为作业控制的需要不能忽略SIGINT,你在终端随意ctrl-c就可能导致所有的后台进程退出,显然这样是不合理的;所以为了避免这种干扰后台进程设置为自己的pgid。

而非交互模式下,通常是不需要作业控制的,所以作业控制在非交互模式下默认也是关闭的(当然也可以在脚本里通过选项set -m打开作业控制选项)。不开启作业控制的话,脚本里的后台进程可以通过设置忽略SIGINT信号来避免父进程对组中成员的传播,因为对它来说这个信号已经没有意义。

回到tomcat的例子,catalina.sh脚本通过start参数启动的时候,就是以非交互方式后台启动,java进程也被shell设置了忽略SIGINT信号,因此在ctrl-c结束test.sh进程时,系统发送的SIGINT对java没有影响。

SIGHUP (kill -1) 让tomcat进程退出的原因

在非交互模式下,shell对java进程设置了SIGINTSIGQUIT信号设置了忽略,但并没有对SIGHUP信号设为忽略。再看一下当时的进程层级:

|-sshd(1622)-+-sshd(11681)---sshd(11699)---bash(11700)---test.sh(13285)---tail(13299)

sshd把SIGHUP传递给bash进程后,bash会把SIGHUP传递给它的子进程,并且对于其子进程test.sh,bash还会对test.sh的进程组里的成员都传播一遍SIGHUP。因为java后台进程从父进程catalina.sh(又是从其父进程test.sh)继承的pgid,所以java进程仍属于test.sh进程组里的成员,收到SIGHUP后退出。

如果我们在test.sh里设置开启作业控制的话,就不会让java进程退出了

#!/bin/bash
set -m  
cd /home/admin/tt/tomcat/bin/
./catalina.sh start
tail -f /home/admin/tt/tomcat/logs/catalina.out

此时java后台进程继承父进程catalina.sh的pgid,而catalina.sh不再使用test.sh的进程组,而是自己的pid作为pgid,catalina.sh进程在执行完退出后,java进程挂到了init下,java与test.sh进程就完全脱离关系了,bash也不会再向它发送信号。