前言

最近用 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. 子进程会继承父进程的文件描述符。

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

自带的 print 函数居然会报错?的更多相关文章

  1. MyEclipse上有main函数类运行报错:Editor does not contain a main type

    MyEclipse下有main函数类运行报错:Editor does not contain a main type 出现这种问题的原因是,该java文件所在的包没有被MyEclipse认定为源码包. ...

  2. php通过JavaBridge调用Java类库和不带包的自定义java类成功 但是调用带包的自定义Java类报错,该怎么解决

    php通过JavaBridge调用Java类库和不带包的自定义java类成功 但是调用带包的自定义Java类报错,Class.forName("com.mysql.jdbc.Driver&q ...

  3. MyEclipse上有main函数类运行报错:Editor does not contain a

    MyEclipse下有main函数类运行报错:Editor does not contain a main type?出现这种问题的原因是,该java文件   MyEclipse下有main函数类运行 ...

  4. round函数解决oracle报错"OCI-22053: 溢出错误"的问题

    继上次公司网站报错除数为0的问题,这次又来报错溢出错误,还是同一条语句!搜索网上的解决方法,发现问题描述和解决方法如下: Oracle 数值数据类型最多可存储 38 个字节的精度.当将 Oracle ...

  5. decode函数解决oracle报错"除数为0"的问题

    公司的网站在运行的时候突然报错打不开了,打开一看发现报了一个错:ORA-01476:除数为0. 网上一搜发现还是挺多人遇到这个问题的,解决办法就是用decode函数. decode是oracle内置的 ...

  6. shell函数中eof报错(warning: here-document at line 9 delimited by end-of-file (wanted `EOF'))

    在shell编写函数时,函数中有eof和EOF,如果是在sublime编写按照格式tab缩进会有以下报错 解决办法: 取消函数中的tab缩进,在运行即可

  7. open函数新建文件报错

    报错原因很多,我这里只写我遇到的: 给的路径或者文件名中包含了这些字符的:/\:*?"><| 都不行,我说的是Windows平台下的.

  8. 高可用安装k8s1.13.0 --不能带cavisor、不能加cni ,带上这两个总是报错,kubelet无法启动

    高可用安装k8s1.13.0 --不能带cavisor,总是报错,kubelet无法启动

  9. c++函数模板作为类的成员函数,编译报错LNK2019的解决方法

    为了使某个类的成员函数能对不同的参数进行相同的处理,需要用到函数模板,即template<typename T> void Function(). 编译时报错LNK2019 解决方法: 1 ...

随机推荐

  1. P1008_三连击(JAVA语言)

    /*  * 题目描述 将1,2,⋯,9共9个数分成3组, 分别组成3个三位数,且使这3个三位数构成1:2:3的比例,试求出所有满足条件的3个三位数. 输入输出格式 输入格式: 木有输入 输出格式: 若 ...

  2. net start MongoDB 服务没有响应控制功能。 请键入 NET HELPMSG 2186 以获得更多的帮助。

    我在 Windows 电脑上修改了 mongo.cfg 文件后 执行 net start mongodb 的时候,命令行出现了这个报错. 这个报错,有几种情况可以进行排查 1. 你的 mongod 命 ...

  3. [组合][DP]luogu P3643 [APIO2016]划艇

    题面 https://www.luogu.com.cn/problem/P3643 对于一个序列,第i项可取的值在{0}∪[ai,bi]之间,求使序列非零部分单调递增的方案数 分析 设 $f[i][j ...

  4. pycharm在debug时总是报UnicodeDecodeError

    1,原文链接 解决pycharm run 正常 debug 报 UnicodeDecodeError 错误的问题 2,解决方法 首先尝试 如果上面还不行

  5. JPEG解码——(6)IDCT逆离散余弦变换

    本篇是该系列的第六篇,承接上篇IZigZag变换,介绍接下来的一个步骤--逆离散余弦变换,即逆零偏置前的一个步骤. 该步骤比较偏理论,其业务是对IZigZag变换后的数据,再进一步的处理,使其恢复DC ...

  6. 前端使用bcrypt对密码加密,服务器对密码进行校验

    以前为了防止前端密码安全问题,都是对密码进行md5(password + salt). 有些也会用别的加密方式,但还是会存在撞库,彩虹表等破解常规密码. 因此使用bcrypt加密是一个不错的选择,因为 ...

  7. Tomcat详解系列(3) - 源码分析准备和分析入口

    Tomcat - 源码分析准备和分析入口 上文我们介绍了Tomcat的架构设计,接下来我们便可以下载源码以及寻找源码入口了.@pdai 源代码下载和编译 首先是去官网下载Tomcat的源代码和二进制安 ...

  8. 关于生产环境改用G1垃圾收集器的思考

    背景 由于我们的业务量非常大,响应延迟要求高.目前沿用的老的ParNew+CMS已经不能支撑业务的需求.平均一台机器在1个月内有1次秒级别的stop the world.对系统来说是个巨大的隐患.所以 ...

  9. 【工程应用一】 多目标多角度的快速模板匹配算法(基于NCC,效果无限接近Halcon中........)

    愿意写代码的人一般都不太愿意去写文章,因为代码方面的艺术和文字中的美学往往很难兼得,两者都兼得的人通常都已经被西方极乐世界所收罗,我也是只喜欢写代码,让那些字母组成美妙的歌曲,然后自我沉浸在其中自得其 ...

  10. 一键部署etcd集群管理脚本

    一.编写脚本 1 #!/bin/sh 2 # 安装 3 # ./run.sh etcd03 etcd01=http://192.168.2.44:2380,etcd02=http://192.168. ...