七种武器问题定位篇之实战Python执行命令僵尸卡死

结合《七种武器设计篇之设计是自找的》看,前面设计了一个Python调用命令行的封装,我一般在自测上做很多功夫,所以,幸与不幸,还是测试出了问题。

构造必现环境

在启动mysql的时候,进程会卡主直到TIMEOUT,子进程是僵尸进程defunct,如下:

子进程是僵尸进程,那肯定是父进程没有去waitpid,这个问题不是必然的,如果调用的是其他命令,不会出现,所以,我先把命令精简一下,构造一个必现的环境。
test.py代码:

b.sh代码:

strace工具定位

先strace看下test.py在做啥。

卡在了read函数,从strace跟踪看fd=3是读管道,子进程已经退出了,父进程还在读管道。

proc文件系统定位

从上面的进程列表可以看出, 28459进程是28058的孙子进程,既然28058卡在读管道上,那孙子进程是否会有相应的写管道未CLOSE?我们查看一下:

GDB工具验证

的确,孙子继承了咱们的句柄,我们尝试关闭孙子进程继承的管道看看:

28058进程的终于往下走了,从fork到我们close管道另一端,耗时690s:

这里,等待了很久没有继续,现在终于结束了

问题确认,造成python 执行命令卡死的原因是管道读写句柄继承了,然而继承端并没有关闭管道。

原因分析

我们知道,linux句柄会继承是个好事也是个头疼的问题,很少有人记得加:FD_CLOEXEC或SOCK_CLOEXEC,于是,我在做supervisor模块的时候,特别处理过类似问题,处理办法很暴力,直接在fork后exec前关闭句柄:

既然subprocess.Popen对象参数里面可以设置close_fds标记,那为何不生效?
看看subprocess的源码:

关闭方法和我的一样暴力,但是有一个but参数,会将写入端的管道排除,子进程其实是没有继承其他句柄的,但是,偏偏就在排除的句柄上,出了问题,真是防不胜防。

再一睹Python库communicate代码,看是否和分析吻合:

虽然子进程已经退出了,但是test.py并没有调用wait,因为它被read PIPE卡主了,所以才会出现子进程defunct,而父进程一直不去回收,和现象完全吻合。

解决

解决方法比较简单,问题出在脚本执行的时候,不能用后台运行符号&简单了事,需要用daemon命令替代,daemon命令的源码我之前参考过,里面特别干过close fd的事情,所以,将:

改为:

即可。

或者,Python里面不要用PIPE方式取STDOUT亦可。

通过调试过程记录,可以看出,也就是一些知识和工具的运用,技巧不多,还在于积累。

PDF下载

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">