众所周知,head
命令用于过滤标准输入前面 10 行(或由用户指定数量)的内容。即便上游命令的输出很多,且执行费时,但是一旦用管道连接到 head
命令,只要输出指定的行数后,上游命令也结束。下面是一个例子:
grep xxx abc.txt | head
上面的例子中,假设 abc.txt
的内容较长,这样 grep
命令的执行时间比 head
命令长。那么背后发生了什么呢?当 head
命令结束时,管道的读端被关闭,这时 grep
命令再写入管道,内核立刻把 SIGPIPE
信号发给 grep
进程,而 SIGPIPE
默认的行为是终止进程。
Python 中,为了能自己处理管道错误,抛出 OSError
异常,Python 启动时把 SIGPIPE
的处置方式设为“忽略”,那么底层系统调用里 write(2)
就能返回 EPIPE
错误。但是这种做法存在一个问题,子进程会继承信号的处置方式,若信号的处置方式为“默认”或“忽略”,则 exec()
后仍保留。因此,在 Python 使用 subprocess
模块执行上面的命令,我们会见到 grep
命令输出管道错误的信息。
其中一种解决方法是:调用 subprocess
模块前,先把 SIGPIPE
的处置方式恢复为“默认”。但是,在这句代码与调用 subprocess
的 Python 代码之间,无法达到原来 Python 希望达到的效果,即写入管道错误时抛出异常。
有没有两全其美的方法?答案是“有”。方法是给 SIGPIPE
信号设置一个处理函数,且内容为空:
import signal def sigpipe_handler(sig, frame): pass signal.signal(signal.SIGPIPE, sigpipe_handler)
首先,当写入管道失败时,如果 SIGPIPE
处理函数能返回,则底层会返回 EPIPE
错误,而不是 EINTR
错误,因此 Python 层面仍然能抛出一样的 OSError
异常。其次,当子进程进行 exec()
,那么有处理函数的信号的处置方式重置为“默认”,也达到我们想要的效果。