在脚本终止时终止后台进程的可靠技术是什么?

在脚本终止时终止后台进程的可靠技术是什么?

我使用 shell 脚本来响应系统事件并更新窗口管理器中的状态显示。例如,一个脚本通过监听多个源来确定当前的 wifi 状态:

  1. 与 wpa_supplicant 关联/取消关联事件
  2. 地址从 ip 更改(所以我知道 dhcpcd 何时分配了地址)
  3. 定时器进程(因此信号强度会时常更新)

为了实现多路复用,我最终产生了后台进程:

{ wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while sleep 30; do echo; done } |
while read line; do update_wifi_status; done &

即,设置是,只要任何事件源输出一行,我的 wifi 状态就会更新。整个管道在后台运行(最后的“&”),因为我还监视导致我的脚本终止的另一个事件源:

wait_for_termination
kill $!

kill 应该清理后台进程,但这种形式并不能完全完成任务。至少“wpa_cli”和“ip”进程始终存活,也不会在下一次事件发生时死亡(理论上它们应该收到 SIGPIPE;我猜读进程也一定还活着)。

问题是,如何可靠地(并且优雅地)清理所有产生的后台进程?

答案1

超级简单的解决方案是在脚本末尾添加以下内容:

kill -- -$$

解释:

$$为我们提供正在运行的 shell 的 PID。因此,kill $$将向 shell 进程发送 SIGTERM。但是,如果我们否定PID,kill发送 SIGTERM 到进程组中的每个进程。我们需要--事先kill知道这-$$是一个进程组 ID,而不是一个标志。

请注意,这依赖于正在运行的 shell 作为进程组领导者!否则,$$(PID)将与进程组ID不匹配,最终您会向不知道哪里发送信号(好吧,可能无处可去,因为如果我们不是组长,就不太可能有具有匹配ID的进程组)。

当 shell 启动时,它会创建一个新的进程组[1]。每个分叉的进程都将成为该进程组的成员,除非它们通过系统调用 ( setpgid) 明确更改其进程组。

确保特定脚本作为进程组领导者运行的最简单方法是使用 启动它setsid。例如,我有几个从父脚本启动的状态脚本:

#!/bin/sh
wifi_status &
bat_status &

按照这样的写法,wifi 和电池脚本都与父脚本使用相同的进程组运行,并且kill -- -$$不起作用。解决方法是:

#!/bin/sh
setsid wifi_status &
setsid bat_status &

我发现pstree -p -g可视化进程和进程组 ID 很有用。

感谢每一位做出贡献并让我深入挖掘的人,我学到了很多东西!:)

[1] 还有其他情况下 shell 会创建进程组吗?例如,启动子 shell 时?我不知道……

答案2

好的,我想出了一个相当不错的解决方案,它不使用 cgroups。正如 Leonardo Dagnino 指出的那样,它在分叉进程的情况下不起作用。

通过 $! 手动跟踪进程 ID 以便稍后终止它们的问题之一是固有的竞争条件 - 如果进程在您终止它之前完成,脚本将向不存在或可能不正确的进程发送信号。

我们可以通过以下方式检查 shell 中的进程终止情况:等待内置,但我们只能等待任一终止全部后台进程,或者单个 pid。在这两种情况下等待块,这使得它不适合检查给定 PID 是否仍在运行的任务。

在寻找上述问题的解决方案时,我偶然发现了工作命令,我以前以为它只适用于交互式 shell。事实证明,它在脚本中运行良好,并自动跟踪我们启动的后台进程 - 如果进程已终止,它将不再显示在作业列表中。

因此,命令:

trap 'kill $(jobs -p)' EXIT

足以确保——在简单情况下——当当前 shell 退出时终止后台进程。

在我的情况下,一个是不够的,因为我也从子 shell 启动后台进程,并且每个新的子 shell 都会清除陷阱。因此,我需要在子 shell 中执行相同的陷阱:

{ trap 'kill $(jobs -p)' EXIT
wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while echo; do sleep 30; done } |
while read line; do update_wifi_status; done &

最后,作业-p只提供管道中最后一个进程的 pid(就像 $! 一样)。如你所见,我在第一的后台管道的进程,所以我想也向该 pid 发出信号。

第一个进程的 pid 可以从工作,但我不确定这可以多么方便地实现。使用 bash,我得到以下样式的输出:

$ sleep 20 | sleep 20 &
$ jobs -l
[1]+ 25180 Running                 sleep 20
     25181                       | sleep 20 &

因此,通过使用父脚本中稍微修改后的 kill 命令,我可以向管道中的所有进程发出信号:

wait_for_termination
kill $(jobs -l |awk '$2 == "|" {print $1; next} {print $2}')

答案3

你可以通过输入类似以下内容来获取每个进程的 PID

PID1=$!

在 wpa_cli 之后,

PID2=$!

在 ip 之后,在脚本的最后你杀死它们两个:

kill $PID1
kill $PID2

但是,如果进程分叉,这种方法就行不通了。那么 cgroups 就是最好的解决方案。

相关内容