乐趣区

关于python:Python-中的-ospopen-函数-与-Pipe-管道的坑

前言

最近用 Python 写了几个简略的脚本来解决一些数据,因为只是简略性能所以我就间接应用 print 来打印日志。

工作运行时偶然会呈现一些异样:

因为我在不同中央都有打印日志,导致每次报错的中央都不太一样,从而导致程序运行后果十分诡异;有时候是这段代码没有运行,下一次就可能是另外一段代码没有触发。

虽说过后有留神到 Broken pipe 这个要害异样,但没有特地在意,因为代码中也有一些发送 http 申请的中央,始终认为是网络 IO 呈现了问题,压根没往 print 这个最根本的打印函数上思考🤔。

直到这个问题重复呈现我才认真看了这个异样,定睛一看 print 不也是 IO 操作嘛,难道真的是自带的 print 函数都出问题了?


但在本地、测试环境我运行无数次也没能发现异常;于是我找运维拿到了线上的运行形式。

原来为了不便保护大家提交上来的脚本工作,运维本人有保护一个对立的脚本,在这个脚本中应用:

cmd = 'python /xxx/test.py'
os.popen(cmd)

来触发工作,这也是与我在本地、开发环境的惟一区别。

popen 原理

为此我在开发环境模拟出了异样:

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 信号。

解决办法

既然晓得了问题起因,那解决起来就比较简单了,次要有以下几个计划:

  1. 应用 read() 函数读取管道中的数据,全副读取之后再敞开。
  2. 如果不须要子过程中的输入时,也能够将 command 的规范输入重定向到 /dev/null
  3. 也能够应用 Python3subprocess.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")

之后查看文件描述符时会发现父子过程都会有这个文件:

但相同的,子过程中关上的文件父过程是不会有的,这个应该很容易了解。

总结

一些基础知识在排查一些诡异问题时显得尤为重要,比方本次波及到的父子过程的管道通信,最初来总结一下:

  1. os.popen() 函数是异步执行的,如果须要拿到子过程的输入,须要自行调用 read() 函数。
  2. 父子过程是通过匿名管道进行通信的,当读取端敞开时,写入端输入达到管道最大缓存时会收到 SIGPIPE 信号,从而抛出 Broken pipe 异样。
  3. 子过程会继承父过程的文件描述符。

你的点赞与分享是对我最大的反对

退出移动版