确保子进程在退出Python程序时失效


68

有没有办法确保所有创建的子进程在Python程序退出时都消失了?所谓子流程,是指那些用subprocess.Popen()创建的内容。

如果不是,我是否应该遍历所有发出的终止点,然后终止-9?有什么清洁剂吗?



Answers:


52

您可以为此使用atexit,并注册程序退出时要运行的所有清理任务。

atexit.register(func [,* args [,** kargs]])

在清理过程中,您还可以实现自己的等待,并在发生所需的超时时将其终止。

>>> import atexit
>>> import sys
>>> import time
>>> 
>>> 
>>>
>>> def cleanup():
...     timeout_sec = 5
...     for p in all_processes: # list of your processes
...         p_sec = 0
...         for second in range(timeout_sec):
...             if p.poll() == None:
...                 time.sleep(1)
...                 p_sec += 1
...         if p_sec >= timeout_sec:
...             p.kill() # supported from python 2.6
...     print 'cleaned up!'
...
>>>
>>> atexit.register(cleanup)
>>>
>>> sys.exit()
cleaned up!

注意-如果该进程(父进程)被杀死,则不会运行注册的函数。

python> = 2.6不再需要以下Windows方法

这是一种杀死Windows中进程的方法。您的Popen对象具有pid属性,因此您可以通过success = win_kill(p.pid)进行调用(需要安装pywin32):

    def win_kill(pid):
        '''kill a process by specified PID in windows'''
        import win32api
        import win32con

        hProc = None
        try:
            hProc = win32api.OpenProcess(win32con.PROCESS_TERMINATE, 0, pid)
            win32api.TerminateProcess(hProc, 0)
        except Exception:
            return False
        finally:
            if hProc != None:
                hProc.Close()

        return True

您能解释一下您在Windows代码中正在做什么。
Roshan Mehta 2014年

考虑到p.kill()存在,为什么需要“ win_kill”?适用于2.6之前的python用户吗?
DA

是的,我相信当时2.5仍在广泛使用,而p.kill()在Windows中不可用。
monkut,

对于Popen.wait()接受timeout参数的Python 3.3+,可以简化此答案。首先循环调用p.teminate()子流程(请求正常关机)。然后wait()在设置p.kill()了超时的情况下调用每个进程,并在抛出超时到期异常时进行调用。
ncoghlan

43

在* nix上,也许使用进程组可以为您提供帮助-您还可以捕获子进程产生的子进程。

if __name__ == "__main__":
  os.setpgrp() # create new process group, become its leader
  try:
    # some code
  finally:
    os.killpg(0, signal.SIGKILL) # kill all processes in my group

另一个考虑因素是升级信号:从SIGTERM(的默认信号kill)到SIGKILL(又名kill -9)。在信号之间等待一会儿,使过程有机会干净地退出,然后再kill -9处理。


该策略似乎在流程组中包括了父流程。因此,当您将SIGTERM发送到进程组时,父级也会收到它。这可能是不可取的(因为它在我的流程中)。
drootang '20

16

subprocess.Popen.wait()是确保它们已死的唯一方法。实际上,POSIX OS要求您等待孩子。许多* nix会创建一个“僵尸”进程:一个死掉的孩子,父母没有等待。

如果孩子的笔迹合理,则终止。通常,孩子们会从PIPE阅读。关闭输入对孩子来说是一个很大的暗示,它应该关闭商店并退出。

如果孩子有错误并且没有终止,则可能必须杀死它。您应该修复此错误。

如果该子项是“永远保留”循环,并且不旨在终止,则应杀死该子项或提供一些输入或消息以强制其终止。


编辑。

在标准操作系统中, os.kill( PID, 9 )。杀死-9是残酷的,顺便说一句。如果您可以使用SIGABRT(6?)或SIGTERM(15)杀死它们,那会更礼貌。

在Windows操作系统中,没有os.kill适用的软件。查看此ActiveState食谱以终止Windows中的进程。

我们有属于WSGI服务器的子进程。要终止它们,我们在特殊的URL上执行GET;这会导致孩子清理并离开。


5

查找适用于linux的解决方案(无需安装prctl):

