解决办法
既然知道了问题原因,那解决起来就比较简单了,主要有以下几个方案:
- 使用
read()
函数读取管道中的数据,全部读取之后再关闭。
- 如果不需要子进程中的输出时,也可以将
command
的标准输出重定向到/dev/null
。
- 也可以使用
Python3
的subprocess.Popen
模块来运行。
这里使用第一种方案进行演示:
import os import time if __name__ == '__main__': start = int(time.time()) cmd = 'python test.py' with os.popen(cmd) as p: print p.read() end = int(time.time()) print 'end****{}s'.format(end-start)
运行 task.py
之后不会再抛异常,同时也将 command
的输出打印出来。
线上修复时我没有采用这个方案,为了方便查看日志,还是使用标准的日志框架将日志输出到了 es 中,方便统一在 kibana
中进行查看。
由于日志框架并没有使用到管道,所以自然也不会有这个问题。
更多内容
问题虽然是解决了,其中还是涉及到了一些咱们平时不太注意的知识点,这次我们就来一起回顾一下。
首先是父子进程的内容,这个在 c/c++/python
中比较常见,在 Java/golang
中直接使用多线程、协程会更多一些。
比如这次提到的 Python
中的 os.popen()
就是创建了一个子进程,既然是子进程那肯定是需要和父进程进行通信才能达到协同工作的目的。
很容易想到,父子进程之间可以通过上文提到的管道(匿名管道)来进行通信。
还是以刚才的 Python 程序为例,当运行 task.py 后会生成两个进程:
分别进入这两个程序的 /proc/pid/fd
目录可以看到这两个进程所打开的文件描述符。
父进程:
子进程:
可以看到子进程的标准输出与父进程关联,也就是 popen()
所返回的那个文件描述符。
这里的
0 1 2
分别对应一个进程的stdin
(标准输入)/stdout
(标准输出)/stderr
(标准错误)。
还有一点需要注意的是,当我们在父进程中打开的文件描述符,子进程也会继承过去;
比如在 task.py
中新增一段代码:
x = open("1.txt", "w")
之后查看文件描述符时会发现父子进程都会有这个文件:
但相反的,子进程中打开的文件父进程是不会有的,这个应该很容易理解。
总结
一些基础知识在排查一些诡异问题时显得尤为重要,比如本次涉及到的父子进程的管道通信,最后来总结一下:
os.popen()
函数是异步执行的,如果需要拿到子进程的输出,需要自行调用read()
函数。
- 父子进程是通过匿名管道进行通信的,当读取端关闭时,写入端输出到达管道最大缓存时会收到
SIGPIPE
信号,从而抛出Broken pipe
异常。
- 子进程会继承父进程的文件描述符。