Defect 管道死锁
Posted 海枫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Defect 管道死锁相关的知识,希望对你有一定的参考价值。
作者:林海枫
网址:http://blog.csdn.net/linyt/archive/2008/09/26/2983960.aspx
[注]:本文版权有作者拥有,可以自由转载,但不能部分转载;请匆用于任何商业用途
随着工作压力和项目时间的逼近,我想会有很多程序员写的出代总会有这样或那样的bug,更有甚者可以隠藏几个月或一年,到爆发之时损失却是非常惨重。诚然,有代码之处,就有 bug藏身之地,这句话也不无道理。对于开发人员来说,自己代码出现bug,无异于当头棒喝。由于工作的缘由,最近在改系统代码的bug,并且要做一些代码质量检测工作。今天工作非常忙,却很有成功感,那是因为我发现隠藏了很久的bug。本来我是解决另外一个bug的,在验证的过程中,由于当时版本有点小问题,造成某个程运行失败,并产生大量的错误信息。正是这些错误信息,让我找到这个bug的窝。
为了说明我遇到的问题 ,我用一个例子来说明。下面是出现bug的代码(python 语言),我工作的代码不是运行cat test,而是其它。
- #!/usr/bin/python
- import os
- import subprocess
- def main():
- proc = subprocess.Popen("cat test",
- shell = True,
- cwd = '.',
- stdin = subprocess.PIPE,
- stdout = subprocess.PIPE,
- stderr = subprocess.PIPE);
- retcode = proc.wait();
- print 'ret code = ', retcode
- print 'loginfo: ', proc.stdout.read(512)
- if retcode != 0:
- print 'errmsg: ', proc.stderr.read()
- if __name__ == '__main__':
- main()
相信有linux经验的朋友很快可以把这个小程序看得明明白白(显然我工作的程序不会这么简单的,main 除了产生一个子进程来运行一个命令外,还做其它事情,在这里略过代码)。
上面代码是这样的:
产生一个子进程,用来执行”cat test”命令,父进程等待它结束。由于要获得子进程的输出,故在创建子进程时给子进程序的stdout, stderr 指定为PIPE。
Bug现身:
我在测试时发现该软件包在运行过程中卡住了,即不是运行出错,也没结束。工作要写报告,故要分析这样的结果是不是我代码所造成的(我当想肯定不是我的问题 ,我写的代码和这个软件包的代码是风牛马不相及的事情)。代码相当长,不过以我的经验,很快跟踪到它的调用链,感觉告诉我,非常可能在wait函数出现问题,但不知是为什么,这是我始料不及的。因为我认为上面的代码写得还不错,相当优美。接着google一下,接下来我开始傻了眼,一行为“dead lock on wait”的E文吸引了我。究其原因,原来是这样的:
被创建子进程在开始运行时,它的stdout, stderr已被重定向到管道里面了。Linux里的管道都会有一定的容量,当道管满了写执行write操作就会block,直到可以写为止。在上面的代码里,父进程创建子进程后,没有对它们通信的管道进行read操,而是调用 wait 等待子进程结束。如果子进程把输出写满了管道,那它会非常希望父进程尽快把它清理掉;而父进程此时在希望子进程尽快结束。这样一来,我们在OS上学到的死锁终于现身了。
要测试上面的代码相当容易,创建一个比较大的文件test就可以了。一运行程序就卡在那了。
发现了问题,也就找到了解决之道。在等待子进程结束的同时,也要把管道的内容清掉(read)。Popen类有个方法为communicate来获得子进程的标准输出和错误输出。尽管这个方法可以解燃眉之急,却不可用之。这是因这个方法一次性获得子进程的输出,并放到内存里面,如果子进程产生大量的输出或错误信息时,那么程序依然存在很多问题。那么最好是比较及时并且分次来获得子进程的输出是比较好的办法。这样可以免除占用大量的内存空间。于是我选用select来处理子进程的输出。并且要达到以下目标:
1. 及时清量管道的内容。
由于父进程必须等待子进程结束后才可以着手做其它事情,在这过程中实现及时清理管道是很 easy的事情。 Linux有很多的方法来实现。
2. 在清理管道,必须要悉知子进程的结束。
能及时地读取到子进程的输出,那读到输出EOF就表明子进程已运行结束了。
根据这样的思想,我采用 select去处理子进程的输出,更改后的代码如下:
- #!/usr/bin/python
- import os
- import subprocess
- import select
- def main():
- proc = subprocess.Popen("cat test",
- shell = True,
- cwd = '.',
- stdin = subprocess.PIPE,
- stdout = subprocess.PIPE,
- stderr = subprocess.PIPE)
- first_out = ''
- first_err = ''
- rest_out = ''
- rest_err = ''
- READ_LEN = 1024
- select_rfds = [ proc.stdout, proc.stderr]
- while len(select_rfds) > 0:
- (rfds, wfds, efds) = select.select(select_rfds, [],[])
- if proc.stdout in rfds:
- if len(first_out) == 0:
- first_out = proc.stdout.read(READ_LEN)
- rest_out = first_out
- else:
- rest_out = proc.stdout.read(READ_LEN)
- if len(rest_out) == 0:
- select_rfds.remove(proc.stdout)
- if proc.stderr in rfds:
- if len(first_err) == 0:
- first_err = proc.stderr.read(READ_LEN)
- rest_err = first_err
- else:
- rest_err = proc.stderr.read(READ_LEN)
- if len(rest_err) == 0:
- select_rfds.remove(proc.stderr)
- retcode = proc.wait()
- print 'ret code = ', retcode
- print 'loginfo: ', first_out
- if retcode != 0:
- print 'errmsg: ', first_err
- if __name__ == '__main__':
- main()
由于程序输出和错误信息有交错出现的可能,故select同时查看它们是否有数据可读。若有则处理。当子进程结束时,标准输出和错误输出都会关闭管道,此后父进程向管道读数据时,会读到EOF(文件结束符)。这样父进程就获知了子进程的结束。并调用wait获得它的返回值和使免受僵死之灾。
小结:
不要对别人的程序作任何假设。回看第一个代码,会发现作者认为子进程的输出不会超出512个字节。 这种假设是错误的,因为不能确定会传什么样的命令参数给subprocess.Popen类。就算是这一点可以确定,也不能确定输出的字节数。因此写代码时要对调用程序作一般化的思考:
1. 运行成功,只有标准输出,但数据量不能确定
2. 运行失败,只有错误输出,数据量可大可小
3. 运行失败,标准输出和错误输出交错进行,无法确定谁先谁后,以及次数。
记住:不能对程序作任何假设。正如不能假定打开文件成功,内容分配成功一样,必须对函数的返回值作相当的处理。
以上是关于Defect 管道死锁的主要内容,如果未能解决你的问题,请参考以下文章