Python:如何在父进程终止时杀死子进程?

52 投票
5 回答
32338 浏览
提问于 2025-04-18 05:10

子进程是通过以下方式启动的:

subprocess.Popen(arg)

有没有办法确保当父进程异常终止时,子进程也会被杀掉?我需要这个在Windows和Linux上都能工作。我知道有一个适用于Linux的解决方案

编辑:

如果有其他方法可以启动进程,启动子进程的要求可以放宽,不一定非要用subprocess.Popen(arg)

5 个回答

0

如果你写的子程序是用Python做的,一个简单的方法就是定期检查一下父程序是否已经退出:

import os, sys, asyncio, psutil

async def check_orphaned():
        parent = psutil.Process(os.getppid())
        while True:
            if not parent.is_running():
                sys.exit()
            await asyncio.sleep(2.5)

# check if orphaned in the background
orphan_listener_task = asyncio.create_task(check_orphaned()))

这个方法比设置操作系统特定的父子关系要简单得多,我觉得在大多数情况下都能满足需求。

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: deleting child processes in 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://stackoverflow.com/a/23436111/396373 有点相似,但我觉得我想出的这个方法对我来说更简单,因为要清理的进程是由父进程直接创建的。另外,虽然不需要一直检查父进程的状态,但在终止过程中,如果你想像这个例子一样,先尝试终止、监控,然后如果终止不成功再杀掉它,还是需要使用 psutil 来检查目标子进程的状态。

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命令)也会被结束。

45

嘿,我昨天也在研究这个问题!假设你不能修改子程序:

在Linux上,prctl(PR_SET_PDEATHSIG, ...) 可能是唯一可靠的选择。(如果你一定要杀掉子进程,建议把死亡信号设置为SIGKILL,而不是SIGTERM;你链接的代码使用的是SIGTERM,但子进程可以选择忽略SIGTERM。)

在Windows上,最可靠的选择是使用作业对象。这个方法的思路是,你创建一个“作业”(类似于进程的容器),然后把子进程放进这个作业里,并设置一个特殊的选项,意思是“当没有人持有这个作业的‘句柄’时,就杀掉里面的进程”。默认情况下,只有父进程持有这个作业的句柄,当父进程死掉时,操作系统会关闭所有它的句柄,然后发现这个作业没有打开的句柄了。于是,它就会按照要求杀掉子进程。(如果你有多个子进程,可以把它们都放到同一个作业里。)这个回答提供了使用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外壳启动的(也就是点击图标),那么可能已经有一个作业对象被分配,尝试创建新的作业对象会失败。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)

这样可以避免我提到的作业对象可能出现的任何问题。

如果你想确保万无一失,可以把这些解决方案结合起来。

希望这对你有帮助!

撰写回答