Python:当父进程死亡时如何杀死子进程?

Posted

技术标签:

【中文标题】Python:当父进程死亡时如何杀死子进程?【英文标题】:Python: how to kill child process(es) when parent dies? 【发布时间】:2014-06-19 12:48:22 【问题描述】:

子进程启动于

subprocess.Popen(arg)

有没有办法确保它在父母异常终止时被杀死?我需要它才能在 Windows 和 Linux 上工作。我知道这个solution for Linux。

编辑:

如果使用不同的启动进程方法存在解决方案,则可以放宽使用subprocess.Popen(arg) 启动子进程的要求。

【问题讨论】:

这很模糊,你能提供更多细节吗?也许描述一下父进程和子进程是什么? the link you provided 的第一个解决方案也适用于 Windows。 @J.F.Sebastian: 当然,但是如果进程被sigkill 终止,第二个就可以了。 Windows 上没有 sigkill @J.F.Sebastian:让我换个说法。如果父进程因任何原因终止,子进程必须退出。第一种解决方案不保证。 【参考方案1】:

呵呵,我昨天才自己研究这个!假设您不能更改子程序:

在 Linux 上,prctl(PR_SET_PDEATHSIG, ...) 可能是唯一可靠的选择。 (如果绝对有必要杀死子进程,那么您可能希望将死亡信号设置为 SIGKILL 而不是 SIGTERM;您链接到的代码使用 SIGTERM,但如果子进程愿意,可以选择忽略 SIGTERM。 )

在 Windows 上,最可靠的选项是使用 Job object。这个想法是您创建一个“作业”(一种进程容器),然后将子进程放入作业中,并设置“当没有人持有此作业的‘句柄’时,然后杀死其中的进程”。默认情况下,作业的唯一“句柄”是您的父进程持有的那个,当父进程死亡时,操作系统将检查并关闭其所有句柄,然后注意这意味着没有打开的句柄工作。因此,它按照要求杀死了孩子。 (如果您有多个子进程,您可以将它们全部分配给同一个作业。)This answer 有执行此操作的示例代码,使用 win32api 模块。该代码使用CreateProcess 来启动子进程,而不是subprocess.Popen。原因是他们需要为生成的孩子获取一个“进程句柄”,CreateProcess 默认返回这个。如果您更愿意使用subprocess.Popen,那么这是该答案中代码的(未经测试的)副本,它使用subprocess.PopenOpenProcess 而不是CreateProcess

import subprocess
import win32api
import win32con
import win32job

hJob = win32job.CreateJobObject(None, "")
extended_info = win32job.QueryInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation)
extended_info['BasicLimitInformation']['LimitFlags'] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
win32job.SetInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation, extended_info)

child = subprocess.Popen(...)
# Convert process id to process handle:
perms = win32con.PROCESS_TERMINATE | win32con.PROCESS_SET_QUOTA
hProcess = win32api.OpenProcess(perms, False, child.pid)

win32job.AssignProcessToJobObject(hJob, hProcess)

从技术上讲,如果孩子在PopenOpenProcess 调用之间死亡,这里有一个很小的竞争条件,您可以决定是否要担心。

使用作业对象的一个​​缺点是,在 Vista 或 Win7 上运行时,如果您的程序是从 Windows shell 启动的(即,通过单击图标),那么可能会出现 already be a job object assigned 并尝试创建一个新作业对象将失败。 Win8 解决了这个问题(通过允许嵌套作业对象),或者如果你的程序是从命令行运行的,那么它应该没问题。

如果您可以修改孩子(例如,像使用multiprocessing 时),那么最好的选择可能是以某种方式将父母的 PID 传递给孩子(例如,作为命令行参数,或在args= 参数中multiprocessing.Process),然后:

在 POSIX 上:在子进程中生成一个线程,它偶尔会调用 os.getppid(),如果返回值不再匹配从父进程传入的 pid,则调用 os._exit()。 (这种方法适用于所有 Unix,包括 OS X,而 prctl 技巧是特定于 Linux 的。)

在 Windows 上:在使用 OpenProcessos.waitpid 的子进程中生成一个线程。使用 ctypes 的示例:

from ctypes import WinDLL, WinError
from ctypes.wintypes import DWORD, BOOL, HANDLE
# Magic value from http://msdn.microsoft.com/en-us/library/ms684880.aspx
SYNCHRONIZE = 0x00100000
kernel32 = WinDLL("kernel32.dll")
kernel32.OpenProcess.argtypes = (DWORD, BOOL, DWORD)
kernel32.OpenProcess.restype = HANDLE
parent_handle = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
# Block until parent exits
os.waitpid(parent_handle, 0)
os._exit(0)

