执行摘要(或“tl;dr”版本):如果最多只有一个subprocess.PIPE
,那么很容易处理,否则就很难。
现在是时候解释一下subprocess.Popen
如何工作了。
(警告:这适用于Python 2.x,尽管3.x类似;而且我对Windows变体非常模糊。我更好地理解POSIX的东西。)
Popen
函数需要同时处理零到三个I/O流。这些通常被标记为stdin
、stdout
和stderr
。
您可以提供:
None
,表示您不想重定向流。它将像往常一样继承这些。请注意,在至少POSIX系统上,这并不意味着它将使用Python的sys.stdout
,而只是Python的实际 stdout;请参见末尾的演示。
- 一个
int
值。这是一个“原始”的文件描述符(至少在POSIX中)。 (副注:PIPE
和STDOUT
实际上是内部int
,但是是“不可能”的描述符-1和-2。)
- 一个流 - 实际上是任何具有
fileno
方法的对象。 Popen
将查找该流的描述符,使用stream.fileno()
,然后像int
值一样继续。
subprocess.PIPE
,表示Python应创建一个管道。
subprocess.STDOUT
(仅适用于stderr
):告诉Python使用与stdout
相同的描述符。这只在您为stdout
提供了(非None
)值,并且即使如此,它也只是需要如果您设置了stdout=subprocess.PIPE
。(否则,您可以只提供与stdout
提供的相同参数,例如,Popen(...,stdout = stream,stderr = stream)
。)
最简单的情况(没有管道)
如果您不重定向任何内容(将所有三个保留为默认的None
值或提供显式的None
),则Pipe
非常容易处理。它只需启动子进程并让其运行即可。或者,如果您重定向到非PIPE
- 一个int
或流的fileno()
,那么它仍然很容易,因为操作系统会完成所有工作。Python只需启动子进程,将其stdin、stdout和/或stderr连接到提供的文件描述符即可。
还是比较简单的情况:一个管道
如果你只重定向一个流,Pipe
仍然非常容易处理。我们逐个流来看。
假设你想提供一些 stdin
,但让 stdout
和 stderr
未被重定向,或者重定向到文件描述符。作为父进程,你的 Python 程序只需要使用 write()
将数据发送到管道中。你可以自己这样做,例如:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n')
或者您可以将标准输入数据传递给
proc.communicate()
,然后执行上述的
stdin.write
。由于没有输出返回,因此
communicate()
只有另一个真正的任务:它还会为您关闭管道。(如果您不调用
proc.communicate()
,则必须调用
proc.stdin.close()
来关闭管道,以便子进程知道没有更多的数据通过。)
假设您想捕获
stdout
但保留
stdin
和
stderr
。同样,这很容易:只需调用
proc.stdout.read()
(或等效函数),直到没有更多的输出为止。由于
proc.stdout()
是一个普通的Python I/O流,您可以在其上使用所有常规结构,例如:
for line in proc.stdout:
或者,你可以使用
proc.communicate()
,它会为你简单地执行
read()
。
如果你只想捕获
stderr
,那么它的用法与
stdout
相同。
在事情变得困难之前,还有一个技巧。假设你想捕获
stdout
,并且也想捕获
stderr
,但是
在与stdout
相同的管道上:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
在这种情况下,
subprocess
“作弊”了!好吧,它必须这样做,所以它并不是真正的作弊:它启动子进程,并将其标准输出和标准错误输出都导向反馈到其父(Python)进程的(单个)管道描述符中。在父进程端,仍然只有一个用于读取输出的管道描述符。所有“stderr”输出都会显示在
proc.stdout
中,如果调用
proc.communicate()
,则stderr结果(元组中的第二个值)将为
None
,而不是字符串。
困难的案例:两个或更多的管道
问题都出现在您想要使用至少两个管道时。实际上,
subprocess
代码本身就有这部分内容:
def communicate(self, input=None):
...
if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
但是,不幸的是,我们至少创建了两个,甚至三个不同的管道,因此
count(None)
返回1或0。我们必须采取更困难的方法。
在Windows上,这使用
threading.Thread
来累积
self.stdout
和
self.stderr
的结果,并且父线程传递
self.stdin
输入数据(然后关闭管道)。
在POSIX上,如果可用,这将使用
poll
,否则使用
select
来累积输出并传递stdin输入。所有这些都在(单个)父进程/线程中运行。
这里需要线程或轮询/选择以避免死锁。例如,假设我们已将所有三个流重定向到三个单独的管道。进一步假设在写入进程被暂停等待从另一端“清除”管道之前,可以将多少数据塞入管道中存在小限制。为了说明问题,让我们将该小限制设置为一个字节。 (事实上,这就是事情的工作方式,只是限制比一个字节大得多。)
如果父(Python)进程尝试写入几个字节-例如,
'go\n'
到
proc.stdin
,第一个字节进入,然后第二个字节导致Python进程暂停,等待子进程读取第一个字节,从另一端清空管道。
同时,假设子进程决定打印友好的“Hello!不要惊慌!”问候语。
H
进入其stdout管道,但
e
导致它暂停,等待其父进程读取该
H
,并清空stdout管道。
现在我们陷入了困境:Python进程正在睡眠中,等待完成“go”的说法,而子进程也在睡眠中,等待完成“Hello!不要惊慌!”的说法。
subprocess.Popen
代码通过线程或选择/轮询避免了这个问题。当字节可以通过管道时,它们会通过。当不能时,只有一个线程(而不是整个进程)必须睡眠-或者,在选择/轮询的情况下,Python进程同时等待“可以写入”或“数据可用”,仅在有空间时将数据写入进程的stdin,并且仅在准备好数据时读取其stdout和/或stderr。
proc.communicate()
代码(实际上是
_communicate
,其中处理棘手的情况)返回一旦所有stdin数据(如果有)已发送并且已累积所有stdout和/或stderr数据。
如果您想在两个不同的管道上读取
stdout
和
stderr
(无论是否重定向了任何
stdin
),您需要避免死锁。 此处的死锁场景不同-当子进程在您从
stdout
拉取数据时写入一些长字符到
stderr
,或反之亦然-但它仍然存在。
演示
我承诺演示未重定向的Python subprocess
将写入底层stdout,而不是sys.stdout
。 所以,这里有一些代码:
from cStringIO import StringIO
import os
import subprocess
import sys
def show1():
print 'start show1'
save = sys.stdout
sys.stdout = StringIO()
print 'sys.stdout being buffered'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
in_stdout = sys.stdout.getvalue()
sys.stdout = save
print 'in buffer:', in_stdout
def show2():
print 'start show2'
save = sys.stdout
sys.stdout = open(os.devnull, 'w')
print 'after redirect sys.stdout'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
sys.stdout = save
show1()
show2()
当运行时:
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
请注意,如果添加
stdout=sys.stdout
,第一个例程将失败,因为
StringIO
对象没有
fileno
。如果添加
stdout=sys.stdout
,第二个例程将省略
hello
,因为
sys.stdout
已被重定向到
os.devnull
。
(如果您重定向 Python 的文件描述符 1,则子进程将遵循该重定向。调用
open(os.devnull, 'w')
会产生一个流,其
fileno()
大于 2。)
Popen.poll
。链接:https://dev59.com/bnA75IYBdhLWcg3w_ef1。 - Paulo Almeidagit
)只有在它们的输出是“tty设备”(通过libcisatty()
测试)时才会这样做。在这种情况下,您可能需要打开一个伪终端。 - torek0
,则while not ret_val.poll()
将创建一个无限循环。 - Viktor Kerkez