1.1 踩坑案例
踩坑的程序是个常驻的Agent类管理进程, 包括但不限于以下类型的任务在执行:python
- a. 多线程的网络通讯包处理
- 和控制Master节点交互
- 有固定Listen端口
- b. 按期做业任务, 经过subprocess.Pipe执行shell命令
- c. etc
发现坑的过程颇有意思:shell
- a.重启Agent发现Port被占用了
- => 马上想到可能进程没被杀死, 是否是中止脚本出问题
- => 排除发现不是, Agent进程确实死亡了
- => 经过
netstat -tanop|grep port_number
发现端口确实有人占用
- => 调试环境, 直接杀掉占用进程了之, 错失首次发现问题的机会
- b.问题在一段时间后重现, 重启后Port仍是被占用
- 定位问题出如今一个叫作xxxxxx.sh的脚本, 该脚本占用了Agent使用的端口
- => 奇了怪了, 一个xxx.sh脚本使用这个奇葩Port干啥(大于60000的Port, 有兴趣的砖友能够想下为何Agent默认使用6W+的端口)
- => review该脚本并无进行端口监听的代码
- 一拍脑壳, c.进程共享了父进程资源了
- => 溯源该脚本,发现确实是Agent启动的任务中的脚本之一
- => 问题基本定位, 该脚本属于Agent调用的脚本
- => 该Agent继承了Agent原来的资源FD, 也就是这个port
- => 虽然该脚本因为超时被动触发了terminate机制, 但terminate并无干掉这个子进程
- => 该脚本进程的父进程(ppid) 被重置为了1
- d.问题出在脚本进程超时kill逻辑
1.2 填坑解法
经过代码review, 找到shell具体执行的库代码以下:bash
self._subpro = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=_signal_handle
)
# 重点是shell=True !
把上述代码改成:网络
self._subpro = subprocess.Popen(
cmd.split(), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, preexec_fn=_signal_handle
)
# 重点是去掉了shell=True
1.3 坑位分析
Agent会在一个新建立的threading线程中执行这段代码, 若是线程执行时间超时(xx seconds), 会调用 self._subpro.terminate()
终止该脚本.多线程
表面正常:线程
- 启用新线程执行该脚本
- 若是出现问题,执行超时防止hang住其余任务执行调用terminate杀死进程
深层问题:调试
- Python 2.7.x中subprocess.Pipe 若是shell=True, 会默认把相关的pid设置为shell(sh/bash/etc)自己(执行命令的shell父进程), 并不是执行cmd任务的那个进程
- 子进程因为会复制父进程的opened FD表, 致使即便被杀死, 依然保留了拥有这个Listened Port FD
这样虽然杀死了shell进程(未必死亡, 可能进入defunct状态), 但实际的执行进程确活着. 因而1.1
中的坑就被结实的踩上了.code
1.4 坑后扩展
1.4.1 扩展知识
本节扩展知识包括二个部分:htm
- Linux系统中, 子进程通常会继承父进程的哪些信息
- Agent这种常驻进程选择>60000端口的意义
扩展知识留到下篇末尾讲述, 感兴趣的能够自行搜索
1.4.1 技术关键字
- Linux系统进程
- Linux随机端口选择
- 程序多线程执行
- Shell执行
1.5 填坑总结
- 子进程会继承父进程的资源信息
若是只kill某进程的父进程, 集成了父进程资源的子进程会继续占用父进程的资源不释放, 包括但不限于
- listened port
- opened fd
- etc
Python Popen使用上, shell的bool状态决定了进程kill的逻辑, 须要根据场景选择使用方式
建议你们也看一下这篇文章的姊妹篇, 此篇是子孙进程没法 kill/杀死的终极解法 [Python 踩坑之旅进程篇其三pgid是个什么鬼 (子进程\子孙进程没法kill 退出的解法)] (http://www.javashuo.com/article/p-nxquorsz-ck.html)