最近发现了一个 python 特有的卡死问题,是通过 python 调用 shell 命令出现的,特此记录一下。

 

1、问题描述

这里我用一个例子来进行说明,并非真实使用场景。

 

1.1、普通 shell 命令执行:

yes yes | echo 'hello'

在 shell 中能够正常结束并输出。

shell调用python 参数传递 python调用shell命令但执行无效_python

 

1.2、python 调用 shell 命令执行:

import os
os.system("yes yes | echo 'hello'")

但在 python 中会卡死,用其他调用函数或者换 subprocess 模块也一样。

shell调用python 参数传递 python调用shell命令但执行无效_执行时间_02

 

1.3、yes 进程

这里也解释一下 yes 进程的作用,一些命令可能需要手动输入 ‘yes’ 来告知选择,但不方便自动化,所以利用 yes 进程不断地输出 ‘yes’ 来自动填。

shell调用python 参数传递 python调用shell命令但执行无效_子进程_03

 

2、问题分析&解决

 

2.1、管道

yes "yes" | $(其他命令)

‘|’其实是管道符号,前后两句命令其实是通过管道通信的,

即前面命令的结果写入管道,而后面命令从管道中读取数据作为输入。

 

管道遵循 FIFO 的使用方式

shell调用python 参数传递 python调用shell命令但执行无效_子进程_04

由于 yes 进程是不断地输出的,命令执行时间为无限(若不从外部中止);

而此时其他命令执行时间短,这时候管道的读端已经关闭,

这时 yes 进程再想写入管道的时候,内核立刻将 SIGPIPE 发送给 yes 进程,SIGPIPE 默认行为是终止进程。

/* write on a pipe with no one to read it */
#define        SIGPIPE        13

也即术语:管道破裂

管道破裂,这个信号通常在进程间通信产生,比如采用 FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到 SIGPIPE 信号.  -- 维基百科

 

2.2、python 问题

那为什么 shell 执行不会卡死,通过 python 调用 shell 命令反而卡死了呢?

On Unix, Python sets SIGPIPE to SIG_IGN on startup, because it prefers to check every write and raise an IOError exception rather than taking SIGPIPE. 

Python 为了自己能处理写入错误,报告异常,启动时就把 SIGPIPE 设置为忽略状态。

但是对于非 python 的类 Unix 中的子进程,是靠 SIGPIPE 进行通信的。

如果不采用 SIGPIPE 的话,就需要自己检查系统调用 write 返回的 EPIPE。

 

2.3、如何解决

import signal
# 恢复为默认状态
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

shell调用python 参数传递 python调用shell命令但执行无效_shell调用python 参数传递_05

 

3、结论

使用 python 调用子进程执行 bash 语句要注意管道破裂问题,为保安全,可以先设置信号为默认状态。