自带的 print 函数居然会报错?

前言
最近用 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 信号。
解决办法
既然知道了问题原因,那解决起来就比较简单了,主要有以下几个方案:
- 使用
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异常。 - 子进程会继承父进程的文件描述符。
你的点赞与分享是对我最大的支持
自带的 print 函数居然会报错?的更多相关文章
- MyEclipse上有main函数类运行报错:Editor does not contain a main type
MyEclipse下有main函数类运行报错:Editor does not contain a main type 出现这种问题的原因是,该java文件所在的包没有被MyEclipse认定为源码包. ...
- php通过JavaBridge调用Java类库和不带包的自定义java类成功 但是调用带包的自定义Java类报错,该怎么解决
php通过JavaBridge调用Java类库和不带包的自定义java类成功 但是调用带包的自定义Java类报错,Class.forName("com.mysql.jdbc.Driver&q ...
- MyEclipse上有main函数类运行报错:Editor does not contain a
MyEclipse下有main函数类运行报错:Editor does not contain a main type?出现这种问题的原因是,该java文件 MyEclipse下有main函数类运行 ...
- round函数解决oracle报错"OCI-22053: 溢出错误"的问题
继上次公司网站报错除数为0的问题,这次又来报错溢出错误,还是同一条语句!搜索网上的解决方法,发现问题描述和解决方法如下: Oracle 数值数据类型最多可存储 38 个字节的精度.当将 Oracle ...
- decode函数解决oracle报错"除数为0"的问题
公司的网站在运行的时候突然报错打不开了,打开一看发现报了一个错:ORA-01476:除数为0. 网上一搜发现还是挺多人遇到这个问题的,解决办法就是用decode函数. decode是oracle内置的 ...
- shell函数中eof报错(warning: here-document at line 9 delimited by end-of-file (wanted `EOF'))
在shell编写函数时,函数中有eof和EOF,如果是在sublime编写按照格式tab缩进会有以下报错 解决办法: 取消函数中的tab缩进,在运行即可
- open函数新建文件报错
报错原因很多,我这里只写我遇到的: 给的路径或者文件名中包含了这些字符的:/\:*?"><| 都不行,我说的是Windows平台下的.
- 高可用安装k8s1.13.0 --不能带cavisor、不能加cni ,带上这两个总是报错,kubelet无法启动
高可用安装k8s1.13.0 --不能带cavisor,总是报错,kubelet无法启动
- c++函数模板作为类的成员函数,编译报错LNK2019的解决方法
为了使某个类的成员函数能对不同的参数进行相同的处理,需要用到函数模板,即template<typename T> void Function(). 编译时报错LNK2019 解决方法: 1 ...
随机推荐
- Centos7 Firewall 使用笔记
在 Centos 7 中防火墙由 firewalld 来管理,而不是以前的 iptables. 记录一下常用操作备查 firewall-cmd 操作 firewall-cmd --state 查看防火 ...
- 循环3n加1问题
package 第二章; import java.util.Scanner; //int 32位整数 /* * 猜想:对于任意大于一的自然数n,若n为奇数,则将n变为3n+1,否则变为n的一半 经过 ...
- 【博弈论】组合游戏及SG函数浅析
目录 预备知识 普通的Nim游戏 SG函数 预备知识 公平组合游戏(ICG) 若一个游戏满足: 由两名玩家交替行动: 游戏中任意时刻,合法操作集合只取决于这个局面本身: 若轮到某位选手时,若该选手无合 ...
- 有意思!强大的 SVG 滤镜
想写一篇关于 SVG 滤镜的文章已久,SVG 滤镜的存在,让本来就非常强大的 CSS 如虎添翼.让仅仅使用 CSS/HTML/SVG 创作的效果更上一层楼.题图为袁川老师使用 SVG 滤镜实现的云彩效 ...
- idea报错Selected Java version 11 is not supported by SDK (maximum 8)
解决方案
- Python基础之:Python中的内部对象
目录 简介 内置函数 内置常量 内置类型 逻辑值检测 逻辑值的布尔运算 比较运算 数字类型 整数类型的位运算 整数类型的附加方法 浮点类型的附加方法 迭代器 序列类型 集合类型 映射类型 字典视图对象 ...
- SpringBoot-13 Dubbo实战
SpringBoot-13 Dubbo实战 前提: 已经准备好Dubbo-admin和Zookeeper 前置准备 1.创建项目 显示创建一个Empty Project,创建两个Module---&g ...
- 翻译:《实用的Python编程》09_02_Third_party
目录 | 上一节 (9.1 包) | 下一节 (9.3 版本分发) 9.2 第三方模块 Python 拥有一个包含各种内置模块的大型库(自带电池(batteries included))(译注:&qu ...
- Spring Boot demo系列(一):Hello World
2021.2.24 更新 1 新建工程 打开IDEA选择新建工程并选择Spring Initializer: 可以在Project JDK处选择JDK版本,下一步是选择包名,语言,构建工具以及打包工具 ...
- JUC包的线程池详解
为什么要使用线程池 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程. 控制并发的数量.并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃.(主要原因) 可以对线程做统一管理. JUC ...