程序员但凡工作时间久一点,总会遇到一些诡异的事情,比如每当你下班时,服务就挂,然后业务同学就各种找过来了,似乎业务与服务程序就离不开你一样。
而当你登录机器去排查问题时,又发现机器上连进程都没了,心里咯噔一下慌了神,进程咋就消失了?
刚接触Linux的同学,可能都不知道Shell里面前台任务与后台任务的概念,先介绍一下这个,如下:
看个例子:
# 启动前台进程,然后按Ctrl+z使其变后台进程
$ java -jar app.jar
^Z
[1]+ Stopped java -jar app.jar
# 查询后台进程,可发现它是Stopped状态
$ jobs -l
[1]+ 19316 Stopped java -jar app.jar
# 使用bg后,可以发现任务变Running状态了
$ bg %1
[1]+ java -jar app.jar &
$ jobs
[1]+ Running java -jar app.jar &
# 再使用fg,会发现任务变成前台任务了
$ fg %1
# 直接使用&符号,直接就是后台任务并且Running状态
$ java -jar app.jar &
[1] 19620
$ jobs
[1]+ Running java -jar app.jar &
然而,如果你使用上面的方式启动进程,当你下班关掉电脑时,ssh终端会失去连接,你的java进程就会被杀死。
因为在shell里面启动的进程,都是shell这个进程的子进程,ssh失去连接时,shell进程会给它的子进程发SIGHUP信号,这会杀死在shell中启动的子进程,包括后台进程也不例外。
使用nohup命令可以解决这个问题,通过nohup命令启动的进程,会忽略SIGHUP信号,从而让进程能活下来,如下:
$ nohup java -jar app.jar &
如果之前没有使用nohup启动进程,可以使用disown命令使得进程忽略SIGHUP信号,如下:
除了nohup外,还可以使用tmux、screen等伪终端软件,一样可以避免终端断开时进程被杀死,如下:
# ubuntu安装tmux
$ sudo apt install tmux
# 新建一个伪终端会话,会话名为app
$ tmux new -s app
# 启动进程,注意现在已经在tmux伪终端里面了,这和平时操作没什么两样
# 如果要退出伪终端,先按Ctrl + b再按d,不要使用Ctrl+c
$ java -jar app.jar
# 列出所有伪终端会话
$ tmux ls
# 重新attach进入app这个会话,这样就可以看任务运行情况了
$ tmux attach -t app
除了安全启动后台进程外,tmux还可以实现多窗口、分屏等高级功能,感兴趣可以man tmux查看。
我们知道kill可以杀死进程,但其实从原理上来讲,进程并不是被kill杀死的,kill只是给进程发送了一个信号,进程若不捕获信号,内核会执行默认处理程序杀死进程。
通过kill -l可以查看kill命令能够发送的信号,如下:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
前面是信号编号,后面是信号名称,可以发现所谓kill -9其实就是向进程发送SIGKILL信号,也可以写成kill -SIGKILL。
如下是常见信号出现的场景:
信号 | 默认行为 | 场景 |
---|---|---|
SIGHUP | 终止进程 | 终端挂断,比如远程ssh断网 |
SIGINT | 终止进程 | 来自键盘的中断,Ctrl+c就是发出此信号 |
SIGQUIT | 终止进程 | 来自键盘的退出,Ctrl+\发此信号 |
SIGKILL | 终止进程 | 杀死程序,kill -9发此信号 |
SIGPIPE | 终止进程 | 向一个没有读用户的管道做写操作,比如Socket网络连接,远端关闭连接后,本端还继续写 |
SIGTERM | 终止进程 | 软件终止信号,kill默认发此信号 |
SIGCHLD | 终止进程 | 一个子进程停止或终止 |
SIGCONT | 终止进程 | 如果进程停止,继续该进程 |
SIGTSTP | 终止进程 | 来自终端的停止信号,Ctrl+z发此信号 |
除了上面的情况外,oom(内存溢出)也是一种常见的进程消失原因,程序中申请了大量的内存,内存不足导致进程死亡,分如下两种情况:
对于java这样的程序,程序使用内存超过-Xmx阈值,jvm会自动退出,并在标准输出流中留下java.lang.OutOfMemoryError异常。
因此启动java进程时,最好使用重定向将标准输出与标准错误保存到日志文件中,然后就可以通过如下方式确认是否oom了:
# 启动进程时,将标准输出与标准错误保存到日志文件中
$ nohup java -jar app.jar >stdout.log 2>stderr.log &
# 搜索是否存在OutOfMemoryError异常
$ grep -A5 'OutOfMemoryError' stdout.log stderr.log
stdout.log:java.lang.OutOfMemoryError: Java heap space
stdout.log- at java.base/java.lang.StringCoding.decodeUTF8_0(StringCoding.java:753) ~[na:na]
stdout.log- at java.base/java.lang.StringCoding.decodeUTF8(StringCoding.java:712) ~[na:na]
stdout.log- at java.base/java.lang.StringCoding.decode(StringCoding.java:318) ~[na:na]
stdout.log- at java.base/java.lang.String.<init>(String.java:592) ~[na:na]
stdout.log- at java.base/java.lang.String.<init>(String.java:614) ~[na:na]
这是不熟悉Linux的同学比较容易忽略的一个点,Linux系统如果内存不足了,会使用oom-killer机制找一个内存占用大的进程牺牲掉,而一般Java进程占用内存都挺大,所以它经常被牺牲。
而oom-killer杀死进程时,会打印一些信息在dmesg日志中,因此,可以通过如下方式确认进程消失是否是被oom-killer杀死:
$ dmesg -T | grep -A10 -i "kill"
[Fri Dec 17 22:35:47 2021] Memory cgroup out of memory: Killed process 3102186 (java) total-vm:10487368kB, anon-rss:287124kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:3764kB oom_score_adj:0
[Fri Dec 17 22:35:47 2021] oom_reaper: reaped process 3102186 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
注:dmesg相当于是内核日志,内核会记录一些关键信息在其中,如果排查某些问题没有头绪时,养成习惯常规性看一下dmesg的内容,说不定能发现啥呢!