def _set_pdeathsig(sig=signal.SIGTERM):
    """help function to ensure once parent process exits, its childrent processes will automatically die
    """
    def callable():
        libc = ctypes.CDLL("libc.so.6")
        return libc.prctl(1, sig)
    return callable


subprocess.Popen(your_command, preexec_fn=_set_pdeathsig(signal.SIGTERM)) 

4

警告:仅Linux!您可以让孩子在父母去世时收到信号。

首先安装python-prctl == 1.5.0,然后更改父代码以启动子进程,如下所示

subprocess.Popen(["sleep", "100"], preexec_fn=lambda: prctl.set_pdeathsig(signal.SIGKILL))

这是什么意思:

  • 启动子过程:睡眠100
  • 在分叉之后且在子流程执行之前,子级注册为“当我的父级终止时向我发送SIGKILL”。


3

我需要这个问题的一个小变化(清理子进程,但不退出Python程序本身),因为在其他答案中未提及:

p=subprocess.Popen(your_command, preexec_fn=os.setsid)
os.killpg(os.getpgid(p.pid), 15)

setsid将在新的会话中运行该程序,从而为其分配一个新的进程组及其子进程。os.killpg因此,调用它也不会降低您自己的python进程。


不,您不能...这将改变流程本身,如果您
要害的

我已经阅读了问题,您已经阅读了我的答案吗?我明确地写道,我需要这个问题的一个小变化...如果您用谷歌搜索“子进程出口子项”,这将是您会找到的第一个结果。实际上,能够杀死孩子而不退出而不是简单地在退出时杀害孩子,这是一个更普遍的问题,因此这对于其他人在遇到同样问题时会很有用。
berdario 2014年

stackoverflow常见问题What, specifically, is the question asking for? Make sure your answer provides that – or a viable alternative.解答说:我提供了一个替代方法和一个有用的答案,与此同时,您对我发布的两个答案都持否定态度。如果您停止骚扰试图帮助stackoverflow用户的人们,那么我相信整个社区都会对此表示欢迎。
berdario 2014年

3

orip的答案很有帮助,但缺点是它会杀死您的进程并向您的父母返回错误代码。我避免这样:

class CleanChildProcesses:
  def __enter__(self):
    os.setpgrp() # create new process group, become its leader
  def __exit__(self, type, value, traceback):
    try:
      os.killpg(0, signal.SIGINT) # kill all processes in my group
    except KeyboardInterrupt:
      # SIGINT is delievered to this process as well as the child processes.
      # Ignore it so that the existing exception, if any, is returned. This
      # leaves us with a clean exit code if there was no exception.
      pass

接着:

  with CleanChildProcesses():
    # Do your work here

当然,您可以使用try / except / finally进行此操作,但是必须分别处理特殊情况和非特殊情况。


这不是禁用control-c吗?
dstromberg '19


2

有没有办法确保所有创建的子进程在Python程序退出时都消失了?所谓子流程,是指那些用subprocess.Popen()创建的内容。

您可能会违反封装并通过执行以下操作来测试所有Popen进程是否已终止

subprocess._cleanup()
print subprocess._active == []

如果不是,我是否应该遍历所有发出的终止点,然后终止-9?有什么清洁剂吗?

您不能确保所有子进程都死了,而不必退出并杀死每个幸存者。但是,如果您遇到此问题,则可能是因为您有一个更深层次的设计问题。


2

我实际上需要执行此操作,但是它涉及运行远程命令。我们希望能够通过关闭与服务器的连接来停止进程。另外,例如,如果您正在python repl中运行,则希望使用Ctrl-C退出时可以选择作为前台运行。

import os, signal, time

