自带的 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 ...
随机推荐
- P1008_三连击(JAVA语言)
/* * 题目描述 将1,2,⋯,9共9个数分成3组, 分别组成3个三位数,且使这3个三位数构成1:2:3的比例,试求出所有满足条件的3个三位数. 输入输出格式 输入格式: 木有输入 输出格式: 若 ...
- Ingress-nginx工作原理和实践
本文记录/分享 目前项目的 K8s 部署结构和请求追踪改造方案 这个图算是一个通用的前后端分离的 k8s 部署结构: Nginx Ingress 负责暴露服务(nginx前端静态资源服务), 根据十二 ...
- C语言之文件操作
C语言之文件操作 在本节我们将会讲解有关文件的读写操作: 纲要: 一些需要掌握的知识点 文件名 文件类型 数据流 文件缓冲区 文件指针 与文件操作相关的一些函数 文件的打开及关闭 文件的顺序读写 文件 ...
- 【分布式】SpringCloud(3)--Eureka服务注册与发现
1.Eureka概述 1.1.什么是Eureka Eureka是Netflix的一个子模块.基于REST的服务,用于定位服务,以实现云端中间层服务发现和故障转移. 只需要使用服务的标识符,就可以访问到 ...
- java面试-生产环境服务器变慢,谈谈你的诊断思路
1.uptime:查询linux系统负载 11:16:16 系统当前时间 up 64 days, 19:23 从上次启动开始系统运行的时间3 users 连接数量,同一用户多个连接的时候算多个load ...
- 2020 OO 第一单元总结 表达式求导
title: BUAA-OO 第一单元总结 date: 2020-03-19 20:53:41 tags: OO categories: 学习 OO第一单元通过三次递进式的作业让我们实现表达式求导,在 ...
- 解决Deepin-wine-wechat-arch 文件不能正常发送
1 问题描述 系统Manjaro,使用Deepin-wine微信,目前最新的版本为2.9.5.56-1,发送图片时,会发送不成功,经常在转圈,对于发送其他文件会出现红色感叹号发送失败. 2 解决方法 ...
- ES6 第一天
study in bilibili: es6视频教程(无废话版) 关键字: let 代码块内起作用,不会代码提升 const 常量 ES6结构赋值 作用:解析解构,然后自动赋值,简化程序员代码编写: ...
- 6. Mybatis resultMap
resultMap 元素是MyBatis中最重要最强大的元素.它就是让你远离90%的需要从结果集中取出数据的JDBC代码的那东西,而且在一些情形下允许你做一些JDBC不支持的事情.事实上,编写相似于对 ...
- 集群,lvs负载均衡的四种工作模式
集群 集群的三种分类以及用途 负载均衡: 分配流量(调度器),提升速度 高可用: 关键性业务 高性能: 开发算法,天气预报,国家安全 负载均衡的集群 lvs(适用于大规模) haproxy(适用于中型 ...