Python 里处理 SIGPIPE 异常
因为项目需要,用 Python 定制了一个小工具。具体功能就不说了,以下记录遇到的 SIGPIPE 问题及解决办法。
问题
工具(以下就称为 app)使用时,需要与其他已有工具通过管道连接,比如 xxx 的输出,app 读入、处理并写到标准输出,后面再通过 head 截取前 10 行:
$ xxx -a -b ... | app -c -d ... | head -10
现在遇到的问题是,如果没有最后一步的 head 操作,app 运行正常;加上 head 这一步后,app 会有这样的提示:
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe
同时发现,尝试在 app 程序里加了 try ... except BrokenPipeError:
捕捉并进行异常处理后,仍然有这样的错误提示。
解决
经过搜索后,Python 标准库文档 Note on SIGPIPE 里有这样的描述:
Piping output of your program to tools like head(1) will cause a SIGPIPE signal
to be sent to your process when the receiver of its standard output closes
early. This results in an exception like BrokenPipeError: [Errno 32] Broken
pipe...
归纳下来,整个过程是这样的:
- 当 head 这样的程序运行完成、进程结束时,会关闭输入管道,这会引起 app 里的写操作(app 的输出,对应到 head 的输入)的管道异常 BrokenPipeError:
- 如果不处理这个异常,app 程序崩溃,并结束;
- 如果处理了,app 继续运行(然后肯定是尽快结束进程);
- app 进程结束时,Python 会有一次 stdout 的 flush 操作,因为管道已经关闭,这会再次引起 BrokenPipeError;
文档里提供的解决办法是这样的:
try:
# simulate large output (your code replaces this loop)
for x in range(10000):
print("y")
# flush output here to force SIGPIPE to be triggered
# while inside this try block.
sys.stdout.flush()
except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output
# to devnull to avoid another BrokenPipeError at shutdown
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(1) # Python exits with error code 1 on EPIPE
即在前一次的异常处理时,把 stdout 重定向到 /dev/null
,这样可以避免 app 在进程结束时对已经关闭管道的 flush 操作引起异常。