Axe*_*ton 3 bash process background-process
免责声明:这个问题出现的时间比预期的要长得多。我把它分成 5 个子问题。在打开之前,我真的很想弄清楚自己的想法,但是此刻太多的方面让我感到困惑。
为了阐明我对如何以可靠的方式正确处理 Bash 中的进程的想法,我偶然发现了这篇 Greg 的 Wiki 文章。在那里,而不是在开始时,有这样的声明
如果您仍处于启动您想要对其进行处理的子进程的父进程中,那就太完美了。您可以保证 PID 是您的子进程(死的或活的),原因如下所述。您可以使用
kill它来发出信号、终止它,或者只是检查它是否仍在运行。您可以使用wait等待它结束或在它结束时获取其退出代码。
在页面的末尾,找到了下面解释的上述原因。
每个 UNIX 进程也有一个父进程。此父进程是启动它的进程,但
init如果父进程在新进程结束之前结束,则可以更改为该进程。(也就是说,init将选择孤立进程。)理解这种父/子关系至关重要,因为它是 UNIX 中可靠进程管理的关键。进程wait终止后,进程的 PID 将永远不会被释放以供使用,直到父进程为 PID 查看它是否结束并检索其退出代码。如果父进程结束,进程将返回到init,它会为您执行此操作。这是一个主要的原因,重要的是:如果父进程管理其子进程,也可以是绝对肯定的是,即使孩子过程中的模具,没有其他新的进程可能会意外地回收子进程的PID,直到父进程已经
wait编为PID并注意到孩子死了。这让父进程保证它为子进程拥有的 PID 将始终指向该子进程,无论它是活着的还是“僵尸”。没有其他人有这种保证。不幸的是,这种保证不适用于 shell 脚本。Shell 积极地获取它们的子进程并将退出状态存储在内存中,在调用
wait. 但是因为孩子在调用之前就已经被收割了wait,所以没有僵尸来持有PID。内核可以自由地重用该 PID,而您的保证已被违反。
到目前为止,我已多次阅读以上段落,但我仍然不确定我是否正确理解了其背后的信息。
问题 1:从第二个长引用,特别是从它的最后一段,我会得出结论,在 shell(我只对 Bash 感兴趣)脚本中,我不能 100% 确定我存储在变量中的 PID 仍然指的是我启动了后台进程,因为它可能被内核重用于任何其他进程(甚至不是子进程)。这样对吗?上述保证适用于系统中的哪些地方?
问题2:似乎第二个引用的最后一段与第一个引用矛盾。一般来说,在 shell 脚本中,“如果您仍在启动子进程的父进程中 [...] 您可以保证 PID 是您的子进程(死的或活的)”,这是真的吗?
问题 3:我试图在网络上找到有关此主题的其他来源,但一如既往,很难区分真实和不准确的陈述。我得到了一些确认,但也有更多的疑问。参考this和this问题,似乎是在后台启动脚本进程的天真方法,将其PID存储在变量中,做一些事情,然后将PID结合使用wait以获取其退出代码或kill发送由于 PID 的内核重用,信号可能会失败。有通用的食谱吗?
问题 4:我还发现这条评论建议“让后台(进程)将返回代码存储在文件中,并让父级从文件中获取它”。这是可靠的方法吗?
问题 5:关于 的使用有wait -n什么注意事项吗?我想,如果我没有明确地将(可能重用)PID 给wait,那么应该不会发生任何错误。但是,在 Bash v4.4 中,似乎在启用作业控制的情况下的-n选项wait很有用,set -m. 在 Bash v5.0 中是否仍然如此?
额外问题: 此答案与 Greg 的 Wiki 类似。
只有一种情况可以安全地使用 pid 发送信号:当目标进程是将发送信号的进程的直接子进程,并且父进程尚未等待它时。
什么是直系孩子?和小孩子有区别吗?
...在 shell(我只对 Bash 感兴趣)脚本中,我不能 100% 确定我存储在变量中的 PID 仍然指的是我启动的后台进程,因为它可能被内核重用于任何其他进程。 ..
正确的。
shell 的编程方式是,一旦子进程死亡,shell 就会立即调用wait()它(将终止状态存储为其内部状态的一部分),这将释放 PID 以供另一个进程重用。
在 shell 脚本中,“如果您仍在启动子进程的父进程中 [...] 您可以保证 PID 是您的子进程(死的或活的)”,这是真的吗?
不,这不是真的。
因为,如前所述(以及引用中),shell 本身会立即收获子进程,这基本上破坏了这一保证。
在后台启动脚本中的进程,将其 PID 存储在变量中,执行一些操作,然后将 PID 与 wait 结合使用以获取其退出代码或与 kill 以发送信号的天真方式可能会由于重复使用而失败PID 的内核。
使用外壳,这是您能做的最好的事情。
请注意, usingwait并不是真正的问题,只是使用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,您将从该表中获取信息。
我认为如果wait在第一个实例上调用PID 之前,该 PID 被同一个 shell 的新后台子进程重用,则可能存在一个潜在问题,从那时起,该表中将发生冲突,最终会出现两个具有相同 PID 的单独后台进程。这是一个极端情况,我想这是非常非常罕见的,但可能是真实的。不太确定 shell 在这些情况下会做什么......它也可能取决于 shell 的实现并且可能因版本而异。
如前所述,这个问题的真正解决方案是维护关于 PID 存在的保证,当这些保证对您很重要时,使用 shell 以外的其他东西。
什么是直系孩子?和小孩子有区别吗?
这和孩子一样。
那是你自己分叉的孩子。
例如,如果您的子进程派生一个进程并将该进程的 PID 传递给您,则您不再保证它会一直存在。
既然收获这个过程是您孩子的工作,那么您的孩子就可以保证 PID 在他们收获之前不会被重用。不是你的。
当然,父进程可以与子进程协调以扩展该保证,例如通过在查询子进程是否仍然是它期望的 PID 时阻止它获取任何子进程,然后向它发送信号,或者可能通过询问孩子(有保证)代表父母发送信号。
希望这将有助于清除它。