Python subprocess shell=True导致子进程无法正确终止的排查与解决

蓝鲸
蓝鲸
发布时间:2026-10-12| 3人浏览过
原创

本文深入分析Python中使用subprocess模块时设置shell=True导致的子进程无法正确终止及资源泄漏问题。当shell=True时,实际执行的是Shell解释器而非命令本身,导致直接杀死子进程ID无法释放其继承的文件描述符(如监听端口)。通过对比shell=False的使用方式,并解析进程继承机制,提供了避免此类坑位的具体代码修改建议及排查思路。

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. 其他任务

发现该问题的过程颇具趣味性:

a. 重启Agent后发现端口被占用

=> 首先想到可能是进程未被彻底杀死,怀疑停止脚本存在问题

=> 经过排查排除此可能性,确认Agent进程确实已死亡

=> 通过执行 netstat -tanop|grep port_number 命令发现端口确实仍被占用

=> 在调试环境中直接杀死了占用该端口的进程,从而错失了首次发现问题的机会

b. 问题在一段时间后重现,重启后端口依然被占用

最终定位问题出现在一个名为xxxxxx.sh的脚本中,该脚本占用了Agent使用的端口

=> 感到疑惑,一个xxx.sh脚本为何会使用这个特殊的端口(大于60000的端口,有兴趣的读者可以思考为什么Agent默认使用6W+的端口)

=> 审查该脚本,并未发现进行端口监听的代码

灵光一现,推测c. 进程共享了父进程的资源

=> 追溯该脚本,发现它确实是Agent启动的任务中的脚本之一

=> 问题基本定位,该脚本属于Agent调用的子脚本

=> 该Agent继承了原Agent的资源文件描述符(FD),即该端口对应的FD

=> 虽然该脚本由于超时被动触发了terminate机制,但terminate并未真正杀死该子进程

=> 该脚本进程的父进程ID(ppid)被重置为了1

d. 问题****出在脚本进程超时kill逻辑

1.2 填坑解法

通过代码审查,找到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() 终止该脚本。

表面正常:

启用新线程执行该脚本

如果出现问题,执行超时防止其他任务挂起,调用terminate杀死进程

深层问题:

在Python 2.7.x中,subprocess.Pipe如果设置shell=True,会默认将相关的pid设置为Shell(sh/bash/etc)本身(即执行命令的Shell父进程),而非执行cmd任务的那个实际进程

子进程由于会复制父进程打开的文件描述符表(FD表),导致即使被杀死,依然保留了拥有该Listen 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 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。