杀死父节点同时删除子节点

16
我有一个程序生成和与我的代码无关的CPU密集型、不稳定的进程通信。 如果我的应用程序崩溃或被SIGKILL杀死,我希望子进程也能被杀死,这样用户就不必手动跟踪并杀死它们。
我知道这个主题以前已经讨论过,但我已经尝试了所有描述的方法,但似乎都不能通过测试。
我知道这一定是可能的,因为终端会一直做到这一点。如果我在终端中运行某些内容,并杀死终端,那些进程总是会死掉。
我尝试过atexit,双重fork和ptysatexit对于sigkill无效; 双重fork根本不起作用;而且我没有找到使用Python与ptys 配合工作的方法。
今天,我了解到prctl(PR_SET_PDEATHSIG,SIGKILL),这应该是子进程在其父进程死亡时发出自杀指令的方法。 我尝试使用popen,但似乎根本没有效果:
import ctypes, subprocess
libc = ctypes.CDLL('/lib/libc.so.6')
PR_SET_PDEATHSIG = 1; TERM = 15
implant_bomb = lambda: libc.prctl(PR_SET_PDEATHSIG, TERM)
subprocess.Popen(['gnuchess'], preexec_fn=implant_bomb)
在上述代码中,子进程被创建并且父进程已退出。现在你期望 gnuchess 接收到一个 SIGKILL 信号并终止,但它并没有。我仍然可以在我的进程管理器中找到它,使用了100%的CPU。请问是否有人能告诉我我对prctl的使用是否有问题?或者你知道终端是如何杀死它们的子进程的吗?

28
我觉得这个标题非常令人不安 :( - Dalbir Singh
6
应该在moms4moms.com的stackexchange网站上。 - Antony
3
实际上,我发现你的方法似乎完全可行 - 也许只是因为你的测试程序(gnuchess)实际上进行了一些双重分叉或类似的操作? 我使用了一个简单的Python脚本进行测试 - 可以看下面的“答案”中我使用的确切测试代码。(抱歉,评论中不能包含代码块,否则我会将代码放在评论里的...) - Paul Molodowitch
8个回答

14

虽然已经过去了几年,但我找到了一个简单(有点巧妙)的解决方案来解决这个问题。从您的父进程中,将所有调用都包装在一个非常简单的C程序中,该程序调用prctl(),然后执行exec()在Linux上解决了这个问题。我称之为“yeshup”:

#include <linux/prctl.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char **argv) {
     if(argc < 2)
          return 1;
     prctl(PR_SET_PDEATHSIG, SIGHUP, 0, 0, 0);
     return execvp(argv[1], &argv[1]);
}

在Python(或其他任何语言)中产生子进程时,您可以运行“yeshup gnuchess [argments]”。如果父进程被杀死,您会发现所有的子进程(应该)都会得到良好的SIGHUP信号。

这起作用是因为Linux将尊重对prctl的调用(不清除它),即使在调用execvp之后(它有效地将yeshup进程转换为gnuchess进程或您在那里指定的任何命令)也是如此,这与fork()不同。


这个能在基于Bash的测试用例中运行吗?我一直在试验但没有成功。 - Rhys Ulerich
1
但是这与调用 fork(); prctl(); exec() 有什么不同呢? - Thomas Ahle
Thomas:那可能行,但请记住,OP试图从Python调用子进程。 - coolbho3k
1
@coolbho3k 我知道,但是Python中的preexecfn与在fork和exec之间插入调用是完全相同的(?)。这相当于这个解决方案。 - Thomas Ahle
@coolbho3k - 我认为这是最好的解决方案 - 另外,关于在 prctl() 后添加 if (getppid() == 1) return 1; 作为检查的想法 - 这样,如果父进程在 prctl() 之前死亡并且 init 接管了孤儿进程作为父进程,你不必等待 init 死亡(你可能要等很长时间 :) - gnr

6

prctlPR_SET_DEATHSIG只能为当前调用prctl的进程设置,不能为其他进程设置,包括该进程的子进程。这个我所指向的man页面表达的方式是“该值在fork()时被清除”--当然,在Linux和任何其他类Unix OS中,fork是其他进程生成的方式。

如果你无法控制想要在子进程中运行的代码(正如你的gnuchess示例一样),我建议你首先生成一个单独的小型“监视器”进程,其角色是跟踪所有兄弟进程(当父进程生成它们时,父进程可以让监视器知道这些兄弟进程的pid),并在共同的父进程死亡时向它们发送终止信号(监视器需要轮询,每N秒唤醒一次,其中N是您选择的某个数,以检查父进程是否仍然存在;使用带有超时时间为N秒的select在循环内等待来自父进程的更多信息)。

这并不是件容易的事情,但这种系统任务通常也不是。终端通过“进程组的控制终端”概念以不同的方式实现,但是对于任何子进程来说,阻止它也是微不足道的(双重fork,nohup等)。


