Defect(1) 管道死锁

本文通过一个Python子进程示例,介绍了如何避免因子进程输出导致的父进程等待子进程结束时产生的死锁问题,并提供了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:林海枫
网址:http://blog.youkuaiyun.com/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.    运行失败,标准输出和错误输出交错进行,无法确定谁先谁后,以及次数。

 

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

 

<think>好的,我现在需要处理用户关于软件缺陷相关概念的查询。用户提到“softwaredefect ORbug trackingOR defectmanagement insoftware development”,所以核心是缺陷管理、缺陷跟踪这些内容。首先,我得回忆软件开发生命周期中缺陷管理的基本流程,可能包括缺陷的定义、分类、跟踪工具、解决流程等。 接下来,用户之前提供的引用中有提到码中的潜在问题,比如空指针解引用、数组溢,这可能缺陷的类型相关。引用1中的内容可以用于说明缺陷检测的重要性。引用2提到敏捷开发中的测试变化,这可能涉及到敏捷环境下的缺陷管理方法。引用3是关于项目管理的术语,可能缺陷处理的优先级排期有关,但暂时相关性不大,可能需要少用。用户需要的是概念解决方案,所以应该分点回答。首先定义软件缺陷,然后介绍缺陷跟踪系统,比如常见的工具如JIRA、Bugzilla。接着讨论缺陷的生命周期,从发现到修复的各个阶段。然后可以提到缺陷分类,如严重性、优先级,以及如何管理这些分类。最后,可能涉及预防缺陷的方法,如码审查、自动化测试等。在生成相关问题时,需要覆盖用户可能关心的其他方面,比如工具比较、最佳实践、敏捷中的缺陷管理、预防措施等。确保问题能够引导用户深入探讨不同方面。检查是否需要引用之前的引用内容。引用1提到码中的潜在问题,可以用在缺陷检测部分。引用2关于敏捷开发中的测试变化,可以关联到敏捷环境下的缺陷管理挑战。引用3可能不太相关,但如果有涉及项目排期的部分,可以提到滞后优先级,不过可能不太必要。最后,确保使用正确的中文术语,LaTeX格式正确,行内公式用$...$,独立公式用$$...$$,但这里可能数学公式较少,更多是概念性的内容。结构清晰,分点回答,并生成相关问题。</think>### 软件缺陷与缺陷管理 在软件开发中,**软件缺陷(Software Defect)**指码中不符合预期行为需求的问题。缺陷管理(Defect Management)涵盖从发现、记录、跟踪到修复的全流程,通常依赖工具(如JIRA、Bugzilla)实现系统化协作[^1]。以下是核心概念与解决方案: #### 1. 缺陷分类与优先级 - **严重性(Severity)**:缺陷对系统的影响程度,例如: - *致命*:导致系统崩溃(如空指针解引用)[^1] - *次要*:界面排版问题 - **优先级(Priority)**:修复紧急程度,可能独立于严重性。 #### 2. 缺陷跟踪流程 ```mermaid graph LR A[缺陷报告] --> B(分类与分配) B --> C[开发修复] C --> D[测试验证] D --> E{通过?} E -->|是| F[关闭] E -->|否| C ``` 工具(如GitLab Issues)通常支持自动化状态流转与通知。 #### 3. 缺陷预防 - **码审查**:通过同行评审提前发现逻辑错误。 - **自动化测试**:单元测试、集成测试覆盖关键路径,减少回归缺陷[^2]。 - **静态码分析**:使用工具(如SonarQube)检测潜在码问题(如数组越界)[^1]。 #### 4. 敏捷开发中的缺陷管理 敏捷团队需缩短反馈周期,例如: - **每日站会**同步缺陷状态; - **缺陷看板**可视化处理进度[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值