子进程:在Windows中删除子进程

51
在Windows上,subprocess.Popen.terminate调用win32的TerminalProcess。 但是,我看到的行为是我尝试终止的进程的子进程仍在运行。为什么会这样?如何确保杀死进程启动的所有子进程?

以下是两个选项:1. 使用此exe作为子进程,为您杀死进程树:http://www.latenighthacking.com/projects/2002/kill/ 2. 使用ctypes将以下C代码转换为Python:https://dev59.com/questions/zXM_5IYBdhLWcg3w6HvT - Unknown
Python 的哪个版本和 Windows? - Piotr Dobrogost
8个回答

79

通过使用psutil

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)

me = os.getpid()
kill_proc_tree(me)

2
那段代码看起来只会杀死第一层子进程,而不是孙子进程等。如果你正在使用cmd.exe启动构建工具或.bat/.cmd文件,这可能会成为一个问题。除非get_children()也包括孙子进程? - Macke
1
没错。如果要包含孙子节点,你应该指定'recursive'选项,例如parent.get_children(recursive=True)。 - Giampaolo Rodolà
6
这是一个旧的答案,但它正是我正在寻找的——一种跨平台的杀死所有子进程的方法。谷歌上出现的其他答案声称这是不可能的。感谢psutil! - Gilead
1
请注意,Windows不维护进程树。parent.children(recursive=True)通过将父进程链接到子进程来动态构建树形结构,因此它无法找到孤立的进程(即如果父进程已死亡)。 - Eryk Sun
非常感谢您!这正是我正在寻找的。您是否同意让这个答案中的代码在开源许可条款下使用?BSD或MIT是理想的选择,因为它们与Numpy、Pandas、Scipy等兼容。 - naitsirhc
它并不总是有效。如果进程可以随机生成。 - andreykyz

47

使用带有 /T 标志的 taskkill 命令

p = subprocess.Popen(...)
<wait>
subprocess.call(['taskkill', '/F', '/T', '/PID', str(p.pid)])

taskkill命令的标志具有以下文档:

TASKKILL [/S system [/U username [/P [password]]]]
         { [/FI filter] [/PID processid | /IM imagename] } [/T] [/F]

/S    system           Specifies the remote system to connect to.
/U    [domain\]user    Specifies the user context under which the
                       command should execute.
/P    [password]       Specifies the password for the given user
                       context. Prompts for input if omitted.
/FI   filter           Applies a filter to select a set of tasks.
                       Allows "*" to be used. ex. imagename eq acme*
/PID  processid        Specifies the PID of the process to be terminated.
                       Use TaskList to get the PID.
/IM   imagename        Specifies the image name of the process
                       to be terminated. Wildcard '*' can be used
                       to specify all tasks or image names.
/T                     Terminates the specified process and any
                       child processes which were started by it.
/F                     Specifies to forcefully terminate the process(es).
/?                     Displays this help message.

或者使用comtypes和win32api遍历进程树:

def killsubprocesses(parent_pid):
    '''kill parent and all subprocess using COM/WMI and the win32api'''

    log = logging.getLogger('killprocesses')

    try:
        import comtypes.client
    except ImportError:
        log.debug("comtypes not present, not killing subprocesses")
        return

    logging.getLogger('comtypes').setLevel(logging.INFO)

    log.debug('Querying process tree...')

    # get pid and subprocess pids for all alive processes
    WMI = comtypes.client.CoGetObject('winmgmts:')
    processes = WMI.InstancesOf('Win32_Process')
    subprocess_pids = {} # parent pid -> list of child pids

    for process in processes:
        pid = process.Properties_('ProcessID').Value
        parent = process.Properties_('ParentProcessId').Value
        log.trace("process %i's parent is: %s" % (pid, parent))
        subprocess_pids.setdefault(parent, []).append(pid)
        subprocess_pids.setdefault(pid, [])

    # find which we need to kill
    log.debug('Determining subprocesses for pid %i...' % parent_pid)

    processes_to_kill = []
    parent_processes = [parent_pid]
    while parent_processes:
        current_pid = parent_processes.pop()
        subps = subprocess_pids[current_pid]
        log.debug("process %i children are: %s" % (current_pid, subps))
        parent_processes.extend(subps)
        processes_to_kill.extend(subps)

    # kill the subprocess tree
    if processes_to_kill:
        log.info('Process pid %i spawned %i subprocesses, terminating them...' % 
            (parent_pid, len(processes_to_kill)))
    else:
        log.debug('Process pid %i had no subprocesses.' % parent_pid)

    import ctypes
    kernel32 = ctypes.windll.kernel32
    for pid in processes_to_kill:
        hProcess = kernel32.OpenProcess(PROCESS_TERMINATE, FALSE, pid)
        if not hProcess:
            log.warning('Unable to open process pid %i for termination' % pid)
        else:
            log.debug('Terminating pid %i' % pid)                        
            kernel32.TerminateProcess(hProcess, 3)
            kernel32.CloseHandle(hProcess)

