Python守护进程不终止子进程

21 投票
3 回答
8901 浏览
提问于 2025-04-15 21:01

在使用 python-daemon 的时候,我是这样创建子进程的:

import multiprocessing

class Worker(multiprocessing.Process):
   def __init__(self, queue):
      self.queue = queue # we wait for things from this in Worker.run()

   ...

q = multiprocessing.Queue()

with daemon.DaemonContext():
    for i in xrange(3):
       Worker(q)

    while True: # let the Workers do their thing
       q.put(_something_we_wait_for())

当我用 Ctrl-C 或 SIGTERM 等方式杀掉父进程(也就是不是工作进程的那个)时,子进程却没有被杀掉。那怎么才能把子进程也杀掉呢?

我最初的想法是用 atexit 来杀掉所有的工作进程,像这样:

 with daemon.DaemonContext():
    workers = list()
    for i in xrange(3):
       workers.append(Worker(q))

    @atexit.register
    def kill_the_children():
        for w in workers:
            w.terminate()

    while True: # let the Workers do their thing
       q.put(_something_we_wait_for())

不过,处理守护进程的子进程是个棘手的事情,我希望能听听大家的想法和建议,看看应该怎么做。

谢谢。

3 个回答

2

Atexit这个东西不太管用——它只在程序正常结束时才会运行,不包括因为信号而结束的情况——具体可以看看文档开头的说明。你需要通过两种方式之一来设置信号处理。

听起来比较简单的选项是:给你的工作进程设置守护进程标志,具体可以参考这里

听起来稍微复杂一点的选项是:PEP-3143似乎暗示在python-daemon中有一种内置的方法来处理程序清理的需求。

4

当子进程第一次创建时,你应该保存父进程的ID(我们称之为 self.myppid)。如果 self.myppidgetppid() 不一样,那就说明父进程已经死掉了。

为了避免一直检查父进程是否改变,你可以使用 PR_SET_PDEATHSIG,这个在 信号文档中有描述。

5.8 Linux的“父进程死亡”信号

每个进程都有一个变量 pdeath_signal,这个变量在调用 fork() 或 clone() 后初始化为 0。它表示当父进程死掉时,子进程应该收到的信号。

在这种情况下,如果你希望子进程也跟着死掉,可以把它设置为 SIGHUP,像这样:

prctl(PR_SET_PDEATHSIG, SIGHUP);
32

你的选择有点有限。如果在Worker类的构造函数中设置self.daemon = True没有解决问题,而在父进程中尝试捕捉信号(比如SIGTERMSIGINT)也不奏效,你可能需要尝试相反的解决方案——也就是说,不是让父进程杀掉子进程,而是让子进程在父进程死掉时自己“自杀”。

第一步是给Worker的构造函数传入父进程的PID(你可以用os.getpid()来获取)。然后,在工作循环中,不仅仅是执行self.queue.get(),而是要做一些类似下面的事情:

waiting = True
while waiting:
    # see if Parent is at home
    if os.getppid() != self.parentPID:
        # woe is me! My Parent has died!
        sys.exit() # or whatever you want to do to quit the Worker process
    try:
        # I picked the timeout randomly; use what works
        data = self.queue.get(block=False, timeout=0.1)
        waiting = False
    except queue.Queue.Empty:
        continue # try again
# now do stuff with data

上面的解决方案会检查父进程的PID是否和最开始的不一样(也就是说,如果子进程被initlaunchd收养了,因为父进程死掉了)——具体可以参考这个链接。不过,如果这个方法出于某种原因不管用,你可以用下面这个函数替代(改编自这里):

def parentIsAlive(self):
    try:
        # try to call Parent
        os.kill(self.parentPID, 0)
    except OSError:
        # *beeep* oh no! The phone's disconnected!
        return False
    else:
        # *ring* Hi mom!
        return True

现在,当父进程死掉时(无论是什么原因),子进程会像苍蝇一样纷纷倒下——正如你所希望的那样,你这个小恶魔!:-D

撰写回答