subprocess.call()和subprocess.Popen()之间的区别是什么,为什么对于前者使用PIPE会更不安全?

16
我已查看了它们的文档。
这个问题是由J.F.在这里发表评论引起的:检索subprocess.call()的输出 关于使用PIPE进行subprocess.call(),当前Python文档中subprocess.call()表示如下:
注意,不要在此函数中使用stdout=PIPEstderr=PIPE。如果子进程生成足够的输出填充操作系统管道缓冲区,则该子进程将阻塞,因为管道未被读取。
Python 2.7 subprocess.call()
请勿在此函数中使用stdout=PIPEstderr=PIPE,因为这可能会导致子进程输出量死锁。当需要使用管道时,请使用带有communicate()方法的Popen。
Python 2.6没有这样的警告。
另外,subprocess.call()subprocess.check_call()似乎没有访问其输出的方法,除了使用stdout=PIPE与communicate()一起使用。

https://docs.python.org/2.6/library/subprocess.html#convenience-functions

请注意,如果您想将数据发送到进程的标准输入(stdin),则需要使用stdin=PIPE创建Popen对象。类似地,要在结果元组中获得除None以外的任何内容,您还需要同时提供stdout=PIPE和/或stderr=PIPE。

https://docs.python.org/2.6/library/subprocess.html#subprocess.Popen.communicate

< p> subprocess.call() subprocess.Popen() 之间的区别是什么,使得对于 subprocess.call()来说, PIPE 不够安全?

更具体地说:为什么 subprocess.call()会出现“基于子进程输出量的死锁”,而 Popen()则不会?


// ,后续问题:这种差异在Python 2和Python 3之间有何变化? - Nathan Basanese
你为什么总是用 // 开头呢? - chepner
3
@chepner: 好的,按钮上确实写着“添加评论”.... :) - John Zwinck
//,关于 Meta 的后续问题:为什么在 SO 上很少有人有幽默感? - Nathan Basanese
3
或许我完全没有理解你的意思,但Python中的注释符号是#,不同于C++、PHP和JavaScript。 - Damian Yerrick
// 好的,我猜这个页面运行 JavaScript? - Nathan Basanese
2个回答

21

call()实际上就是Popen().wait()(带有错误处理)。

不应该在call()中使用stdout=PIPE,因为它不会从管道中读取数据,因此当子进程填满相应的操作系统管道缓冲区时,子进程将挂起。下面是一个显示command1 | command2 shell管道中数据流动情况的图片:

pipe/stdio buffers

无论您的Python版本如何--管道缓冲区(请看图片)都在您的Python进程之外。Python 3不使用C stdio,但仅影响内部缓冲。当内部缓冲被刷新时,数据会进入管道。如果command2(您的父Python程序)不从管道中读取,则由call()启动的子进程(例如,command1)一旦管道缓冲区已满(pipe_size = fcntl(p.stdout, F_GETPIPE_SZ)在我的Linux系统上约为65K(最大值为/proc/sys/fs/pipe-max-size约为1M))将挂起。

如果以后从管道中读取数据,可以使用stdout=PIPE,例如使用Popen.communicate()方法。您也可以直接从process.stdout(代表管道的文件对象)中读取


// 看起来“管道”隐喻相当好用。我不知道我可以那么字面理解,以至于可能会有一个_填充的_管道。让我想知道为什么内核的管道缓冲区不能自动刷新该命令,或者为什么call()不直接刷新该缓冲区。 扩展“管道”隐喻,如果管道安装在可能无法走出管道的地方,为什么不留下压力激活连接(电磁阀?)到排水坑? (“刷新缓冲区!使代码更强大!”- 可能的口号?) - Nathan Basanese
// 我可以猜到为什么有些人称系统程序员为“管道工”。这都与管道有关。 - Nathan Basanese
1
有一种方法可以说“自动清空管道缓冲区”,它被称为:“丢弃数据”,并实现为stdout=DEVNULL(您可以安全地在call()中使用它)。 - jfs
1
//,但是这样做就不能再以后可能使用communicate()call()的输出或check_call()的输出了,对吧? - Nathan Basanese
1
@NathanBasanese:call()返回子进程的退出状态——它是一个普通的整数(在POSIX上为8位范围,在Windows上为32位);你不能用它来调用communicate()。无论如何,子进程已经死亡,即使在call()返回时,其PID可能已经被其他进程重新使用。check_call()只是call(),如果退出状态为非零,则会引发异常。使用仅使用POSIX调用实现shell管道将是一项有用的练习:pipe、fork、exec、dup2、waitpid,例如recursive-pipe.c - jfs

2
callPopen 都提供了访问命令输出的方法:
  • 使用 Popen,你可以使用 communicate 或者为 stdout=... 参数提供文件描述符或文件对象。
  • 使用 call,你唯一的选择是将文件描述符或文件对象传递给 stdout=... 参数(不能使用 communicate)。
现在,call 中使用 stdout=PIPE 不安全的原因是,call 在子进程完成之前不会返回,这意味着所有输出都必须在内存中等待,如果输出量太大,那么就会填满操作系统管道的缓冲区。
可以验证上述信息的参考文献如下:
  1. 根据此处callPopen 的参数相同:

上面显示的参数只是最常见的参数,在下面的常用参数中进行了描述(因此在缩写签名中略有不同)。完整的函数签名与 Popen 构造函数相同-此函数直接将所有提供的参数传递到该接口。

  1. 根据此处stdout 参数的可能值为:

有效值为 PIPE、现有文件描述符(正整数)、现有文件对象和 None。PIPE 表示应创建到子进程的新管道。


// 在第四段中,stdout=POPEN实际上是指stdout=PIPE吗?还是POEPN是一个值得信赖的文件描述符? - Nathan Basanese
// 像我说的那样,我确实阅读了文档。我读了太多遍了。这让我很难过。另外,关于•点,我知道你必须传递文件描述符或文件对象,并且PIPE是一个坏主意,但我只是想知道为什么在`子进程方法的情况下,这种方式不够安全。此外,在第四段中,为什么方便函数的第一部分会出现,而不是Popen()?为什么这种差异会导致“这意味着...”部分? - Nathan Basanese
1
@NathanBasanese 你好,关于 stdout=POPEN 的问题,是一个笔误。现在已经更正了。至于你其他的疑问:使用 Popen() + PIPE 的原因是你可以随意调用 communicate() 来避免内部操作系统管道缓冲区填满,而在 call() + PIPE 的情况下,你不能调用 communicate(),因为一旦你调用 call(),你的进程就会阻塞。 - Daniel

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