10

这是一个Job对象方法的示例代码,但是它使用win32api.CreateProcess而不是subprocess

import win32process
import win32job
startup = win32process.STARTUPINFO()
(hProcess, hThread, processId, threadId) = win32process.CreateProcess(None, command, None, None, True, win32process.CREATE_BREAKAWAY_FROM_JOB, None, None, startup)

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)
win32job.AssignProcessToJobObject(hJob, hProcess)

4
+1:它似乎是最强大的解决方案,即使父进程崩溃也能正常工作。这里是它如何工作的解释 - jfs

9
这是一件难事。Windows实际上并没有在进程空间中存储进程树。也无法终止一个进程并指定其子进程也应该退出。
解决这个问题的一种方法是使用taskkill,并要求它杀死整个进程树。
另一种方法(假设您正在生成顶级进程)是使用专为此类操作开发的模块:http://benjamin.smedbergs.us/blog/tag/killableprocess/ 要想通用地完成此操作,您需要花费一些时间从后往前建立列表。也就是说,进程存储指向其父进程的指针,但是父进程似乎不存储有关子进程的信息。
因此,您必须查看系统中的所有进程(这并不难),然后自己手动连接这些点,查看父进程字段。然后,选择您感兴趣的树并遍历整个树,逐个杀死每个节点。
请注意,当父进程终止时,Windows不会更新子进程的父指针,因此您的树中可能存在间隙。我不知道您能否对此做出任何更改。

6
将孩子们放在一个 NT 作业对象 中,然后你就可以杀死所有孩子。

3
“Job对象似乎是解决这个问题的正确方法,可惜它没有与子进程模块集成。” - Sridhar Ratnakumar

4

我曾经遇到过同样的问题,解决方法是通过Windows命令杀死进程,并使用子进程杀死选项“/T”。

def kill_command_windows(pid):
    '''Run command via subprocess'''
    dev_null = open(os.devnull, 'w')
    command = ['TASKKILL', '/F', '/T', '/PID', str(pid)]
    proc = subprocess.Popen(command, stdin=dev_null, stdout=sys.stdout, stderr=sys.stderr)

1
我试图终止使用subprocess.Popen()启动的Gradle构建进程。在Windows 7上,简单的process.terminate()process.kill()都不起作用,上面提到的psutils选项也不行,但是这个方法可以。 - Ken Pronovici

2
我使用了kevin-smyth的回答,创建了一个可以替代subprocess.Popen的插拔式替换方案,它将创建的子进程限制在一个匿名作业对象中,并设置为在关闭时终止:最初的回答。
# coding: utf-8

from subprocess import Popen
import subprocess
import win32job
import win32process
import win32api


