终端断开导致Tomcat进程被kill问题分析
在测试环境中,我们经常会写类似这样的tomcat启动脚本,通过tail日志来检查启动是否成功
1 2 3
| #!/bin/bash ~/apache-tomcat-8.5.9/bin/startup.sh tail -f ~/catalina.out
|
有的时候我们会遇到tomcat进程启动完一段时间后就退出了的情况。通过测试发现,如果不退出脚本就直接关闭终端,tomcat就会在终端关闭后退出
TL;DR
有如下几种解决方式
1 2
| #!/bin/bash nohup ~/apache-tomcat-8.5.9/bin/startup.sh
|
1 2
| #!/bin/bash setsid ~/apache-tomcat-8.5.9/bin/startup.sh
|
1 2 3
| #!/bin/bash set -m ~/apache-tomcat-8.5.9/bin/startup.sh
|
原因分析
看下tomcat自身的启动脚本,没发现什么特殊的地方
startup.sh通过exec将自己替换为catalina.sh
catalina.sh直接以后台方式&启动了java,并且只有当OS为HP-UX时才会加上nohup
catalina.sh退出,java进程ppid变为1
用前面我们自己写的脚本启动一下,观察启动完以后的进程状态
1 2 3 4 5
| $ ps xo pid,ppid,pgid,sid,tty,tpgid,stat,command PID PPID PGID SID TT TPGID STAT COMMAND 1810 1799 1810 1799 pts/0 1810 S+ /bin/bash ./start.sh 1819 1 1810 1799 pts/0 1810 Sl+ /home/express/jdk1.8.0_101/bin/java -Djava.util.logging.config.file=/home/express/apache-tomcat-8.5.9/conf/logging.properties -Djava.util.logging.manager=org.ap 1820 1810 1810 1799 pts/0 1810 S+ tail -f /home/express/catalina.out
|
用strace跟踪一下tomcat进程,然后关闭终端,可以看到是SIGHUP信号导致了tomcat退出
1 2 3 4 5
| $ strace -f -e trace=signal -p1819 Process 1819 attached with 28 threads [pid 1850] --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=1799, si_uid=1000} --- ... +++ exited with 129 +++
|
既然是SIGHUP导致的退出,那么最直接的解决方式自然是用nohup了
那为什么以&方式后台启动的tomcat会收到SIGHUP信号,但是退出启动脚本后再关闭终端又不会有问题呢
这个问题先放一下,我们尝试退出脚本后再看下进程状态,这时start.sh, tail等进程都结束了,只剩下tomcat
1 2 3
| $ ps xo pid,ppid,pgid,sid,tty,tpgid,stat,command PID PPID PGID SID TT TPGID STAT COMMAND 1994 1 1985 1945 pts/0 1945 Sl /home/express/jdk1.8.0_101/bin/java -Djava.util.logging.config.file=/home/express/apache-tomcat-8.5.9/conf/logging.properties -Djava.util.logging.manager=org.ap
|
对比上面的状态图可以看到tomcat进程状态STAT发生了变化,从Sl+变成了Sl
通过man ps我们可以找到+号的意义
+ is in the foreground process group
继续查找原因需要了解一些进程和Job Control相关的知识
理解Unix进程
APUE - Chapter 9. Process Relationships
When does kernel send SIGHUP
Terminally confused (part six)

