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,而是其它。

 

  1.   #!/usr/bin/python
  2.   import os
  3.   import subprocess
  4.   def main():
  5.       proc = subprocess.Popen("cat test",
  6.       shell = True,
  7.       cwd = '.',
  8.       stdin = subprocess.PIPE,
  9.       stdout = subprocess.PIPE,
  10.       stderr = subprocess.PIPE);
  11.       retcode = proc.wait();
  12.       print 'ret code = ', retcode
  13.       print 'loginfo: ', proc.stdout.read(512)
  14.       if retcode != 0:
  15.           print 'errmsg: ', proc.stderr.read()
  16.   if __name__ == '__main__':
  17.       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去处理子进程的输出,更改后的代码如下:

 

  1.  #!/usr/bin/python
  2.  import os
  3.  import subprocess
  4.  import select
  5.  def main():
  6.      proc = subprocess.Popen("cat test",
  7.                              shell = True,
  8.                              cwd = '.',
  9.                              stdin = subprocess.PIPE,
  10.                              stdout = subprocess.PIPE,
  11.                              stderr = subprocess.PIPE)
  12.      first_out = ''
  13.      first_err = ''
  14.      rest_out = ''
  15.      rest_err = ''
  16.      READ_LEN = 1024
  17.      select_rfds = [ proc.stdout, proc.stderr]
  18.      while len(select_rfds) > 0:
  19.          (rfds, wfds, efds) = select.select(select_rfds, [],[])
  20.          if proc.stdout in rfds:
  21.              if len(first_out) == 0:
  22.                  first_out = proc.stdout.read(READ_LEN)
  23.                  rest_out = first_out
  24.              else:
  25.                  rest_out = proc.stdout.read(READ_LEN)
  26.              if len(rest_out) == 0:
  27.                  select_rfds.remove(proc.stdout)
  28.          if proc.stderr in rfds:
  29.              if len(first_err) == 0:
  30.                  first_err = proc.stderr.read(READ_LEN)
  31.                  rest_err = first_err
  32.              else:
  33.                  rest_err = proc.stderr.read(READ_LEN)
  34.              if len(rest_err) == 0:
  35.                  select_rfds.remove(proc.stderr)
  36.      retcode = proc.wait()
  37.      print 'ret code = ', retcode
  38.      print 'loginfo: ', first_out
  39.      if retcode != 0:
  40.      print 'errmsg: ', first_err
  41.  if __name__ == '__main__':
  42.      main()

由于程序输出和错误信息有交错出现的可能,故select同时查看它们是否有数据可读。若有则处理。当子进程结束时,标准输出和错误输出都会关闭管道,此后父进程向管道读数据时,会读到EOF(文件结束符)。这样父进程就获知了子进程的结束。并调用wait获得它的返回值和使免受僵死之灾。

 

小结:

不要对别人的程序作任何假设。回看第一个代码,会发现作者认为子进程的输出不会超出512个字节。 这种假设是错误的,因为不能确定会传什么样的命令参数给subprocess.Popen类。就算是这一点可以确定,也不能确定输出的字节数。因此写代码时要对调用程序作一般化的思考:

1.    运行成功,只有标准输出,但数据量不能确定

2.    运行失败,只有错误输出,数据量可大可小

3.    运行失败,标准输出和错误输出交错进行,无法确定谁先谁后,以及次数。

 

记住:不能对程序作任何假设。正如不能假定打开文件成功,内容分配成功一样,必须对函数的返回值作相当的处理。

 

以上是关于Defect 管道死锁的主要内容,如果未能解决你的问题,请参考以下文章

如何避免并行命名管道的死锁?

有人可以解释管道缓冲区死锁吗?

使用命名管道时如何防止死锁?

死锁,线程协作(同步,阻塞队列,Condition,管道流)

CreateProcess cmd.exe 读/写管道死锁

Golang并发(Go程管道)