当前位置: 首页 > 科技观察

自带的打印功能居然报错?

时间:2023-03-18 12:40:38 科技观察

本文转载自微信公众号“crossoverJie”,作者crossoverJie。转载本文请联系跨界姐公众号。前言最近,我用Python写了几个简单的脚本来处理一些数据。因为只是一个简单的功能,所以我直接使用print来打印日志。任务运行时偶尔会出现一些异常:因为我在不同的地方打印了日志,所以每次报错的地方都不一样,导致程序运行结果很奇怪;有时这段代码没有运行,下次运行时可能是另一段代码没有触发。虽然当时注意到了Brokenpipe这个关键异常,但是并没有太在意,因为代码中有些地方是发送http请求的。一直以为是网络IO有问题,根本没想过print这个最基本的打印功能??.直到这个问题再次出现,我才认真看这个异常。我仔细看了看print,不就是IO操作吗?会不会是自带的打印功能有问题?但是我在本地和测试环境跑了无数次,都没有用。发现异常;于是就去运维弄了线上的操作方法。原来运维为了方便维护大家提交的脚本任务,维护了一个统一的脚本。在这个脚本中,使用:cmd='python/xxx/test.py'os.popen(cmd)来触发任务,这也是与我本地开发环境的唯一区别。popen原理为此,我在开发环境模拟了一个异常:test.py:importtimeif__name__=='__main__':time.sleep(20)print'1000'*1024task.py:importosimporttimeif__name__=='__main__':start=int(time.time())cmd='pythontest.py'os.popen(cmd)end=int(time.time())print'end****{}s'.format(end-start)运行:pythontask.py等待20s必然会出现这个异常:Traceback(mostrecentcalllast):File"test.py",line4,inprint'1000'*1024IOError:[Errno32]Brokenpipe为什么会出现这个异常?首先我们要了解os.popen(command[,mode[,bufsize]])这个函数是如何工作的。根据官方文档的解释,该函数会执行fork一个子进程来执行command命令,同时将子进程的标准输出通过管道连接到父进程;即此方法返回的文件描述符。这里画个图可以更好的理解原理:这里的使用场景并没有获取popen()的返回值,所以command的执行本质上是异步的;也就是说,当task.py执行时,会自动关闭读端的管道。如图所示,关闭后子进程会向管道输出print'1000'*1024。由于这里输出的内容很多,管道的缓冲区会一下子被填满;所以写端会收到SIGPIPE信号,导致Brokenpipe异常。从维基百科中,我们也可以看到这个异常的一些条件:也提到了SIGPIPE信号。解决方案既然知道了问题的原因,那么解决就相对简单了。主要有以下几种解决方法:使用read()函数读取管道中的数据,全部读取完后再关闭。如果您不需要子进程中的输出,您还可以将命令的标准输出重定向到/dev/null。也可以使用Python3的subprocess.Popen模块运行。这里使用第一种方案进行演示:importosimporttimeif__name__=='__main__':start=int(time.time())cmd='pythontest.py'withos.popen(cmd)asp:printp.read()end=int(time.time())print'end****{}s'.format(end-start)运行task.py后不会抛出异常,还会打印命令的输出。在线修复的时候我没有采用这个方案。为了方便查看日志,我还是使用标准的日志框架将日志输出到es,这样在kibana中可以统一查看。由于日志框架没有使用管道,自然不存在这个问题。虽然解决了更多的内容问题,但是还是涉及到一些我们平时不注意的知识点。这次我们一起回顾一下。首先是父子进程的内容。这个在c/c++/python中比较常见,在java/golang中会更多的直接使用多线程和协程。比如本次提到的Python中的os.popen()创建子进程。既然是子进程,就必须与父进程进行通信,以达到协同工作的目的。很容易想象,父子进程可以通过上述管道(匿名管道)进行通信。还是以刚才的Python程序为例,运行task.py后会生成两个进程:分别进入两个程序的/proc/pid/fd目录,可以看到两个进程打开的文件描述符。父进程:子进程:可以看到子进程的标准输出与父进程相关联,也就是popen()返回的文件描述符。这里的012分别对应一个进程的stdin(标准输入)/stdout(标准输出)/stderr(标准错误)。另外需要注意的是,当我们在父进程中打开文件描述符时,子进程也会继承;比如在task.py中新增一段代码:x=open("1.txt","w")查看文件描述符后,会发现父子进程都会有这个文件:但是在相反,父进程不会在子进程中打开文件。这应该很容易理解。总结一些基础知识在排查一些奇怪的问题时尤为重要,比如本次涉及到的父子进程的管道通信。最后总结一下:os.popen()函数是异步执行的。如果需要获取子进程的输出,需要自己调用read()函数。父子进程通过匿名管道进行通信。当读端关闭时,写端输出到达管道最大缓冲区时会收到SIGPIPE信号,从而抛出Brokenpipe异常。子进程继承父进程的文件描述符。