Bash:PID什么时候被释放?

Bash:PID什么时候被释放?

免责声明:这个问题出现的时间比预想的要长得多。我把它分成 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 是否仍然是它期望的进程时阻止它收割任何子进程,然后向其发送信号,或者可能通过询问孩子们(有保证的人)代表父母发送信号。

希望这将有助于解决这个问题。

相关内容