[译] 如何杀死一个进程和它的所有子进程


如何杀死一个进程和它的所有子进程

在类 Unix 系统中杀死进程比预期中更棘手。上周我在调试一个在 Semaphore 中终止作业的问题。更具体地说,这是一个有关于在作业中终止正在运行的进程的问题。以下是我从中学到的要点:

  • 类 Unix 操作系统有着复杂的进程间关系:父子进程、进程组、会话、会话的领导进程。但是,在 Linux 与 MacOS 等操作系统中,这其中的细节并不统一。符合 POSIX 的操作系统支持使用负 PID 向进程组发送信号。
  • 使用系统调用向会话中的所有进程发送信号并非易事。
  • 用 exec 启动的子进程将继承其父进程的信号配置。例如,如果父进程忽略 SIGHUP 信号,它的子进程也会忽略 SIGHUP 信号。
  • “孤儿进程组内发生了什么”这一问题的答案并不简单。

杀死父进程并不会同时杀死子进程

每个进程都有一个父进程。我们可以使用 pstree ps 工具来观察这一点。

# 启动两个虚拟进程
$ sleep 100 &
$ sleep 101 &
$ pstree -p
init(1)-+
        |-bash(29051)-+-pstree(29251)
                      |-sleep(28919)
                      `-sleep(28964)
$ ps j -A
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init
29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100
29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101
29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A
    1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash

调用 ps 命令可以显示 PID(进程 ID) 和 PPID(父进程 ID)。

我对父子进程间的关系有着错误的假设。我认为如果我杀死了父进程,那么也会杀死它的所有子进程。然而这是错误的。相反,子进程将会成为孤儿进程,而 init 进程将重新成为它们的父进程。

让我们看看通过终止 bash 进程(sleep 命令的当前父进程)来重建进程间的父子关系后发生了哪些变化。

$ kill 29051 # 杀死 bash 进程
$ pstree -A
init(1)-+
        |-sleep(28919)
        `-sleep(28965)

于我而言,重新分配父进程的行为很奇怪。例如,当我使用 SSH 登录一台服务器,启动一个进程,然后退出时,我启动的进程将会被终止。我错误地认为这是 Linux 上的默认行为。当我离开一个 SSH 会话时,进程的终止与进程组、会话的领导进程和控制终端都有关。

什么是进程组和会话领导进程?

让我们再次观察上述事例中 ps j 命令的输出。

$ ps j -A
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init
29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100
29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101
29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A
    1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash

除了使用 PPID 和 PID 表示的父子进程关系外,进程间还有其他两种关系:

  • 用 PGID 表示的进程组
  • 用 SID 表示的会话

我们可以在支持作业控制的 Shell 环境中观察到进程组,例如 bash zsh ,它们为每个管道命令都创建了一个进程组。进程组是一个或多个进程(通常与一个作业关联)的集合,可以从同一个终端接收信号。每个进程组都有一个唯一的进程组 ID。

# 启动一个由 tail 和 grep 命令组成的进程组
$ tail -f /var/log/syslog | grep "CRON" &
$ ps j
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
29051 19701 19701 29051 pts/2    19784 SN    1000   0:00 tail -f /var/log/syslog