最近用 Python
写了几个简单的脚原本处理一些数据,由于只是简单功能因此我就直接使用 print
来打印日志。python
任务运行时偶尔会出现一些异常:c++
由于我在不一样地方都有打印日志,致使每次报错的地方都不太同样,从而致使程序运行结果很是诡异;有时候是这段代码没有运行,下一次就多是另一段代码没有触发。golang
虽然说当时有注意到 Broken pipe
这个关键异常,但没有特别在乎,由于代码中也有一些发送 http
请求的地方,一直觉得是网络 IO
出现了问题,压根没往 print
这个最基本的打印函数上思考🤔。缓存
直到这个问题反复出现我才认真看了这个异常,定睛一看 print
不也是 IO
操做嘛,难道真的是自带的 print
函数都出问题了?网络
但在本地、测试环境我运行无数次也没能发现异常;因而我找运维拿到了线上的运行方式。多线程
原来为了方便维护你们提交上来的脚本任务,运维本身有维护一个统一的脚本,在这个脚本中使用:框架
cmd = 'python /xxx/test.py' os.popen(cmd)
来触发任务,这也是与我在本地、开发环境的惟一区别。运维
为此我在开发环境模拟出了异常:异步
test.py:函数
import time if __name__ == '__main__': time.sleep(20) print '1000'*1024
task.py:
import os import time if __name__ == '__main__': start = int(time.time()) cmd = 'python test.py' os.popen(cmd) end = int(time.time()) print 'end****{}s'.format(end-start)
运行:
python task.py
等待 20s 必然会复现这个异常:
Traceback (most recent call last): File "test.py", line 4, in <module> print '1000'*1024 IOError: [Errno 32] Broken pipe
为何会出现这个异常呢?
首先得了解 os.popen(command[, mode[, bufsize]])
这个函数的运行原理。
根据官方文档的解释,该函数会执行 fork
一个子进程执行 command
这个命令,同时将子进程的标准输出经过管道链接到父进程;
也就该方法返回的文件描述符。
这里画个图能更好地理解其中的原理:
在这里的使用场景中并无获取 popen()
的返回值,因此 command
的执行本质上是异步的;
也就是说当 task.py
执行完毕后会自动关闭读取端的管道。
如图所示,关闭以后子进程会向 pipe
中输出 print '1000'*1024
,因为这里输出的内容较多会一会儿填满管道的缓冲区;
因而写入端会收到 SIGPIPE
信号,从而致使 Broken pipe
的异常。
从维基百科中咱们也能够看出这个异常产生的一些条件:
其中也提到了 SIGPIPE
信号。
既然知道了问题缘由,那解决起来就比较简单了,主要有如下几个方案:
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
异常。你的点赞与分享是对我最大的支持