这避免了我提到的作业对象的任何可能问题。

如果您想非常非常确定,那么您可以结合所有这些解决方案。

希望有帮助!

【讨论】:

避免您提到的竞争条件的一种方法是在启动子进程之前将自己添加到作业对象中;孩子将继承会员资格。另一种方法是启动暂停的子进程,只有在将其添加到作业后才能恢复它。 对于 Windows 7,shell 的作业对象允许分离,因此您可以使用创建标志 CREATE_BREAKAWAY_FROM_JOB 允许将进程添加到新作业。 我不明白,为什么谋杀子进程这么复杂? @CharlieParker 问题是关于如何处理父母异常终止的情况。如果父级出现段错误或被强制终止(例如kill -9),那么它就没有机会谋杀孩子。 这对很多人来说可能很明显,但要真正终止,需要在不再需要进程后添加win32job.TerminateJobObject(hJob, hProcess)【参考方案2】:

Popen 对象提供了终止和终止方法。

https://docs.python.org/2/library/subprocess.html#subprocess.Popen.terminate

这些会为您发送 SIGTERM 和 SIGKILL 信号。 您可以执行类似于以下的操作:

from subprocess import Popen

p = None
try:
    p = Popen(arg)
    # some code here
except Exception as ex:
    print 'Parent program has exited with the below error:\n0'.format(ex)
    if p:
        p.terminate()

更新:

你是对的——上面的代码不能防止硬崩溃或有人杀死你的进程。在这种情况下,您可以尝试将子进程包装在一个类中并使用轮询模型来监视父进程。 请注意 psutil 是非标准的。

import os
import psutil

from multiprocessing import Process
from time import sleep


class MyProcessAbstraction(object):
    def __init__(self, parent_pid, command):
        """
        @type parent_pid: int
        @type command: str
        """
        self._child = None
        self._cmd = command
        self._parent = psutil.Process(pid=parent_pid)

    def run_child(self):
        """
        Start a child process by running self._cmd. 
        Wait until the parent process (self._parent) has died, then kill the 
        child.
        """
        print '---- Running command: "%s" ----' % self._cmd
        self._child = psutil.Popen(self._cmd)
        try:
            while self._parent.status == psutil.STATUS_RUNNING:
                sleep(1)
        except psutil.NoSuchProcess:
            pass
        finally:
            print '---- Terminating child PID %s ----' % self._child.pid
            self._child.terminate()


if __name__ == "__main__":
    parent = os.getpid()
    child = MyProcessAbstraction(parent, 'ping -t localhost')
    child_proc = Process(target=child.run_child)
    child_proc.daemon = True
    child_proc.start()

    print '---- Try killing PID: %s ----' % parent
    while True:
        sleep(1)

在这个例子中,我运行 'ping -t localhost' b/c,它将永远运行。如果你杀死父进程,子进程(ping 命令)也将被杀死。

【讨论】:

这没有回答问题。如果父进程崩溃,谁会调用p.terminate()?我正在寻找一种在 Windows 上启动进程的方法,以便在其父进程因任何原因终止时退出。这可以在 Linux 上完成。 你是对的,它没有解决你的问题。希望上面的编辑能做到这一点。 整洁。并且看起来便携。我喜欢它。 据我所知,即使在编辑之后,这...根本不起作用。看起来你是在建议父母应该观察自己,看看它是否已经死了,如果是,那就杀死孩子。显然,这没有任何意义。也许您实际上的意思是应该将示例代码拆分为一个新程序,因此父进程会产生两个进程:子进程和一个监视父进程和子进程的看门狗。但这只是重现了最初的问题——如果看门狗异常终止会发生什么?谁在看守望者? 啊,我看到我错过了示例中的滚动条,它巧妙地剪掉了if __name__ == "__main__": 块。有了它就更有意义了!尽管如此,与使用操作系统级工具(在 Linux 和 Windows 上都可用)的更多解决方案相比,这种方法似乎不必要地复杂和不可靠,并为问题创造了新的机会——例如,当前编写的代码使parent 监视孩子的生活或获取退出代码,如果运行多个孩子,则会泄漏看门狗进程。

以上是关于Python:当父进程死亡时如何杀死子进程?的主要内容,如果未能解决你的问题,请参考以下文章

当父进程被杀死时,使用 fork() 创建的子进程是不是会自动被杀死?

当父进程在python中终止时如何避免进程终止

QProcess::startDetached shell 在父进程死亡时被杀死,如果父进程是一个 systemd 服务

当节点进程被杀死时杀死所有child_process

如何使用子进程模块杀死(或避免)僵尸进程

进程管理