class CleanChildProcesses:
    """
    with CleanChildProcesses():
        Do work here
    """
    def __init__(self, time_to_die=5, foreground=False):
        self.time_to_die = time_to_die  # how long to give children to die before SIGKILL
        self.foreground = foreground  # If user wants to receive Ctrl-C
        self.is_foreground = False
        self.SIGNALS = (signal.SIGHUP, signal.SIGTERM, signal.SIGABRT, signal.SIGALRM, signal.SIGPIPE)
        self.is_stopped = True  # only call stop once (catch signal xor exiting 'with')

    def _run_as_foreground(self):
        if not self.foreground:
            return False
        try:
            fd = os.open(os.ctermid(), os.O_RDWR)
        except OSError:
            # Happens if process not run from terminal (tty, pty)
            return False

        os.close(fd)
        return True

    def _signal_hdlr(self, sig, framte):
        self.__exit__(None, None, None)

    def start(self):
        self.is_stopped = False
        """
        When running out of remote shell, SIGHUP is only sent to the session
        leader normally, the remote shell, so we need to make sure we are sent 
        SIGHUP. This also allows us not to kill ourselves with SIGKILL.
        - A process group is called orphaned when the parent of every member is 
            either in the process group or outside the session. In particular, 
            the process group of the session leader is always orphaned.
        - If termination of a process causes a process group to become orphaned, 
            and some member is stopped, then all are sent first SIGHUP and then 
            SIGCONT.
        consider: prctl.set_pdeathsig(signal.SIGTERM)
        """
        self.childpid = os.fork()  # return 0 in the child branch, and the childpid in the parent branch
        if self.childpid == 0:
            try:
                os.setpgrp()  # create new process group, become its leader
                os.kill(os.getpid(), signal.SIGSTOP)  # child fork stops itself
            finally:
                os._exit(0)  # shut down without going to __exit__

        os.waitpid(self.childpid, os.WUNTRACED)  # wait until child stopped after it created the process group
        os.setpgid(0, self.childpid)  # join child's group

        if self._run_as_foreground():
            hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN)  # ignore since would cause this process to stop
            self.controlling_terminal = os.open(os.ctermid(), os.O_RDWR)
            self.orig_fore_pg = os.tcgetpgrp(self.controlling_terminal)  # sends SIGTTOU to this process
            os.tcsetpgrp(self.controlling_terminal, self.childpid)
            signal.signal(signal.SIGTTOU, hdlr)
            self.is_foreground = True

        self.exit_signals = dict((s, signal.signal(s, self._signal_hdlr))
                                 for s in self.SIGNALS)                                     

    def stop(self):
        try:
            for s in self.SIGNALS:
                #don't get interrupted while cleaning everything up
                signal.signal(s, signal.SIG_IGN)

            self.is_stopped = True

            if self.is_foreground:
                os.tcsetpgrp(self.controlling_terminal, self.orig_fore_pg)
                os.close(self.controlling_terminal)
                self.is_foreground = False

            try:
                os.kill(self.childpid, signal.SIGCONT)
            except OSError:
                """
                can occur if process finished and one of:
                - was reaped by another process
                - if parent explicitly ignored SIGCHLD
                    signal.signal(signal.SIGCHLD, signal.SIG_IGN)
                - parent has the SA_NOCLDWAIT flag set 
                """
                pass

            os.setpgrp()  # leave the child's process group so I won't get signals
            try:
                os.killpg(self.childpid, signal.SIGINT)
                time.sleep(self.time_to_die)  # let processes end gracefully
                os.killpg(self.childpid, signal.SIGKILL)  # In case process gets stuck while dying
                os.waitpid(self.childpid, 0)  # reap Zombie child process
            except OSError as e:
                pass
        finally:
            for s, hdlr in self.exit_signals.iteritems():
                signal.signal(s, hdlr)  # reset default handlers

    def __enter__(self):
        if self.is_stopped:
            self.start()

    def __exit__(self, exit_type, value, traceback):
        if not self.is_stopped:
            self.stop()

感谢Malcolm Handley的最初设计。在Linux上使用python2.7完成。




0

您可以尝试subalive,我为类似问题编写的软件包。它通过RPC使用定期的活动ping,当主服务器出于某种原因停止活动ping时,从属进程会自动终止。

https://github.com/waszil/subalive

大师的例子:

from subalive import SubAliveMaster

# start subprocess with alive keeping
SubAliveMaster(<path to your slave script>)

# do your stuff
# ...

从属子过程的示例:

from subalive import SubAliveSlave

# start alive checking
SubAliveSlave()

# do your stuff
# ...
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.