Python subprocess.Popen二次运行时readlines阻塞挂起问题
# @file Test.py # 需求1 def requirement1(): ''' 需求1: 服务端需支持以下命令行参数: -a / --address <ip地址> - 指定监听的IP地址(默认:监听所有网卡/默认接口) -p / --port <端口> - 指定监听的端口(默认:12345) ''' log = "需求1:\n" args1 = ["", "-a 127.0.0.1", "--address 127.0.0.1"] args2 = ["", "-p 7777", "--port 7777"] processName = f"..\exe\server.exe" stdout.write(f" [•] {processName} {args1[2]} {args2[2]}\n") someProcess = None someProcess = subprocess.Popen([processName, args1[2], args2[2]], stdout = subprocess.PIPE) sleep(1) # 等Windows给进程分配资源,系统响应慢 # 进程启动失败判定 if someProcess is None: log += f" [F] {processName} {args1[2]} {args2[2]}\n" return log lines = someProcess.stdout.readlines() for line in lines: line = cleanLine(line) stdout.write(f"{line}\n") if "ERROR" in line: log += f" [F] {processName} {args1[2]} {args2[2]}\n" elif "ready" in line: log += f" [P] {processName} {args1[2]} {args2[2]}\n" else: continue someProcess.kill() return log
问题背景
当前正在对自研服务端程序做单元测试,测试逻辑如下:
- 客户端启动后获取输入,向服务端发送字符串,最长等待1秒接收服务端返回的相同字符串,超时直接关闭客户端
- 服务端启动后等待客户端连接,原样返回接收到的字符串,持续监听连接,直到用户在命令提示符窗口按
Ctrl+C终止程序
测试server.exe时出现异常现象:Windows10系统重启后首次执行someProcess = subprocess.Popen([processName, args1[2], args2[2]], stdout = subprocess.PIPE)启动子进程时,lines = someProcess.stdout.readlines()可以正常读取子进程标准输出,后续脚本逻辑全部运行正常。但如果在运行Test.py的命令提示符窗口通过Ctrl+C键盘中断强制终止脚本,后续每次运行脚本,执行到lines = someProcess.stdout.readlines()语句时都会永久挂起。
已尝试的排查与修复方案
- 重启机器后改用
with subprocess.Popen([processName, args1[2], args2[2]], stdout = subprocess.PIPE) as someProcess:上下文管理器写法包裹Popen调用,原本认为上下文管理器可以在脚本以任意方式退出时自动清理所有资源,但实际强制退出后再次运行脚本仍然出现相同挂起问题,上下文管理器没有达到预期的资源清理效果。 - 通过任务管理器、ProcessExplorer查找并手动终止所有Popen启动的残留进程后,
readlines()调用仍然挂起,必须重启整个操作系统才能恢复正常运行。 - 通过
netstat -ano排查端口占用情况,没有发现脚本启动的僵尸进程占用对应监听端口;且如果端口被占用,服务端会抛出WSA error:10048错误,该错误信息本应被someProcess.stdout.readlines()读取并输出到Test.py的标准输出,但实际readlines()直接挂起,没有任何返回。
故障流程推测
- 首次运行Test.py,所有逻辑运行正常;
- 再次运行Test.py时,通过
Ctrl+C强制中断退出; - 之后每次运行Test.py,脚本执行到
someProcess.stdout.readlines()时会无限阻塞等待读取数据,但实际管道中无数据可读,怀疑强制退出后子进程相关的流、管道资源损坏,没有被系统正确释放。
待解决问题
如何保证每次调用subprocess.Popen()时都能返回可安全交互的进程对象,不受Test.py之前的运行状态、终止方式的影响?
问题根因:Windows下控制台程序的
Ctrl+C信号默认会发送给绑定到同一个控制台的所有进程,而非仅发给Python脚本进程。按下Ctrl+C时,Python脚本和启动的server.exe子进程会同时收到终止信号,二者退出顺序不可控,极易出现子进程未正确关闭标准输出句柄、父进程持有的管道句柄被内核标记为异常状态但未触发EOF的情况,后续新启动的进程如果复用同一控制台的句柄表,就会出现读管道永久阻塞的问题。
按以下步骤修改即可彻底解决:
1. 修正Popen参数配置
- 首先修正参数传递错误:当前写法中
"-a 127.0.0.1"会被当成一个完整参数传给server.exe,需要把参数名和参数值拆成独立列表元素 - 添加进程创建标志隔离信号,核心:给子进程创建新的进程组,隔离Ctrl+C信号
- 合并标准错误输出到标准输出管道,避免单独的stderr管道触发阻塞
cmd = [processName, "--address", "127.0.0.1", "--port", "7777"] someProcess = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP )
2. 替换无界阻塞的readlines()
readlines()会一直读到管道被关闭才会返回,只要子进程不主动关闭stdout就会永久阻塞。改用逐行读取+超时判断,拿到需要的启动标识就停止读取:
import time deadline = time.time() + 5 # 最多等待5秒,足够服务端启动输出ready信息 output = [] while time.time() < deadline: line = someProcess.stdout.readline() if not line: break # 读到EOF,子进程已退出 line = cleanLine(line) stdout.write(f"{line}\n") output.append(line) if "ready" in line or "ERROR" in line: break # 拿到目标标记即可停止读取,无需等待管道关闭
3. 注册强制清理逻辑
不管脚本是正常结束还是被Ctrl+C中断,都主动终止子进程、关闭所有管道句柄,不依赖系统自动回收资源:
import signal import atexit def cleanup(signum=None, frame=None): global someProcess if someProcess and someProcess.poll() is None: someProcess.terminate() # 最多等3秒让子进程正常退出,超时则强杀 try: someProcess.wait(timeout=3) except subprocess.TimeoutExpired: someProcess.kill() if someProcess: someProcess.stdout.close() if signum == signal.SIGINT: exit(0) # 注册Ctrl+C信号处理 signal.signal(signal.SIGINT, cleanup) # 注册脚本正常退出时的清理逻辑 atexit.register(cleanup)
4. 启动前兜底清理残留
每次启动新的server.exe之前,先强制杀掉所有同名残留进程,避免之前异常退出的进程占用资源:
subprocess.run( ["taskkill", "/F", "/IM", "server.exe"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True ) time.sleep(0.5) # 等待系统释放相关资源
完成以上修改后,无论脚本之前以何种方式终止,新启动的Popen对象都可以正常交互,不会再出现永久挂起的问题。
内容的提问来源于stack exchange,提问作者Adonay Pichardo

