免责声明:这个问题出现的时间比预想的要长得多。我把它分成 5 个子问题。在打开之前我确实试图理清自己的想法,但目前有太多的方面让我困惑。
试图澄清我的想法如何正确地在 Bash 中处理进程坚实的道路,我偶然发现这篇格雷格的维基文章。在那里,而不是在一开始,有这样的声明
如果您仍在启动您想要执行某些操作的子进程的父进程中,那就完美了。您可以保证 PID 是您的子进程(死的或活的),原因如下所述。您可以用来
kill
向它发出信号、终止它,或者只是检查它是否仍在运行。您可以用于wait
等待它结束或获取其退出代码(如果它已结束)。
在该页的末尾,上面提到的原因解释如下被发现。
每个 UNIX 进程还有一个父进程。该父进程是启动它的进程,但
init
如果父进程在新进程结束之前结束,则可以更改为该进程。 (即,init
将拾取孤立进程。)理解这种父/子关系至关重要,因为它是 UNIX 中可靠进程管理的关键。进程死亡后,进程的 PID 将永远不会被释放以供使用,直到父进程wait
获取 PID 以查看其是否结束并检索其退出代码。如果父进程结束,进程将返回到init
,它会为您执行此操作。这很重要,一个主要原因是:如果父进程管理其子进程,则可以绝对确定,即使子进程死亡,其他新进程也不会意外地回收子进程的 PID,直到父进程完成该操作
wait
。 PID并注意到孩子死了。这为父进程保证了子进程的 PID 将始终指向该子进程,无论它是活动的还是“僵尸”进程。其他人没有这个保证。很遗憾,此保证不适用于 shell 脚本。 Shell 积极地获取其子进程并将退出状态存储在内存中,您的脚本在调用
wait
.但因为孩子已经被收割了前你调用wait
,没有僵尸持有PID。内核可以自由地重用该 PID,并且您的保证已被违反。
我现在已经读了好几遍上面的段落了,但我仍然不确定我是否正确理解了其背后的信息。
问题一:从第二段长引文,尤其是最后一段,我得出结论,在 shell(我只对 Bash 感兴趣)脚本中,我不能 100% 确定我存储在变量中的 PID 仍然引用我启动的后台进程,因为它可能会被内核重用于任何其他进程(甚至不是子进程)。这是正确的吗?上述保证适用于系统中的哪个地方?
问题2:看来第二段引用的最后一段与第一段引用相矛盾。总体来说是这样吗在 shell 脚本中那“如果您仍在启动子进程的父进程中 [...] 您可以保证 PID 是您的子进程(死的或活的)”?
问题3:我试图在网络上找到有关该主题的其他来源,但一如既往,很难区分真实性和不准确的陈述。我得到了一些确认,但也有更多的疑问。参考这和这问题,似乎在后台启动脚本中的进程,将其 PID 存储在变量中,执行一些操作,然后结合使用 PID 来wait
获取其退出代码或kill
发送信号的天真的方法可能会失败,因为由内核重用PID。有通用的食谱吗?
问题4:我还发现这条评论这表明“让后台(进程)将返回代码存储在文件中,并让父进程从文件中获取它”。这是可靠的方法吗?
问题5:使用时有什么注意事项吗wait -n
?我想,如果我不明确地给 PID(可能会重用)wait
,那么应该不会发生任何错误。然而,它似乎在 Bash v4.4 中,-n
选项wait
在启用作业控制时很有用,set -m
. Bash v5.0 中还是这样吗?
奖金问题: 这个答案说的内容与格雷格的维基类似。
只有一种情况可以安全地使用 pid 发送信号:当目标进程是将发送信号的进程的直接子进程,并且父进程尚未等待它时。
什么是直接的孩子?和小孩子有什么不同吗?
答案1
...在 shell(我只对 Bash 感兴趣)脚本中,我无法 100% 确定存储在变量中的 PID 仍然引用我启动的后台进程,因为它可能被内核重用于任何其他进程。 ..
正确的。
按照 shell 的编程方式,一旦子进程死亡,shell 将wait()
立即调用它(将终止状态存储为其内部状态的一部分),这将释放 PID 以供另一个进程重用。
shell 脚本中是这样吗“如果您仍在启动子进程的父进程中 [...] 您可以保证 PID 是您的子进程(死的或活的)”?
不,这不是真的。
因为,正如前面提到的(以及在引用中),shell 本身将立即收获子进程,这基本上破坏了这种保证。
在后台启动脚本中的进程,将其 PID 存储在变量中,执行一些操作,然后将 PID 与 wait 结合使用以获取其退出代码或与 Kill 结合使用来发送信号,这种简单的方法可能会因重复使用而失败PID的内核。
使用 shell,这是您能做的最好的事情。
请注意,使用wait
并不是真正的问题,只是使用kill
,因为您的子进程可能已经死亡,PID已被重用,并且您正在杀死另一个进程。
wait
本身是在shell中实现的。当它获取子进程时,它将将该终止状态存储在内存中,因此它可以wait
使用该信息来实现其内置功能(以及等待仍在运行的子进程)。
另请注意,内核通常会尽力避免重用 PID,至少会尝试延迟重用 PID,正是因为在某些情况下无法保证 PID 不会被重用,因此内核会尝试尽量减少这种情况,其中信号将被传递到错误的进程。
有通用的食谱吗?
为了可靠性?
是的,用 C 或 Python、Perl、Ruby 等实现启动后台进程的代码。而不是在 shell 中。
这些不会有这个问题,因为默认情况下它们不会像 shell 那样获得子级,因此您必须在那里显式地执行此操作。
或者考虑使用系统管理器(例如 systemd)启动后台进程。
“让后台(进程)将返回代码存储在文件中,并让父进程从文件中获取它”。这是可靠的方法吗?
或许。
你很难保证那里没有受到干扰。很难找到一个只有该进程可以写入而其他进程不能写入的位置。
对于调用来说情况并非如此wait
,内核确保它不能被不同的进程伪造。
此外,该wait
调用还可以告诉您进程被杀死甚至崩溃,在这种情况下,如果您依赖进程本身在文件中记录其返回状态,您可能会得到不完整的信息......
另外,PID 重用的主要问题是杀死 PID,通过 获取返回码确实没有问题,并且使用文件存储返回码并不能真正解决wait
问题。kill
使用时是否有注意事项
wait -n
?
并不真地。wait
是可靠的,并且 AFAICT 不受 PID 重用的影响,因为当 shell 获取子进程时,它将保留该信息,包括正在使用的 PID 和返回代码,作为其内部状态的一部分。
当您致电 时wait
,您将从该表中获取信息。
我认为如果 PID 被同一个 shell 的新后台子进程重用,wait
在第一个实例被调用之前,可能会出现一个潜在的问题,从那时起,该表中将会发生冲突,最终会得到两个具有相同 PID 的独立后台进程。这是一个极端的案例,我想这种情况非常罕见,但可能是真实的。不太确定 shell 在这些情况下会做什么...它也可能取决于 shell 的实现,并且可能因版本而异。
不过,如前所述,此问题的真正解决方案是维持有关 PID 的保证,当这些保证对您很重要时,可以使用 shell 以外的其他东西。
什么是直接的孩子?和小孩子有什么不同吗?
就跟小孩子一样。
这是你自己分叉的孩子。
例如,如果您的子进程派生了一个进程并向您传递了该进程的 PID,您将不再保证它会保留下来。
既然收获这个过程是你孩子的工作,那么你的孩子就可以保证在他们收获它之前 PID 不会被重用。不是你的。
当然,父进程可以与子进程协调以扩展该保证,例如,在查询子进程 PID 是否仍然是它期望的进程时阻止它收割任何子进程,然后向其发送信号,或者可能通过询问孩子们(有保证的人)代表父母发送信号。
希望这将有助于解决这个问题。