Python:如何在父进程退出时杀死子进程?

49

子进程是使用以下方式启动的

subprocess.Popen(arg)

有没有一种方法可以确保当父进程异常终止时它被杀死?我需要在Windows和Linux上都能实现这个功能。我知道这个Linux的解决方案

编辑:

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


1
这个描述比较含糊,您能否提供更多细节?可以描述一下父进程和子进程是什么吗? - wnnmaw
1
提供的链接中的第一个解决方案在Windows上也可行。 - jfs
@J.F.Sebastian:当然,但如果进程被“sigkill”终止,则第二个可以工作。 - user443854
Windows上没有SIGKILL。 - jfs
对我来说,我收到了警告:“ResourceWarning:子进程40092仍在运行”。 - Charlie Parker
显示剩余2条评论
4个回答

43

嘿,我昨天也在研究这个问题!假设您无法更改子程序:

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

在Windows上,最可靠的选项是使用Job对象。其思想是创建一个“作业”(一种进程容器),然后将子进程放入作业中,并设置魔术选项,即“当没有人持有该作业的‘句柄’时,则杀死其中的进程”。默认情况下,对作业的唯一“句柄”是父进程持有的句柄,当父进程死亡时,操作系统将关闭它的所有句柄,然后注意到这意味着没有打开作业的句柄。因此,按要求杀死子进程。(如果您有多个子进程,可以将它们全部分配给同一个作业。)这个答案提供了使用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(例如通过单击图标)启动的,则可能会已经分配了作业对象并且尝试创建新的作业对象将失败。Win8可以解决这个问题(通过允许嵌套作业对象),或者如果您的程序是从命令行运行,则应该没问题。

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

在 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)

这样可以避免我提到的作业对象可能出现的任何问题。
如果你想确保得到最好的保障,那么可以将所有这些解决方案结合起来。
希望能有所帮助!

7
避免你提到的竞态条件的一种方法是在启动子进程之前将自己添加到作业对象中;子进程将继承成员身份。另一种方法是将子进程挂起启动,仅在将其添加到作业后恢复它。 - Harry Johnston
对于Windows 7,shell的作业对象允许分离,因此您可以使用创建标志“CREATE_BREAKAWAY_FROM_JOB”将进程添加到新作业中。 - Eryk Sun
我不明白,为什么杀死子进程这么复杂? - Charlie Parker
@CharlieParker 这个问题是关于如何处理父进程异常终止的情况。如果父进程出现段错误或被强制终止(例如使用 kill -9 命令),那么它就没有机会杀死子进程了。 - Nathaniel J. Smith
1
对许多人来说可能很明显,但实际上要终止进程,需要在进程不再需要后添加win32job.TerminateJobObject(hJob, hProcess) - DarkLight
显示剩余3条评论

11

Popen对象提供了terminate和kill方法。

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:\n{0}'.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' 命令,因为它会一直运行。如果你结束父进程,子进程 (ping 命令) 也会被结束。

4
这并没有回答问题。如果一个父进程崩溃了,谁会调用p.terminate()?我正在寻找在Windows上启动一个进程的方法,使它能够在父进程无论因何原因而终止时退出。这在Linux上是可行的。 - user443854
你说得对,它没有回答你的问题。希望上面的编辑能够解决这个问题。 - Nick
在这个例子中实际上创建了3个PID。P1(原始的Python解释器),P2(multiprocessing.Process)和P3(在P2中创建的Popen对象)。因为它们都是独立的PID,所以P2能够监视P1是否消失。 - Nick
2
啊,我看到我在示例中错过了滚动条,这个滚动条整齐地截断了if __name__ == "__main__":块。有了它,就更有意义了!尽管如此,与使用操作系统级工具(在Linux和Windows上都可用)相比,这种方法似乎过于复杂且不可靠,并且为问题创造了新的机会 - 例如,当前编写的代码使得父进程无法监控子进程的生命周期或获取退出码,并且如果运行多个子进程,将会泄漏看门狗进程。 - Nathaniel J. Smith
@NathanielJ.Smith:我尝试了两种方法,必须说我的初始反应是错误的。我太快地采用了这种方法,因为它看起来实现简单和可移植性强。此外,multiprocessing doc指出,“一个守护进程不允许创建子进程”。我也喜欢多个子进程可以链接到同一个作业对象的方式。我接受你的回答。 - user443854
显示剩余3条评论

0
使用SetConsoleCtrlHandler挂钩您的进程退出,并杀死子进程。我认为我有点过度杀伤,但它可以工作 :)
import psutil, os

def kill_proc_tree(pid, including_parent=True):
    parent = psutil.Process(pid)
    children = parent.children(recursive=True)
    for child in children:
        child.kill()
    gone, still_alive = psutil.wait_procs(children, timeout=5)
    if including_parent:
        parent.kill()
        parent.wait(5)

def func(x):
    print("killed")
    if anotherproc:
        kill_proc_tree(anotherproc.pid)
    kill_proc_tree(os.getpid())

import win32api,shlex
win32api.SetConsoleCtrlHandler(func, True)      

PROCESSTORUN="your process"
anotherproc=None
cmdline=f"/c start /wait \"{PROCESSTORUN}\" "
anotherproc=subprocess.Popen(executable='C:\\Windows\\system32\\cmd.EXE', args=shlex.split(cmdline,posix="false"))
...
run program
...

从以下链接中获取了kill_proc_tree函数: subprocess: 在Windows中删除子进程

0

据我所知,PR_SET_PDEATHSIG解决方案在父进程中运行任何线程时可能导致死锁,因此我不想使用它,而是找到了另一种方法。我创建了一个单独的自动终止进程,它会检测其父进程何时完成,并杀死作为其目标的其他子进程。

要实现这一点,您需要pip install psutil,然后编写类似以下代码的内容:

def start_auto_cleanup_subprocess(target_pid):
    cleanup_script = f"""
import os
import psutil
import signal
from time import sleep

try:                                                            
    # Block until stdin is closed which means the parent process
    # has terminated.                                           
    input()                                                     
except Exception:                                               
    # Should be an EOFError, but if any other exception happens,
    # assume we should respond in the same way.                 
    pass                                                        

if not psutil.pid_exists({target_pid}):              
    # Target process has already exited, so nothing to do.      
    exit()                                                      
                                                                
os.kill({target_pid}, signal.SIGTERM)                           
for count in range(10):                                         
    if not psutil.pid_exists({target_pid}):  
        # Target process no longer running.        
        exit()
    sleep(1)
                                                                
os.kill({target_pid}, signal.SIGKILL)                           
# Don't bother waiting to see if this works since if it doesn't,
# there is nothing else we can do.                              
"""

    return Popen(
        [
            sys.executable,  # Python executable
            '-c', cleanup_script
        ],
        stdin=subprocess.PIPE
    )

这与我之前没有注意到的https://dev59.com/F2Ag5IYBdhLWcg3wlLuh#23436111类似,但我认为我想出的方法更容易使用,因为需要清理的进程是直接由父进程创建的。还要注意的是,在终止序列期间,不必轮询父进程的状态,尽管如果您想尝试像在此示例中一样终止、监视,然后在终止未能迅速工作时杀死目标子进程,则仍需要使用psutil并轮询目标子进程的状态。


如果您想要在 ps 输出中轻松识别自动终止进程,可以安装 setproctitle 包。然后,自动终止脚本可以使用该包来设置其自己的进程名称。 - Steve Jorgensen

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