深入分析在Python模块顶层运行的代码引起的一个Bug

作者:Desmond Chen 时间:2021-06-29 01:26:29 

然后我们在Interactive Python prompt中测试了一下:


>>> import subprocess
 >>> subprocess.check_call("false")
 0

而在其他机器运行相同的代码时, 却正确的抛出了错误:


>>> subprocess.check_call("false")
 Traceback (most recent call last):
  File "", line 1, in
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 542, in check_call
   raise CalledProcessError(retcode, cmd)
 subprocess.CalledProcessError: Command 'false' returned non-zero exit status 1

看来是subprecess误以为子进程成功的退出了导致的原因.

深入分析

第一眼看上去, 这一问题应该是Python自身或操作系统引起的. 这到底是怎么发生的? 于是我的同事查看了subprocess的wait()方法:


def wait(self):
 """Wait for child process to terminate. Returns returncode attribute."""
 while self.returncode is None:
  try:
   pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
  except OSError as e:
   if e.errno != errno.ECHILD:
    raise
   # This happens if SIGCLD is set to be ignored or waiting
   # for child processes has otherwise been disabled for our
   # process. This child is dead, we can't get the status.
   pid = self.pid
   sts = 0
  # Check the pid and loop as waitpid has been known to return
  # 0 even without WNOHANG in odd situations. issue14396.
  if pid == self.pid:
   self._handle_exitstatus(sts)
 return self.returncode

可见, 如果os.waitpid的ECHILD检测失败, 那么错误就不会被抛出. 通常, 当一个进程结束后, 系统会继续记录其信息, 直到母进程调用wait()方法. 在此期间, 这一进程就叫"zombie". 如果子进程不存在, 那么我们就无法得知其是否成功还是失败了.

以上代码还能解决另外一个问题: Python默认认为子进程成功退出. 大多数情况下, 这一假设是没问题的. 但当一个进程明确表明忽略子进程的SIGCHLD时, waitpid()将永远是成功的.

回到原来的代码中

我们是不是在我们的程序中明确设置忽略SIGCHLD? 不太可能, 因为我们使用了大量的子进程, 但只有极少数情况下才出现同样的问题. 再使用git grep后, 我们发现只有在一段独立代码中, 我们忽略了SIGCHLD. 但这一代吗根本就不是程序的一部分, 只是引用了一下.

一星期后

一星期后, 这一错误又再一次发生. 并且通过简单的调试, 在debugger中重现了该错误.

经过一些测试, 我们确定了正是由于程序忽略了SIGCHLD才引起的这一bug. 但这是怎么发生的呢?

我们查看了那段独立代码, 其中有一段:

signal.signal(signal.SIGCHLD, signal.SIG_IGN)
我们是不是无意间import了这段代码到程序中? 结果显示我们的猜测是正确的. 当import了这段代码后, 由于以上语句是在这一module的顶层, 而不是在一个function中, 导致了它的运行, 忽略了SIGCHLD, 从而导致了子进程错误没有被抛出!

总结

这一bug的发生, 给了我们两个教训. 第一是, 在debug检查时, 应该从新的代码到老的代码, 再到Python Library. 因为新代码发生错误的几率大于老代码, 而python library中发生错误的几率更小.

第二是, 不要将可能会引起副作用的代码写在module顶层, 而应当写到functuon中. 因为如果该module被import, 那么在顶层的代码就会运行, 导致各种不可知的事件发生.

标签:Python,模块顶层
0
投稿

猜你喜欢

  • 详细讲解Python中的文件I/O操作

    2022-01-01 19:04:53
  • Python集合的增删改查操作

    2023-09-30 00:48:18
  • python中py文件与pyc文件相互转换的方法实例

    2021-03-29 13:15:27
  • python中使用zip函数出现<zip object at 0x02A9E418>错误的原因

    2021-02-24 02:37:14
  • Django执行python manage.py makemigrations报错的解决方案分享

    2021-05-23 06:58:05
  • ORACLE 数据库RMAN备份恢复

    2009-04-24 12:23:00
  • 960 时代的终结

    2011-01-11 19:24:00
  • 安装docker-compose的两种最简方法

    2022-10-03 21:39:44
  • Python多线程下载文件的方法

    2021-12-26 05:21:32
  • Python 实用技巧之利用Shell通配符做字符串匹配

    2021-07-18 22:57:16
  • python读取指定字节长度的文本方法

    2022-04-17 10:42:45
  • Python中super关键字用法实例分析

    2023-12-08 06:11:46
  • Python tkinter库图形绘制例子分享

    2023-05-29 13:19:51
  • python GUI库图形界面开发之PyQt5开发环境配置与基础使用

    2023-11-16 04:45:22
  • Python中的面向对象编程详解(上)

    2021-10-12 14:33:45
  • Python run()函数和start()函数的比较和差别介绍

    2022-07-04 18:38:47
  • python如何爬取个性签名

    2021-03-29 03:34:04
  • JavaScript缓动动画函数的封装方法

    2023-08-07 10:48:26
  • 从Web查询数据库之PHP与MySQL篇

    2009-09-19 16:58:00
  • Golang依赖注入工具digo的使用详解

    2023-08-27 13:00:43
  • asp之家 网络编程 m.aspxhome.com