5
preexec_fn 会在 fork() 之后被调用,而手册页面没有说明这个标志在 exec() 上清除。 - Denis Otkidach
正如Denis所说,我有这样的印象,即preexec_fn参数将把我的prctl调用放置在fork和exec调用之间。监视器的想法实际上相当不错。当然,除非它崩溃了,因此它必须非常简单。 当收到SIGHUP信号时,我可以知道它的父进程是否已经死亡?然后让它终止兄弟姐妹们。您能告诉我更多关于生成控制终端的内容吗? - Thomas Ahle
[Python]: Popen Constructor(可能已经改变):"如果将 preexec_fn 设置为可调用对象,则该对象将在子进程执行之前在子进程中被调用。(仅适用于 POSIX)。" - CristiFati

3

实际上,我发现你最初的方法对我来说完全有效 - 这是我测试过的确切示例代码,可以正常工作:

echoer.py

#!/bin/env python

import time
import sys
i = 0
try:
    while True:
        i += 1
        print i
        time.sleep(1)
except KeyboardInterrupt:
    print "\nechoer caught KeyboardInterrupt"
    exit(0)

parentProc.py

#!/bin/env python

import ctypes
import subprocess
import time

libc = ctypes.CDLL('/lib64/libc.so.6')
PR_SET_PDEATHSIG = 1
SIGINT = 2
SIGTERM = 15

def set_death_signal(signal):
    libc.prctl(PR_SET_PDEATHSIG, signal)

def set_death_signal_int():
    set_death_signal(SIGINT)

def set_death_signal_term():
    set_death_signal(SIGTERM)

#subprocess.Popen(['./echoer.py'], preexec_fn=set_death_signal_term)
subprocess.Popen(['./echoer.py'], preexec_fn=set_death_signal_int)
time.sleep(1.5)
print "parentProc exiting..."

不是“更好”,只是不同-为了看到差异(在Linux系统上),请在同一目录中创建上述两个文件,使它们可执行,然后运行“parentProc.py”。子进程应该会收到一个KeyboardInterrupt信号,你会知道这一点,因为它会打印“echoer caught KeyboardInterrupt”。如果你将parentProc.py更改为使用set_death_signal_term,则echoer仍将被杀死,但方式更加突然。 - Paul Molodowitch

1

我在想,即使你在fork之后(在exec之前)设置了PR_SET_PDEATHSIG标志,它是否被清除了,因此从文档中看起来,它不应该被清除。

为了测试这个理论,您可以尝试以下操作:使用相同的代码运行一个用C编写的子进程,基本上只调用prctl(PR_GET_PDEATHSIG, &result)并打印结果。

您可以尝试的另一件事:在调用prctl时为arg3、arg4和arg5添加显式零。例如:

>>> implant_bomb = lambda: libc.prctl(PR_SET_PDEATHSIG, TERM, 0, 0, 0)

1

我认为双重fork是为了从控制终端分离。我不确定你如何尝试使用它。

这是一个hack,但你可以调用“ps”并搜索要杀死的进程名称。


但是当我的控制进程死亡时,我该如何调用ps呢?这正是重点。我的应用程序的用户不会知道子进程没有死亡,并且只会感觉到计算机变得非常缓慢,直到重新启动。 - Thomas Ahle
你能否将正在创建的程序用你自己的可执行文件包装起来,然后在启动时将PID写入文件中? - BillMan
我可能可以找到一种在重新启动时清理东西的方法,但我不知道是否会重新启动。因此,我需要一种在我死亡时清理的方法。 - Thomas Ahle

1

我见过非常恶劣的"清理"方式,使用像 ps xuawww | grep myApp | awk '{ print $1}' | xargs -n1 kill -9 这样的东西。

如果打开了客户端进程,它可以捕获 SIG_PIPE 并死亡。有很多方法可以解决这个问题,但这实际上取决于很多因素。如果在子进程中添加一些 ping 代码(ping 到父进程),则可以确保在子进程死亡时发出 SIG_PIPE。如果它捕获到它,它应该会终止。为此要使其正常工作需要双向通信...或者始终阻止客户端作为通信发起者。如果您不想修改子进程,则忽略此内容。

假设您不希望 Python 解释器实际崩溃,您可以将每个 PID 添加到序列中,然后在退出时进行杀死。这对于退出甚至未捕获的异常都应该是安全的。Python 有用于进行清理的工具。

以下是一些更安全的“恶意”操作:将每个子 PID 追加到文件中,包括您的主进程(单独的文件)。使用文件锁定。构建一个看守进程,该进程查看您的主 PID 的 flock() 状态。如果它没有被锁定,请杀死您的子 PID 列表中的每个 PID。在启动时运行相同的代码。

更加狡猾的方法:像上面一样将PID写入文件,然后在子shell中调用您的应用程序:(./myMaster; ./killMyChildren)

如果你看了我的代码片段,你会发现我在新进程中运行gnuchess,所以我无法将它作为线程运行。Popen不使用fork吗? - Thomas Ahle

1

由于在执行execv后,子进程无法接收信号,因此需要考虑一些安全限制来调用setuid。这些限制的完整列表在此处

祝好运!
/Mohamed


有趣,所以“信号只有在父进程具有向子进程发送信号的足够特权时才会传递。通常,任何运行具有比其父进程更高特权的子进程都不会收到任何信号。”我从没注意到这一点。如果它以某种方式增加了权限,我该如何检测? - Thomas Ahle

1

其他答案提到了prctlPR_SET_DEATHSIG,但没有提到可以使用setpriv命令从命令行设置它的事实:

setpriv --pdeathsig HUP [command] &

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