Python: 非阻塞 + 非僵尸进程

14

我想创建一个父进程,它将创建许多子进程。由于父进程负责创建子进程,父进程不会关心子进程的状态。

由于subprocess.call是阻塞的,它不起作用。因此,我使用subprocess.Popen替代call。然而,一旦子进程终止,Popen将生成僵尸(defunct)进程(链接)。

有没有办法解决这个问题?


两件事,听起来你混淆了父母和孩子。另一个问题是,为什么你想要创建僵尸进程? - Blubber
1
我不想要僵尸进程。这只是 Popen 的副作用,我想避免它。 - Winston
所以你想要生成N个进程,让它们并行执行任务,然后阻塞父进程直到它们全部完成? - Blubber
实际上,父进程只是创建子进程。子进程随时可能死亡,而父进程并不关心。因此,它不会阻塞。 - Winston
但这仍然太不具体了。父进程是什么?它会在子进程运行时继续运行吗?还是只是一个实用程序,生成N个进程,然后退出并让子进程运行?如果是后者,请参阅https://dev59.com/eW025IYBdhLWcg3w1JiG - Blubber
抱歉没有给出具体细节。父进程会一直运行,并在任意时间创建子进程。子进程将在随机时间死亡,而父进程不关心子进程的生死。 - Winston
4个回答

20

有很多方法可以处理这个问题。关键点在于僵尸/"defunct"进程存在是为了让父进程收集它们的状态。

  1. 作为进程的创建者,你可以宣布忽略该状态。 POSIX方法是设置标志SA_NOCLDWAIT(使用sigaction)。在Python中执行此操作有些麻烦;但是大多数类Unix系统允许简单地忽略SIGCHLD / SIGCLD(不同的类Unix系统拼写有所不同),在Python中很容易实现:

    import signal

    signal.signal(signal.SIGCHLD, signal.SIG_IGN)

  2. 或者,如果出于某些原因无法使用此方法或在您的系统上无法正常工作,则可以使用一个老套路:不要只fork一次,而是fork两次。在第一个子进程中,fork第二个子进程;在第二个子进程中,使用execve(或类似)运行所需的程序;然后在第一个子进程中,退出(使用_exit)。在原始父进程中,使用waitwaidpid或任何操作系统提供的方法,并收集第一个子进程的状态。

    之所以会起作用,是因为第二个子进程现在已成为“孤儿”(其父进程,第一个子进程已经死亡并被你的原始进程收集)。作为孤儿,它被移交给一个代理父进程(具体而言是“init”),后者总是wait-ing,并立即收集所有僵尸进程。

  3. 除了双重fork之外,您还可以使子进程生活在自己单独的会话中和/或放弃控制终端访问权限(在Unix-y术语中称为“daemonize”)。 (这有些混乱且依赖于操作系统;我以前编写过这样的代码,但对于我现在无法访问的某些公司代码。)

  4. 最后,你可以定期收集这些进程。如果你使用的是subprocess模块,只需在方便的时候调用.poll函数。如果进程仍在运行,则返回None,如果它已经完成,则返回退出状态(已收集)。如果有一些进程仍在运行,你的主程序仍然可以退出,而它们继续运行。此时,它们就成为孤儿进程,如上述方法#2。

"忽略SIGCHLD"方法简单易行,但缺点是会干扰创建和等待子进程的库例程。在Python 2.7及更高版本中有一个解决方法(http://bugs.python.org/issue15756),但这意味着库例程无法看到这些子进程的任何失败。

[编辑:http://bugs.python.org/issue1731717是关于p.wait()的,其中p是从subprocess.Popen获得的进程;15756是特别针对p.poll()的;但无论如何,如果你没有修复程序,你必须采取方法2、3或4。]


谢谢,我尝试了signal.signal,但是当我调用subprocess.call时会导致OSError: [Errno 10] No child processes - Winston
这意味着您没有解决方法(上面的bugs.python.org链接)。 subprocesscallPopenwait组成,您已告诉操作系统丢弃子状态,因此底层的os.wait失败并显示errno.ECHILD。(解决方法只是将其视为“子进程退出代码= 0”) - torek
3
"signal.signal(signal.SIGCHLD, signal.SIG_IGN)" 这个方法对我管用。 - Alix Martin
谢谢,signal.signal(signal.SIGCHLD, signal.SIG_IGN) 在运行在 Docker 容器中的 Python 2.7.10 上对我很有用。 - phansen
虽然 signal.signal(signal.SIGCHLD, signal.SIG_IGN) 可以工作,但在使用 multiprocessing 等模块时可能不安全。它的使用并不特定于某个子进程。 - Asclepius

3

在终止或杀死进程后,操作系统会等待父进程收集子进程的状态。您可以使用进程的communicate()方法来收集状态:

p = subprocess.Popen( ... )
p.terminate()
p.communicate()

请注意,终止一个进程允许该进程拦截终止信号并对其进行任何操作。这是关键的,因为p.communicate()是一个阻塞调用。
如果您不希望出现这种行为,请使用p.kill()代替p.terminate(),后者可使进程不拦截信号。
如果您想使用p.terminate()并确保进程自行结束,您可以使用psutil模块检查进程状态。

这似乎是正确且最简单的解决方案。在我的情况下,我将一个子进程导入到另一个子进程中,并且第一个子进程可能会挂起,但它对我有效。 - Jason Drew

0

torek的方法可以!

我发现另一种处理僵尸进程的方式;

我们可以使用waitpid根据需要回收僵尸进程:

import os, subprocess, time

def recycle_pid():
    while True:
        try:
            pid, status, _ = os.wait3(os.WNOHANG)
            if pid == 0:
                break
            print("----- child %d terminated with status: %d" %(pid, status))
        except OSError,e:
            break

print("+++++ start pid:", subprocess.Popen("ls").pid)
recycle_pid()
print("+++++ start pid:", subprocess.Popen("ls").pid)
recycle_pid()
time.sleep(1)
recycle_pid()

recycle_pid是非阻塞的,可以根据需要随时调用。


谢谢。我认为解决方案有点复杂。我认为有一些Python库可以处理子进程(例如,杀死僵尸进程)。你知道有没有这样的库吗?谢谢。 - Winston
无法杀死僵尸进程,您可以参考此线程:https://dev59.com/NGQn5IYBdhLWcg3wTVl0 - xjdrew

-2

我强烈感觉到他不想等孩子们出去。 - Blubber
他似乎想启动一堆进程,可以使用Process.start()来完成。这个调用是非阻塞的。父子关系仍然保持,因此他可以采取任何想要采取的与子进程相关的操作。父进程将并行运行。 - vowelless
谢谢回复。实际上,子进程是一个 shell 命令,无法通过 multiprocessing 执行。是否有其他替代方案? - Winston
你可以执行以下步骤:1. 创建N个进程对象 2. 对所有对象调用start()方法 3. 然后对象调用subprocess.call方法这样,你就可以使用多进程执行shell命令。 - vowelless

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