问题描述
起初是为了 systemd 的 service 单元文件中的 ExecStop 指令才整理的这篇文章,后来看 systemd 的文档说执行 stop 时,执行完 ExecStop 指令后,未结束的进程会由 systemd 来结束。
本来没有什么可写的,直接使用 kill(1) 命令来结束进程就可以了。但是,由几个有意思的问题:
1)如何结束一个进程的全部子进程?
2)如何结束一个进程及其子进程?
3)我想结束某个组或某个用户的进程该怎么做?
通常结束一个进程的时候,它的子进程不一定会退出,子进程可能会变成“孤儿进程”:
所以有的时候,我们就需要结束某个进程以及它的子进程。
我们先从基础的开始吧。
根据进程号,结束进程
在 Linux 中,想要结束一个进程可以直接使用kill(1)命令并指定进程ID(PID)就可以了。比如,我们想要结束PID为3219的进程,只需要执行如下命令:
kill -9 3219 kill -KILL 3219
上面的两个命令是100%完全等价的,只是形式不一样而已,具体参考 kill(1) 手册。唯一需要注意:在 Shell 中,内建的kill命令,参考「注意事项」部分。
根据父进程号,结束子进程
如果想要结束进程所有的子进程的ID,可以使用pkill(1)命令。比如,我们想要结束PID为3219的进程的子进程,只需要执行如下命令:
pkill -KILL -P 3219
该命令可以结束某个进程的全部子进程。
结束进程及其全部子进程
方法一、使用进程组
可以使用kill(1)命令或者pkill(1)命令,配合“进程组”的ID来结束进程。比如进程组的ID为4536如下:
kill -KILL -4536 pkill -KILL -g 4536
注意,这里的“4536”是“进程组”的ID,不是进程ID,不是父进程ID,也不是进程所属组的ID,有一个名词叫做“进程组”(Process Group)。可以用下面的命令来体会组ID(gid)、进程组ID(pgid)及其他ID之间的差异,注意观察各个字段的输出:
ps -o pid,ppid,pgid,gid,sess,cmd -U root
上面的ID分别是进程ID、父进程ID、进程组的ID、进程的组ID、会话、命令。
方法二、使用一点 Shell 命令
但是下面的这条命令应该是一劳永逸了:
kill -KILL $(ps -o pid= --pid 6234 --ppid 6234)
上述命令结束进程ID为6234的全部进程以及它的子进程。
根据GID结束进程
如果要结束属于某个组的进程,可以使用pkill(1)命令。如下,结束GID为34的全部进程:
pkill -KILL -G 34 pkill -KILL -G mail
结束GID为34的组的全部进程。代表,GID的数值34也可以使组名来代替,如上的两个命令是等价的,因为GID为34的组名为“mail”。
根据终端来结束进程
可以使用pkill(1)命令,根据/dev/下的终端来结束进程:
pkill -9 -t 'pts/4'
结束终端为“pts/4”的进程,不需要“/dev/“前缀。但这也仅仅是适用于关联终端的进程。有些后台进程没有与终端关联。
根据进程名来结束进程
可以使用 pkill 命令,配合进程名来结束进程。如下示例,结束进程名为“xterm”的进程:
pkill -KILL -x "xterm"
根据会话来结束进程
依旧是使用 pkill 命令来结束进程。如下示例,结束会话号为 1256 的全部进程:
pkill -KILL -s 1256
结束僵尸进程(Kill Zombie Processes)
Alternative way to kill a zombie process – Unix & Linux Stack Exchange
linux – How to kill zombie process – Stack Overflow
How to find zombie process? – Ask Ubuntu
process – Zombies in bash – Unix & Linux Stack Exchange
c++ – gdb in docker container returns “ptrace: Operation not permitted.” – Stack Overflow
创建僵尸进程
创建持续 10s(而实际上是小于十秒的)的僵尸进程:
(sleep 1 & exec /bin/sleep 10)
命令解释如下:
1)进程 /bin/sleep 10 替代运行 sleep 1 的 Shell 进程,此时 sleep 1 的父进程为 /bin/sleep 10
2)然后,当 sleep 1 退出时,父进程(/bin/sleep 10)无法处理进程退出(没有这个能力),因此 sleep 1 保持僵尸状态;
3)当父进程(/bin/sleep 10)退出后,系统会为子进程(sleep 1)寻找新的父进程,而新父进程(可能是 init 进程)将处理该子进程;
通过如下方法定位僵尸进程
ps aux | grep 'Z'
方法一、向父进程发送 CHID 信号
kill -CHLD <PPID>
但是,在多数情况下是失败的,因为父进程没有合理处理 CHLD 信号
方法二、通过结束父进程的方式(过于野蛮)
通过结束父进程的方式,来结束僵尸进程:
kill -KILL <PPID>
该方法最大的问题是:需要结束父进程,这在部分情况下是不可接受的
方法三、使用 GDB 处理(比较复杂)
还有种更复杂的方法,使用 GDB 附加到进程,然后执行 waitpid() 来结束进程:
#!/bin/sh set -e ######################################## #### 创建 GDB 批处理 ######################################## echo "############## Begin of the Script" > /tmp/gdb_batch.txt last_parent_pid=0 ps -e -o ppid,pid,stat,command | grep -F "Z+" | sort | while read LINE do parent_pid=$(echo $LINE | awk '{print $1}') zombie_pid=$(echo $LINE | awk '{print $2}') process_stat=$(echo $LINE | awk '{print $3}') # 检查 ps 输出,确保为僵尸进程 [ "$process_stat" != "Z+" ] \ && echo "$zombie_pid Not Zombie" && continue # 如果当前父进程与前个父进程不同,则应该 detach 前个父进程(在第一次循环时,不应该执行) [ "$parent_pid" != "$last_parent_pid" ] && [ "$last_parent_pid" != "0" ] && \ echo "detach" >> /tmp/gdb_batch.txt # attach 到父进程 [ "$parent_pid" != "$last_parent_pid" ] && \ echo "attach $parent_pid" >> /tmp/gdb_batch.txt # 调用 watipid 方法 echo "call waitpid ($zombie_pid,0,0)" >> /tmp/gdb_batch.txt # 记录当前已经 attach 的父进程 last_parent_pid=$parent_pid done [ "$last_parent_pid" != "0" ] && \ echo "detach" >> /tmp/gdb_batch.txt echo "############## End of the Script" >> /tmp/gdb_batch.txt ######################################## #### 查看 GDB 脚本 ######################################## cat /tmp/gdb_batch.txt ######################################## #### 执行 GDB 脚本 ######################################## echo "[ENTER] to continue, [CTRL+C] to quit." read pause gdb -batch -x /tmp/gdb_batch.txt rm -rvf /tmp/gdb_batch.txt
对于在 Docker Container 中产生的僵尸进程,该方法是无效的(因为符号表、库哭经不同等等原因,所以 GDB 会失败。)。如果想要通过 GDB 处理容器产生的僵尸进程,需要最开始时以 –cap-add=SYS_PTRACE 运行容器,并在容器中运行 GDB 进程。
注意事项
内建于 Shell 的 kill 命令
在 Shell 中(比如 BASH),它可能内建 kill 命令。可以使用 type -a kill 命令进行查看:
# type -a kill kill is a shell builtin kill is /bin/kill
如果输出的第一行为”kill is a shell builtin“,这就表示你所执行的kill命令Shell内建的kill命令,而不是util-linux包中的kill(1)命令。而Shell内建kill命令的用法需要查看你所使用的Shell的文档,这里不再展开说明。如果你要使用util-linux软件包中的kill(1)命令,可以使用“绝对路径”(/bin/kill …)或者“env”(env kill …)来执行。还有一点要注意procp-ng软件包也提供了kill命令,所以到底使用的哪个包里的kill命令呢?这个需要你自己去判断了,但是看手册肯定不会错的。这些kill命令的用法大同小异,因此也没有什么可以担心的。
关于-KILL(-9)信号
如果关心数据完整性,请勿使用 -KILL(-9)信号,它会直接杀死进程。所有理智的进程都应该会处理 -TERM(-15)信号,通知进程结束自身。
参考文献
孤儿进程与僵尸进程(已经不是第一次参考这篇文章了)
Ways to kill parent and child processes in one command
Disabling column names in ps output
glibc – Unable to use standard library debugging symbols in gdb – Stack Overflow
c++ – about GDB and CRC mismatch – Stack Overflow
Does gdb support comments in the command line (gdb prompt)? – Stack Overflow
bash – How to represent multiple conditions in a shell if statement? – Stack Overflow
Zombie process cannot be removed or killed – Unix & Linux Stack Exchange
c – zombie process can’t be killed – Stack Overflow