【问题标题】:Replacing Shell Pipeline [duplicate]更换外壳管道[重复]
【发布时间】:2015-01-06 10:07:41
【问题描述】:

在 Python 2.7 的 subprocess 模块文档中,我找到了以下 sn-ps:

p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
p1.stdout.close()  # Allow p1 to receive a SIGPIPE if p2 exits.
output = p2.communicate()[0]

来源:https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline

我不明白这一行:p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.

这里 p1.stdout 正在关闭。如果 p2 退出,它如何允许 p1 接收 SIGPIPE?

【问题讨论】:

    标签: python python-2.7


    【解决方案1】:

    如果进程尝试写入没有活动进程正在查找的管道,则通常会发送 SIGPIPE 信号。在相当于您的代码 sn-p 的 shell 管道中:

    `dmesg | grep hda`
    

    如果grep 进程由于某种原因在dmesg 完成写入输出之前终止,dmesg 将收到一个 SIGPIPE 并自行终止。这将是 UNIX/Linux 进程 (http://en.wikipedia.org/wiki/Unix_signal) 的预期行为。

    相比之下,在使用subprocess 的Python 实现中,如果p2p1 完成生成输出之前退出,则不会发送SIGPIPE,因为实际上还有一个进程正在查看管道——Python脚本本身(创建p1p2 的脚本)。更重要的是,脚本正在查看管道但不使用其内容 - 结果是管道无限期地保持打开状态,p1 陷入困境。

    显式关闭 p1.stdout 将 Python 脚本从管道中分离出来,并使得除 p2 之外的任何进程都不会查看管道 - 如果 p2 确实在 p1 之前结束,p1 正确地结束无需任何人为地保持管道打开即可让信号自行结束。

    下面是另一种措辞的解释: http://www.enricozini.org/2009/debian/python-pipes/

    【讨论】:

    • “没有活动进程正在查看的管道”和“仍在查看管道的进程”并不是真正准确的措辞。
    【解决方案2】:

    希望更系统的解释:

    • 管道是由操作系统管理的实例。它有一个读端和一个写端。
    • 两端都可以被多个进程打开。但是,仍然只有一根管道。也就是说,多个进程可以共享同一个管道。
    • 已打开一端的进程拥有相应的文件句柄。进程可以主动close()再来一次!如果进程退出,操作系统会为您关闭相应的文件句柄。
    • 所有涉及的进程都可以close() 代表管道读取端的文件句柄。没有错,这是一个非常好的情况。
    • 现在,如果一个进程将数据写入管道的写入端并且不再打开读取端(没有进程为读取端持有打开的文件句柄),则符合 POSIX 的操作系统会发送一个SIGPIPEwriting 进程发出信号,让其知道不再有阅读器。

    这是接收程序可以隐式告诉发送程序它已停止读取的标准机制。你有没有想过如果

    cat bigfile | head -n5
    

    实际上读取了整个大文件?不,它不会,因为cat 会在head 退出后立即检索SIGPIPE 信号(从标准输入读取5 行后)。值得欣赏的重要一点:cat 被设计为实际上响应SIGPIPE(这是一个重要的工程决策;)):它停止读取文件并退出。其他程序被设计为忽略SIGPIPE(故意自行处理这种情况——这在网络应用程序中很常见)。

    如果您在控制过程中保持管道的读取端打开,则会禁用所描述的机制。 dmesg 将无法注意到 grep 已退出。

    但是,您的示例实际上并不是一个好示例。 grep hda 将读取 整个 输入。 dmesg 是先退出的进程。

    【讨论】:

      【解决方案3】:

      来自docs

      The p1.stdout.close() call after starting the p2 is important in order for p1 to receive a SIGPIPE if p2 exits before p1.
      

      SIGPIPE 信号被发送到一个进程,当它试图写入一个没有连接到另一端的进程的管道时。当使用stdin=p1.stdout创建p2时,有两个进程连接到管道p1.stdout:python父进程和p2。即使 p2 过早关闭,父进程仍在运行,因此不会发送 SIGPIPE 信号。 p1.stdout.close() 在父/调用者进程中关闭 p1.stdout,从而使 dmesg 作为打开该文件描述符的唯一进程。

      换句话说,如果没有p1.stdout.close() 那么:

      • p1.stdout 在父进程中保持打开状态。如果 p2 退出(即有 没有人阅读 p1.stdout),p1 不会知道没有人阅读 p1.stdout 并将继续写信给p1.stdout,直到 对应的操作系统管道缓冲区已满。
      • 如果 p2 过早退出,p1.stdout 仍将是 在父进程中打开,因此不会生成 SIGPIPE。

      【讨论】: