前言

最近用 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. POJ1562_Oil Deposits(JAVA语言)

    思路:bfs.水题,标记下计数就完了. Oil Deposits Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 22928 ...

  2. MySQL中explain语句的使用

    一.概述 在 MySQL 中,我们可以使用慢查询日志或者 show processlist 命令等方式定位到执行耗时较长的 SQL 语句,在这之后我们可以通过 EXPLAIN或者 DESC 命令获取 ...

  3. Android Studio 待看博文

    •前言 学习过程中找到的一些好的博文,有些可能当时就看完了并解决了我的问题,有些可能需要好几天的事件才能消化. 特此记录,方便查阅. •CSDN 给新人的一些基础常识 TextView的文字长度测量及 ...

  4. 第一次OOP作业-Blog总结

    前言 第一次作业一共八道题,此次作业也是这三次作业中最接近面向过程程序设计的题目集,整体难度偏低,总耗时1.5h,主要的知识点在熟悉Java的语法上,整体题目的逻辑非常清晰简单,但最后一个判断三角形类 ...

  5. ClickHouse性能优化?试试物化视图

    一.前言 ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS):目前我们使用CH作为实时数仓用于统计分析,在做性能优化的时候使用了 物化视图 这一特性作为优化手段,本文主 ...

  6. python基础(六):列表的使用(下)

    列表排序的三种方式 sort()方法:原地修改列表的排序方法 注 1:" 默认是升序" ,参数 reverse=True,表示将列表降序. 注 2:" 原地修改列表&qu ...

  7. MySQL提升笔记(1):MySQL逻辑架构

    深入学习MySQL,从概览MySQL逻辑架构开始. 首先来看一下MySQL的逻辑架构图: MySQL逻辑架构大概可以分为三层: 客户端:最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服 ...

  8. PAT A1052 Linked List Sorting

    题意:给出N个结点的地址address.数据域data以及指针域next,然后给出链表的首地址,要求把在这个链表上的结点按data值从小到大输出.样例解释:按照输入,这条链表是这样的(结点格式为[ad ...

  9. 自动化kolla-ansible部署ubuntu20.04+openstack-victoria之ceph部署-07

    自动化kolla-ansible部署ubuntu20.04+openstack-victoria之ceph部署-07 欢迎加QQ群:1026880196 进行交流学习 近期我发现网上有人转载或者复制原 ...

  10. JUC包的线程池详解

    为什么要使用线程池 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程. 控制并发的数量.并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃.(主要原因) 可以对线程做统一管理. JUC ...