El Psy Congroo

终端断开导致Tomcat进程被kill问题分析

终端断开导致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分别为20852087,其中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