class JobPopen(Popen):
    """Start a process in a new Win32 job object.

    This `subprocess.Popen` subclass takes the same arguments as Popen and
    behaves the same way. In addition to that, created processes will be
    assigned to a new anonymous Win32 job object on startup, which will
    guarantee that the processes will be terminated by the OS as soon as
    either the Popen object, job object handle or parent Python process are
    closed.
    """

    class _winapijobhandler(object):
        """Patches the native CreateProcess function in the subprocess module
        to assign created threads to the given job"""

        def __init__(self, oldapi, job):
            self._oldapi = oldapi
            self._job = job

        def __getattr__(self, key):
            if key != "CreateProcess":
                return getattr(self._oldapi, key)  # Any other function is run as before
            else:
                return self.CreateProcess  # CreateProcess will call the function below

        def CreateProcess(self, *args, **kwargs):
            hp, ht, pid, tid = self._oldapi.CreateProcess(*args, **kwargs)
            win32job.AssignProcessToJobObject(self._job, hp)
            win32process.ResumeThread(ht)
            return hp, ht, pid, tid

    def __init__(self, *args, **kwargs):
        """Start a new process using an anonymous job object. Takes the same arguments as Popen"""

        # Create a new job object
        self._win32_job = self._create_job_object()

        # Temporarily patch the subprocess creation logic to assign created
        # processes to the new job, then resume execution normally.
        CREATE_SUSPENDED = 0x00000004
        kwargs.setdefault("creationflags", 0)
        kwargs["creationflags"] |= CREATE_SUSPENDED
        try:
            _winapi = subprocess._winapi  # Python 3
            _winapi_key = "_winapi"
        except AttributeError:
            _winapi = subprocess._subprocess  # Python 2
            _winapi_key = "_subprocess"
        try:
            setattr(subprocess, _winapi_key, JobPopen._winapijobhandler(_winapi, self._win32_job))
            super(JobPopen, self).__init__(*args, **kwargs)
        finally:
            setattr(subprocess, _winapi_key, _winapi)

    def _create_job_object(self):
        """Create a new anonymous job object"""
        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)
        return hjob

    def _close_job_object(self, hjob):
        """Close the handle to a job object, terminating all processes inside it"""
        if self._win32_job:
            win32api.CloseHandle(self._win32_job)
            self._win32_job = None

    # This ensures that no remaining subprocesses are found when the process
    # exits from a `with JobPopen(...)` block.
    def __exit__(self, exc_type, value, traceback):
        super(JobPopen, self).__exit__(exc_type, value, traceback)
        self._close_job_object(self._win32_job)

    # Python does not keep a reference outside of the parent class when the
    # interpreter exits, which is why we keep it here.
    _Popen = subprocess.Popen  
    def __del__(self):
        self._Popen.__del__(self)
        self._close_job_object(self._win32_job)

这是我尝试了大约15个建议后唯一有效的解决方案。 使用JobPopen实例化一个子进程,像这样:with open(JobPopen(command, stdout=subprocess.PIPE, shell=True)) as p: # 在此处编写您的代码 # 然后,在脚本退出'with open'块时 - 它将关闭该进程。 - Elyasaf755

0

如果像我一样,你正在使用类似以下逻辑从vbs脚本中创建对象,则上述任何答案都不起作用:

Set oExcel = CreateObject("Excel.Application")

这是因为在Windows中,这会将Excel作为服务生成,并且它的所有者是svchost.exe而不是您的VBS脚本。感谢spokes提供的答案,在诊断此问题方面非常有帮助。

我相当粗略地处理了这个问题,基本上创建了一个Excel进程列表,然后启动它,在之后获取另一个Excel进程列表并进行比较,新PID成为我的新Excel脚本。然后,如果需要终止它,我通过其PID标识它并将其终止。

this_script_pid = 0

try:
    running_excel_pids = [pid for pid in psutil.pids() \
                          if psutil.Process(pid).name() == "EXCEL.EXE"] # record all instances of excel before this starts
    p = subprocess.Popen(<starts vbs script that starts excel>)
    
    time.sleep(0.05) # give the script time to execute
    for pid in [p for p in psutil.pids() if psutil.Process(p).name() == "EXCEL.EXE"]:
        if pid not in running_excel_pids:
            this_script_pid = pid
            break
    
    p.communicate() # required to make our python program wait for the process to end


except:
    p.terminate() # kill the parent script
    if this_script_pid != 0:
        print("killing individual script")
        psutil.Process(this_script_pid).kill()

    else: 
        for pid in [p for p in psutil.pids() if psutil.Process(p).name() == "EXCEL.EXE"]:
            if (pid not in running_excel_pids) and (psutil.Process(pid).parent().name()=="svchost.exe"):
                proc = psutil.Process(pid)
                proc.kill()
    exit()  # gracefully quit

请注意,上述仅适用于我的特定情况,即使我已经尝试尽可能地针对性强,它几乎肯定不应在多线程环境中使用。

0.05秒的等待是根据经验发现的。 0.01秒太短了,0.03秒可以工作,因此0.05秒看起来很安全。

除块中的else只是一个捕获所有异常的语句,以防它无法记录创建的PID,它将杀死自脚本开始以来启动为服务的所有Excel进程。

更简洁明了的答案可能是扩展Spokes链接的答案并从shell中运行excel,但我没有时间去解决这个问题。


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