自带的 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 ...
随机推荐
- 有必要了解的大数据知识(一) Hadoop
前言 之前工作中,有接触到大数据的需求,虽然当时我们体系有专门的大数据部门,但是由于当时我们中台重构,整个体系的开发量巨大,共用一个大数据部门,人手已经忙不过来,没法办,为了赶时间,我自己负责的系统的 ...
- Java 并发编程 Executor 框架
本文部分摘自<Java 并发编程的艺术> Excutor 框架 1. 两级调度模型 在 HotSpot VM 的线程模型中,Java 线程被一对一映射为本地操作系统线程.在上层,Java ...
- KMP字符串匹配学习笔记
部分内容引自皎月半洒花的博客 模式串匹配问题模型给定一个需要处理的文本串和一个需要在文本串中搜索的模式串,查询在该文本串中,给出的模式串的出现有无.次数.位置等.算法思想每次失配之后不会从头开始枚举, ...
- [BFS]翻币问题
翻币问题 Description 有N个硬币(6<=N<=20000)全部正面朝上排成一排,每次将其中5个硬币翻过来放在原位置,直到最后全部硬币翻成反面朝上为止.试编程找出步数最少的翻法, ...
- c 结构体内存对齐详解
0x00简介 首先要知道结构体的对齐规制 1.第一个成员在结构体变量偏移量为0的地址处 2.其他成员变量对齐到某个数字的整数倍的地址处 对齐数=编辑器默认的一个对齐数与该成员大小的较小值 vs中默认的 ...
- Element源码:项目初始化和webpack配置
0x00.项目初始化 由于整个过程像素级 copy element,所以将不使用vue-cli初始化项目. 创建项目 新建一个空的文件夹,使用npm init 来初始化项目,并安装vue模块. 修改目 ...
- windows2003安装php ,mysql,fastgui
在上一章中,windows2003的iis搭建已经完成,但是我们现在用的多的也包含php,该如何让Windows2003成功使用php文件呢? windows2003需要先行安装vc9运行库才能与fa ...
- 五、python学习-面向对象
1.面对对象程序开发基础(oop) 面对对象:高内聚 低耦合 面向过程: 优点:效率高,执行速度快 缺点:维护性,移植性差,表达不出一类的语义 面向对象: 优点:可读性,可移植性,可维护性高 缺点:执 ...
- 给出N个字符串恰好由三位字母(大小写)组成,再给出M个查询字符串,问每个查询字符串在N个字符中出现的次数。
1 #include<cstdio> 2 const int maxn = 100; 3 char S[maxn][5], temp[5]; 4 int hashTable[52 * 52 ...
- Spring Boot demo系列(六):HTTPS
2021.2.24 更新 1 概述 本文演示了如何给Spring Boot应用加上HTTPS的过程. 2 证书 虽然证书能自己生成,使用JDK自带的keytool即可,但是生产环境是不可能使用自己生成 ...