我如何等待subprocess?

我有一个Python脚本来启动这样的任务:

import os os.system("./a.sh") do_c() 

但是a.sh是启动其他程序的bash脚本。 在所有已启动的脚本准备就绪之前,bash脚本本身似乎已经准备就绪。

在执行do_c()之前,如何等待所有脚本(subprocess)准备就绪?

澄清:当我准备好了,我的意思是完成/退出。

run.py

这个文件可以改变。 但不要依赖睡眠,因为我不知道a.pyb.py花多less时间。

 #!/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") 

期望的输出

最后的消息必须Now all messages should be there.

电stream输出

 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 

处理这种情况的通常方法是行不通的。 等待a.py (默认情况下是a.py )不起作用,因为a.py在其子a.py执行a.py之前退出。 查找b.py的PID是非常棘手的,因为一旦a.py退出, b.py就无法以任何方式连接到它 – 甚至b.py的父PID也是1,即init进程。

但是,有可能利用继承的文件描述符作为一个穷人的信号,表明一个孩子已经死了。 设置一个读取结束在run.py中的run.py ,其写入结束由a.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.") 

说明:

Pipe是一个进程间通信设备,它允许父进程和子进程通过继承的文件描述符进行通信。 通常情况下,创建一个管道,fork一个进程,可能执行一个外部文件,并从管道的读端读取一些数据,另一个数据写入管道的写端。 (Shells通过进一步使用这种机制来实现管道,并使标准文件描述符(如标准输入和标准输出指向管道的适当的末端)。

在这种情况下,我们不关心与孩子交换实际的数据,我们只想在他们退出时通知他们。 为了达到这个目的,我们利用了一个事实,即当一个进程死亡时,内核关闭它的所有文件描述符。 反过来,当分叉进程继承文件描述符时,当描述符的所有副本都关闭时,文件描述符被认为是关闭的。 所以我们设置了一个写入结束的管道,它将被a.py产生的所有进程继承。 这些进程不需要知道这个文件描述符的任何内容,唯一重要的是当它们全部死亡时,管道的写入结束将关闭。 这将在os.read()的管道读端指示不再阻塞并返回一个0长度的字符串,表示文件结束的情况。

代码是这个想法的简单实现:

  • os.pipe()和第一个print之间的部分是os.pipe()的一个实现,区别在于它关闭了子节点中管道的读端。 (这是必要的 – 简单地调用os.system()将保持读取端打开,这将防止父母的最终读取正常工作。)

  • os.fork()复制当前进程,唯一的办法来区分父母和孩子是父母你得到的孩子PID(和孩子得到0,因为它总是可以找到它的PID使用os.getpid() )。

  • if pid == 0:分支运行在子./a.py ,只能执行./a.py 。 “Exec”意味着它运行指定的可执行文件而不会返回 。 在execlp失败的情况下, os._exit()只是在那里(在Python中可能是不必要的,因为execlp失败会引发一个异常,它将退出程序,但仍然)。 程序的其余部分在父项中运行。

  • 父节点关闭管道的写端(否则试图从读端读取会死锁)。 os.waitpid(pid)是通常由os.system()执行的os.system()的等待。 在我们的例子中没有必要调用waitpid ,但是这样做是一个好主意,以防止僵尸的剩余。

  • os.read(r, 1)是魔术发生的地方:它试图从管道的读端读取至多1个字符。 由于没有人写入到管道的写入端,所以读取将被阻塞,直到管道的写入端被关闭。 由于a.py的子项对于继承的文件描述符一无所知,所以关闭它的唯一方法就是内核在各个进程死亡后执行它。 当所有的被继承的write-end描述符关闭时, os.read()返回一个零长度的字符串,我们忽略它并继续执行。

  • 最后,我们关闭管道的写端,以便共享资源被释放。