在Linux中递归杀死带有子进程的R进程

6
我正在寻找一种通用方法来启动并终止一个R进程,可能包括其调用的所有分支或其他进程。
例如,用户运行以下脚本:
library(multicore);
for(i in 1:3) parallel(foo <- "bar");
for(i in 1:3) system("sleep 300", wait=FALSE);
for(i in 1:3) system("sleep 300&");
q("no")

用户退出 R 会话后,子进程仍在运行:
jeroen@jeroen-ubuntu:~$ ps -ef | grep R
jeroen    4469     1  0 16:38 pts/1    00:00:00 /usr/lib/R/bin/exec/R
jeroen    4470     1  0 16:38 pts/1    00:00:00 /usr/lib/R/bin/exec/R
jeroen    4471     1  0 16:38 pts/1    00:00:00 /usr/lib/R/bin/exec/R
jeroen    4502  4195  0 16:39 pts/1    00:00:00 grep --color=auto R
jeroen@jeroen-ubuntu:~$ ps -ef | grep "sleep"
jeroen    4473     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4475     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4477     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4479     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4481     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4483     1  0 16:38 pts/1    00:00:00 sleep 300
jeroen    4504  4195  0 16:39 pts/1    00:00:00 grep --color=auto sleep

更糟糕的是,它们的父进程ID为1,这使得难以识别它们。有没有一种方法可以以递归方式运行R脚本,允许我随时终止该进程及其子进程?
编辑:因此,我不想手动搜索和杀死进程。我也不想杀死所有的R进程,因为可能还有其他正常运行的进程。我需要一种方法来杀死特定进程及其所有子进程。

不要杀死PID 1 - 它不会做你想要的事情。嗯,实际上它会...有点... - thkala
哈哈,@thkala,它将做更多的事情,超出他的预期 :) 为什么不直接删除这个二进制文件呢? - Sergey Benner
3个回答

8
这主要涉及到多核部分。孩子们正在等待您收集结果-请参见?collect。通常,您不应该在没有清理措施的情况下使用parallel,通常是在on.exit中。 multicore在高级函数(如mclapply)中进行了清理,但如果您使用较低级别的函数,则需要负责执行清理(因为多核无法知道您是否有意让孩子们继续运行)。
您的示例实际上很虚假,因为您甚至没有考虑收集结果。但无论如何,如果这确实是您想要的,那么您必须在某个时候进行清理。例如,如果您想在退出时终止所有子进程,则可以这样定义.Last
 .Last <- function(...) {
     collect(wait=FALSE)
     all <- children()
     if (length(all)) {
         kill(all, SIGTERM)
         collect(all)
     }
 }

以上不是推荐的处理方式,而是一种最后的手段。你应该像这样分配任务并收集结果:

jobs <- lapply(1:3, function(i) parallel({Sys.sleep(i); i}))
collect(jobs)

关于一般的子进程问题,init只有在R退出后才继承子进程,但是在.Last中,您仍然可以找到它们的pid,因为父进程在那时仍然存在,所以您可以执行类似于multicore案例中的清理操作。


谢谢。问题在于用户有时会(我想是无意地)在我的服务器上留下混乱。我试图通过限制权限和尽可能地在他们离开后进行清理来将其限制在沙箱中。 - Jeroen Ooms
+1 这是很好的建议,也有助于扩展文档。并不是说 multicore 的文档不好,亲爱的 multicore 作者,但更多的示例和建议可以更容易地掌握 forkcollect - Iterator
@Jeroen 那很公平。不幸的是,在 R 中进行清理是自愿的。但是,您可以编写一个小的 C 函数,并使用 atexit 注册它,以在所有情况下强制进行清理(除了崩溃 - 只有信号处理程序才能帮助解决这些问题)。 - Simon Urbanek
@Iterator 嗯,你不应该使用 fork - 这甚至有一个警告 ;). 大多数用户应该对 mclapply 感到满意。但这也很公平 - 我可以在 parallel/collect 页面上添加额外的关于子进程寿命的警告。 - Simon Urbanek
我认为目前我能想到的最好的方法是 .Last <- function() system("pkill -P 1 R"),它将杀死所有R孤儿进程... - Jeroen Ooms

4
在用户退出R会话之前,您想要关闭的进程将具有与启动它们的会话的进程ID相等的父进程ID。 您可以使用.Last.Last.sys钩子(请参见help(q))在此时杀死所有具有适当PPID的进程;这些可以通过q(runLast = FALSE)抑制,因此它并不完美,但我认为这是您拥有的最佳选项。
在用户退出R会话后,没有可靠的方法来完成您想要的操作-内核保留的进程亲缘关系的唯一记录是您在ps -ef中看到的PPID,当父进程退出时,该信息被销毁,正如您已经发现的那样。
请注意,如果其中一个子进程分叉,则孙子进程将具有与子进程的PID相等的PPID,并且当子进程退出时,它将重置为1,而孙父进程可能会在其之前退出。 因此,即使在进程退出之前进行了捕获,通常也没有可靠的方法来捕获进程的所有后代。(听说“cgroups”提供了一种方法,但对细节不熟悉;无论如何,这是一种可选功能,只有Linux内核的某些迭代/配置才提供,并且在其他地方根本不可用。)

1
您可能还想查看 ps aux --forest,它提供与 PPID 相同的信息(必须在父进程死亡之前完成),并以更图形化的方式呈现。如果您有多个进程代际,这将非常有用。 - Vincent Zoonekynd
1
不幸的是,即使父进程尚未死亡,使用system()命令启动的子进程也将具有父ID 1。 - Jeroen Ooms
@Jeroen 不一定:例如 system("sleep 300") 不会,但是 system("sleep 300 &") 会。然而,根据命令的复杂程度以及所使用的操作系统、C库和 /bin/sh 版本,可能会有一个中间的 sh 进程挂起并混淆问题。 - zwol

1

我认为问题的后半部分更多地考虑了外壳而不是内核。(Simon Urbanek对于multicore部分的回答比其他任何人都好,因为他是作者。)

如果你在使用bash,你可以在$!中找到最近启动的子进程的PID。你可以收集这些PID,并在关闭R时确保将它们杀死。

如果你想做得更加出色,你可以将父进程的PID(即Sys.getpid()的输出)和子进程的PID存储在一个文件中,并拥有一个用来检查父进程PID是否存在的清理守护进程,如果不存在,则杀死孤儿进程。不过,我认为很难将一个名为oRphanKilleR的软件包放入CRAN。

下面是一个将子进程的PID追加到文件中的示例:

system('(sleep 20) & echo $! >> ~/childPIDs.txt', wait = FALSE)

您可以修改此代码以创建自己的shell命令,并使用R的tempfile()命令创建临时文件(尽管,当R实例终止时,该文件将消失,除非您通过权限采取特殊措施来保留该文件)。

有关其他聪明的想法,请参见SO上的另一篇文章

您还可以在shell中创建一个do while循环,以检查特定PID是否存在。只要存在,循环就会休眠。一旦循环终止(因为PID不再使用),脚本将杀死另一个PID。

基本上,我认为您的解决方案将在shell脚本中,而不是R中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接