• 企业400电话
  • 网络优化推广
  • AI电话机器人
  • 呼叫中心
  • 全 部 栏 目

    网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    解决python subprocess参数shell=True踩到的坑
    POST TIME:2021-10-18 14:12

    0x01 问题现象

    写的程序使用subprocess创建子进程运行其他程序,判断其他程序运行完后进行处理。

    在subprocess使用了shell=True,判断用户程序退出的代码如下

    while self.proc.poll() is None:
        do_something

    判断子进程是否运行结束,程序在子进程运行结束后,代码未向下继续运行,而是卡在了这个循环中。

    0x02 原因分析

    百度后对shell参数的解释如下:

    shell=True参数会让subprocess.Popen接受字符串类型的变量作为命令,并调用shell去执行这个字符串,当shell=False是,subprocess.Popen只接受数组变量作为命令,并将数组的第一个元素作为命令,剩下的全部作为该命令的参数。

    通过查看服务器进程可以看到,仍然有进程存在,进程如下

    为shell中运行的程序,由此可以得出,shell=true时,子进程在运行完后,shell并没有退出,而是卡在shell命令中,可由进程看到。

    补充:Python踩坑之旅其一杀不死的Shell子进程

    1.1 踩坑案例

    踩坑的程序是个常驻的Agent类管理进程, 包括但不限于如下类型的任务在执行:

    a. 多线程的网络通信包处理

    和控制Master节点交互

    有固定Listen端口

    b. 定期作业任务, 通过subprocess.Pipe执行shell命令

    c. etc

    发现坑的过程很有意思:

    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具体执行的库代码如下:

    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中的坑就被结实的踩上了.

    1.4 坑后扩展

    1.4.1 扩展知识

    本节扩展知识包括二个部分:

    Linux系统中, 子进程一般会继承父进程的哪些信息

    Agent这种常驻进程选择>60000端口的意义

    扩展知识留到下篇末尾讲述, 感兴趣的可以自行搜索

    1.4.1 技术关键字

    Linux系统进程

    Linux随机端口选择

    程序多线程执行

    Shell执行

    1.5 填坑总结

    1.子进程会继承父进程的资源信息

    2.如果只kill某进程的父进程, 集成了父进程资源的子进程会继续占用父进程的资源不释放, 包括但不限于

    listened port

    opened fd

    etc

    3.Python Popen使用上, shell的bool状态决定了进程kill的逻辑, 需要根据场景选择使用方式

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。

    您可能感兴趣的文章:
    • Python实现系统交互(subprocess)
    • Python实现subprocess执行外部命令
    • Python中使用subprocess库创建附加进程
    • Python中Subprocess的不同函数解析
    • python中subprocess实例用法及知识点详解
    上一篇:Python中判断subprocess调起的shell命令是否结束
    下一篇:python中dlib库的详细安装方法
  • 相关文章
  • 

    关于我们 | 付款方式 | 荣誉资质 | 业务提交 | 代理合作


    © 2016-2020 巨人网络通讯

    时间:9:00-21:00 (节假日不休)

    地址:江苏信息产业基地11号楼四层

    《增值电信业务经营许可证》 苏B2-20120278

    X

    截屏,微信识别二维码

    微信号:veteran88

    (点击微信号复制,添加好友)

     打开微信