1.1踩坑案例
踩坑的程序是个常驻的代理类管理进程,包括但不限于如下类型的任务在执行:
-
<李>。多线程的网络通信包处理
-
<李>和控制主节点交互李>
<李>有固定听端口李>
李
发现坑的过程很有意思:
-
<李> <>强。重启代理发现港口被占用了强>
-
<李>=比;立刻<强>想到可能进程没被杀的死,是不是停止脚本出问题强> 李>
<李>=比;排除发现不是,代理进程确实死亡了李>
<李>=比;通过<代码> netstat -tanop | grep port_number进行> 代码发现端口确实有人占用李>
<李>=比;调试环境,<强>直接杀掉占用进程了之,错失首次发现问题的机会强> 李>
李
-
<李>定位问题出现在一个叫做xxxxxx。sh的脚本,该脚本占用了代理使用的端口李>
<李>=比;奇了怪了,一个xxx。sh脚本使用这个奇葩港口干啥(大于60000的端口,有兴趣的砖友可以想下为什么代理默认使用6 w +的端口)李>
<李>=比;审查<强>该脚本并没有进行端口监听的代码强> 李>
李
-
<李>=比;溯源该脚本,发现确实是代理启动的任务中的脚本之一李>
<李>=比;问题基本定位,该脚本属于代理调用的脚本李>
<李>=比;该代理继承了代理原来的资源FD,也就是这个港口李>
<李>=比;虽然该脚本由于超时被动触发了终止机制,但终止并没有干掉这个子进程李>
<李>=比;该脚本进程的父进程(ppid)被重置为1了李>
李
1.2填坑解法
通过代码审查,找到壳具体执行的库代码如下:
<代码类="语言python ">自我。_subpro=subprocess.Popen ( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=_signal_handle ) #重点是shell=True ! 代码>
把上述代码改为:
<代码类="语言python ">自我。_subpro=subprocess.Popen ( cmd.split (), stdout=subprocess.PIPE, stderr=子流程。管、preexec_fn=_signal_handle ) #重点是去掉了shell=True 代码>
1.3坑位分析
代理会在一个新创建的螺纹线程中执行这段代码,如果线程执行时间超时(xx秒),会调用<代码> self._subpro.terminate() 代码>终止该脚本。
表面正常:
-
<李>启用新线程执行该脚本李>
<李>如果出现问题,执行超时防止挂住其他任务执行调用终止杀死进程李>
深层问题:
-
<李> Python 2.7.x中子流程。管如果壳=True,会默认把相关的pid设置为shell (sh/bash/等)本身(执行命令的壳父进程),并非执行cmd任务的那个进程李>
<李>子进程由于会复制父进程的打开FD表,导致即使被杀死,依然保留了拥有这个港口FD听李>
这样虽然杀死进了壳程(未必死亡,可能进入已经状态),但实际的执行进程确活着。于是<代码> 代码>中1.1的坑就被结实的踩上了。
1.4坑后扩展
1.4.1扩展知识
本节扩展知识包括二个部分:
-
<李> Linux系统中,子进程一般会继承父进程的哪些信息李>
<李>代理这种常驻进程选择祝辞60000端口的意义李>
扩展知识留到下篇末尾讲述,感兴趣的可以自行搜索
1.4.1技术关键字
-
<李> Linux系统进程李>
<李> Linux随机端口选择李>
<李>程序多线程执行李>
<李>壳执行李>
1.5填坑总结
-
<李>子进程会继承父进程的资源信息李>
<李>
如果只杀死某进程的父进程,集成了父进程资源的子进程会继续占用父进程的资源不释放,包括但不限于
-
<李>听李> 港
<李>打开fd李>
<李>等李>
生命是短暂的。我们使用Python
工号:程序员的梦呓指南