这篇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, 避免这个问题.
非常到位
非常到位
非常到位