「Linux」- 如何结束进程?

问题描述

起初是为了 systemd 的 service 单元文件中的 ExecStop 指令才整理的这篇文章,后来看 systemd 的文档说执行 stop 时,执行完 ExecStop 指令后,未结束的进程会由 systemd 来结束。

本来没有什么可写的,直接使用 kill(1) 命令来结束进程就可以了。但是,由几个有意思的问题:
1)如何结束一个进程的全部子进程?
2)如何结束一个进程及其子进程?
3)我想结束某个组或某个用户的进程该怎么做?

通常结束一个进程的时候,它的子进程不一定会退出,子进程可能会变成“孤儿进程”:

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

所以有的时候,我们就需要结束某个进程以及它的子进程。

我们先从基础的开始吧。

根据进程号,结束进程

在 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