Job是交互式shell管理进程的一种抽象手段,借此shell可以批量操作进程,例如使用Ctrl-C会停止tail -f foo | grep bar这个job中所有的进程
Job通过进程组Process Group来实现,进程组是进程的集合,Session是进程组的集合,Session可以有一个控制终端Controlling Terminal(ctty)。
- 进程组分为一个前台进程组
foreground process group和多个后台进程组background process groups
- 前台进程组接收终端触发的信号,如
SIGINT,以及连接中断时的SIGHUP
Session Leader(建立session的进程,例如login shell)接收SIGHUP信号,并决定如何处理。Bash的处理方式是转发给所有的jobs,无论前后台
Orphaned Process Group 简单来说就是组内进程脱离了和Session Leader的联系的进程组,例如tomcat启动脚本,启动java后自身退出,java成为孤儿进程,ppid为1,进程组也成为孤儿进程组
简单做个实验,启动两组tail+grep的命令
1 2
| $ tail -f /dev/null | grep background & $ tail -f /dev/null | grep foreground
|
1 2 3 4 5 6 7
| $ ps xo pid,ppid,pgid,sid,tty,tpgid,stat,command PID PPID PGID SID TT TPGID STAT COMMAND 1945 1944 1945 1945 pts/0 2087 Ss -bash 2085 1945 2085 1945 pts/0 2087 S tail -f /dev/null 2086 1945 2085 1945 pts/0 2087 S grep --color=auto background 2087 1945 2087 1945 pts/0 2087 S+ tail -f /dev/null 2088 1945 2087 1945 pts/0 2087 S+ grep --color=auto foreground
|
可以看到两行命令形成了两个进程组,PGID分别为2085和2087,其中2087是前台进程组(STAT中有+)
用strace跟踪,并输入Ctrl-C,可以看到前台进程组里所有的进程都收到了信号
1 2 3 4 5
| $ strace -e trace=signal -p2085 -p2086 -p2087 -p2088 [pid 2087] --- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} --- [pid 2088] --- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} --- [pid 2087] +++ killed by SIGINT +++ [pid 2088] +++ killed by SIGINT +++
|
关闭终端,后台进程组中的进程收到bash转发的连接中断信号
1 2 3 4
| [pid 2086] --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=1945, si_uid=1000} --- [pid 2086] +++ killed by SIGHUP +++ --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=1945, si_uid=1000} --- +++ killed by SIGHUP +++
|
现在可以回答前面那个问题了
为什么以&方式后台启动的tomcat会收到SIGHUP信号,但是退出启动脚本后再关闭终端又不会有问题呢
因为启动脚本未退出时,tomcat位于前台进程组中,退出后tomcat位于孤儿进程组中
继续,为什么启动脚本中所有的命令都在同一个进程组中
因为执行脚本时(非交互shell)默认不启用Job Control,因此脚本内所有命令的PGID都同脚本本身一致
如果在脚本中启用Job Control会怎样
我们实验下,在脚本中加上set -m,现在每个命令都有了自己的进程组,并且只有tail是前台进程组,那tomcat自然不会收到SIGHUP了
1 2 3 4 5 6
| $ ps xo pid,ppid,pgid,sid,tty,tpgid,stat,command PID PPID PGID SID TT TPGID STAT COMMAND 2693 2692 2693 2693 pts/0 2722 Ss -bash 2712 2693 2712 2693 pts/0 2722 S /bin/bash ./start.sh 2721 1 2713 2693 pts/0 2722 Sl /home/express/jdk1.8.0_101/bin/java -Djava.util.logging.config.file=/home/express/apache-tomcat-8.5.9/conf/logging.properties -Djava.util.logging.manager=org. 2722 2712 2722 2693 pts/0 2722 S+ tail -f /home/express/catalina.out
|
还有其他途径吗
最彻底的,也是linux中启动守护进程的方法,就是启动tomcat时干脆就不和login shell在一个session里
实验下通过setsid来启动tomcat,计划通~
1 2 3 4 5 6
| $ ps xo pid,ppid,pgid,sid,tty,tpgid,stat,command PID PPID PGID SID TT TPGID STAT COMMAND 2693 2692 2693 2693 pts/0 2783 Ss -bash 2783 2693 2783 2693 pts/0 2783 S+ /bin/bash ./start.sh 2792 1 2784 2784 ? -1 Sl /home/express/jdk1.8.0_101/bin/java -Djava.util.logging.config.file=/home/express/apache-tomcat-8.5.9/conf/logging.properties -Djava.util.logging.manager=org. 2793 2783 2783 2693 pts/0 2783 S+ tail -f /home/express/catalina.out
|