如何等待子进程?
我有一个Python脚本,它是这样启动任务的:
import os
os.system("./a.sh")
do_c()
但是,a.sh
是一个bash脚本,它会启动其他程序。这个bash脚本本身似乎在所有被启动的脚本准备好之前就已经准备好了。
我该如何等待所有脚本(子进程)准备好后,再执行do_c()
呢?
澄清一下:我说的准备好,是指完成/退出。
示例
run.py
这个文件可以修改。但不要依赖sleep,因为我不知道a.py
和b.py
需要多长时间。
#!/usr/bin/env python
import os
from time import sleep
print("Started run.py")
os.system("./a.py")
print("a is ready.")
print("Now all messages should be there.")
sleep(30)
a.py
这个文件不能修改:
#!/usr/bin/env python
import subprocess
import sys
print(" Started a.py")
pid = subprocess.Popen([sys.executable, "b.py"])
print(" End of a.py")
b.py
这个文件也不能修改:
#!/usr/bin/env python
from time import sleep
print(" Started b.py")
sleep(10)
print(" Ended b.py")
期望的输出
最后一条消息应该是现在所有消息都应该出现了。
.
当前输出
started run.py
Started a.py
End of a.py
a is ready.
Now all messages should be there.
Started b.py
Ended b.py
1 个回答
处理这种情况的常规方法并不奏效。因为默认情况下,os.system
会等待a.py
执行完毕,但a.py
在它的子进程执行完之前就已经退出了。要找到b.py
的进程ID(PID)也很麻烦,因为一旦a.py
退出,b.py
就无法再与它有任何关联——即使b.py
的父进程ID也是1,也就是init
进程。
不过,我们可以利用继承的文件描述符来作为一种简单的信号,表示子进程已经结束。我们可以在run.py
中设置一个管道,管道的读端在run.py
中,而写端则被a.py
及其所有子进程继承。只有当最后一个子进程退出时,管道的写端才会关闭,这时在管道的读端执行read()
就不会再阻塞了。
下面是一个修改过的run.py
,实现了这个想法,并显示了我们想要的输出:
#!/usr/bin/env python
import os
from time import sleep
print("Started run.py")
r, w = os.pipe()
pid = os.fork()
if pid == 0:
os.close(r)
os.execlp("./a.py", "./a.py")
os._exit(127) # unreached unless execlp fails
os.close(w)
os.waitpid(pid, 0) # wait for a.py to finish
print("a is ready.")
os.read(r, 1) # wait for all the children that inherited `w` to finish
os.close(r)
print("Now all messages should be there.")
解释:
管道是一种进程间通信的工具,允许父进程和子进程通过继承的文件描述符进行交流。通常,我们会创建一个管道,分叉一个进程,可能执行一个外部文件,然后从管道的读端读取一些数据,这些数据是由另一个进程写入管道的写端的。(Shell使用这个机制实现管道,通过进一步操作将标准文件描述符如stdin和stdout指向管道的适当端。)
在这个例子中,我们并不关心与子进程交换实际数据,我们只想在它们退出时得到通知。为此,我们利用一个事实:当一个进程结束时,内核会关闭它的所有文件描述符。反过来,当一个分叉的进程继承文件描述符时,只有当所有该描述符的副本都关闭时,这个文件描述符才被认为是关闭的。因此,我们设置一个管道,其写端会被a.py
生成的所有进程继承。这些进程不需要知道这个文件描述符的存在,唯一重要的是,当它们全部结束时,管道的写端会关闭。这时在管道的读端执行os.read()
就会停止阻塞,并返回一个长度为0的字符串,表示文件结束。
下面的代码是这个想法的简单实现:
在
os.pipe()
和第一个print
之间的部分是os.system()
的实现,唯一的区别是它在子进程中关闭了管道的读端。(这是必要的——简单调用os.system()
会保持读端打开,这会导致父进程的最终读取无法正常工作。)os.fork()
会复制当前进程,区分父进程和子进程的唯一方法是,在父进程中你会得到子进程的PID(而子进程得到的是0,因为它可以通过os.getpid()
获取自己的PID)。if pid == 0:
这个分支在子进程中运行,只执行./a.py
。这里的“执行”意味着它运行指定的可执行文件而不会返回。os._exit()
只是为了防止execlp
失败时的情况(在Python中可能不必要,因为execlp
失败会引发异常并退出程序,但还是保留了)。程序的其余部分在父进程中运行。父进程关闭管道的写端(否则尝试从读端读取会导致死锁)。
os.waitpid(pid)
是父进程通常通过os.system()
等待a.py
的方式。在我们的例子中,调用waitpid
并不是必须的,但这样做是个好主意,可以防止僵尸进程的产生。os.read(r, 1)
是关键所在:它尝试从管道的读端读取最多1个字符。由于没有人会向管道的写端写入数据,因此读取会阻塞,直到管道的写端关闭。由于a.py
的子进程对继承的文件描述符一无所知,关闭的唯一方式是内核在相应进程结束后进行关闭。当所有继承的写端描述符都关闭时,os.read()
会返回一个长度为0的字符串,我们会忽略它并继续执行。最后,我们关闭管道的写端,以释放共享资源。