MacOS 11.2下Python 3.8.2的subprocess.poll()调用约四五十次后挂起
问题定位
- 升级MacOS 11.2后,系统内置Python 3.8.2环境下,原本稳定运行4年的测试套件子进程实时打印逻辑,连续运行四五十个单元测试后会无响应挂起
- 根据日志打印判断,挂起位置卡在
p.stdout.readline().decode()调用处:最后一次触发print('xxx')后,再也不会进入退出分支触发print('yyy')
原问题代码
print( '\nrunning: ' + curFilePath.split('/')[-1] + ': ' + str(curVer)) p = subprocess.Popen(fullCMD, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: # 实时打印子进程输出 print( 'xxx' ) nextline = p.stdout.readline().decode() if nextline == '' and p.poll() is not None: print( 'yyy' ) break sys.stdout.write(nextline) sys.stdout.flush() print( 'completed: ' + curFilePath.split('/')[-1] )
挂起核心原因
问题根源不是Python代码本身的逻辑错误,是MacOS 11.2系统升级带来的两个行为变更触发了原代码的边界问题:
- 管道缓冲区行为调整:原代码用阻塞模式读取子进程输出,当子进程输出没有带换行、或者输出内容攒满系统默认管道缓冲区时,
readline()会永久处于阻塞等待状态,哪怕子进程已经执行完毕,也不会返回空字符串触发循环退出判断 - 管道句柄累积:原代码跑完每个子进程后没有显式关闭stdout管道句柄,连续跑四五十个测试后,累积的未关闭管道句柄会触达MacOS用户级别的句柄数量上限,新的管道读取操作会直接阻塞——这也是为什么挂起的节点刚好在跑了几十个测试之后,旧系统版本没有这个问题才让代码稳定运行了4年。
临时可行解决方案
- 最小改动兜底:给
Popen调用增加bufsize=1参数开启行缓冲,减少缓冲区攒满触发的阻塞概率;每次循环结束后显式调用p.stdout.close()释放句柄,避免累积触达系统上限 - 彻底解决阻塞问题:把原来的阻塞式
readline()改成非阻塞轮询逻辑,设置轮询超时,不会因为管道异常永久卡死 - 快速绕过验证:如果不想改代码,可以临时用pyenv安装一个3.9以上版本的Python跑测试套件,系统内置Python在MacOS大版本升级后经常出现底层管道行为的回归问题,第三方独立安装的Python不受系统管道逻辑变更的直接影响
修复后可直接运行的参考代码
import os import fcntl import time import subprocess import sys print( '\nrunning: ' + curFilePath.split('/')[-1] + ': ' + str(curVer)) # 增加行缓冲参数 p = subprocess.Popen( fullCMD, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1 ) # 将stdout设置为非阻塞模式 fd = p.stdout.fileno() flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) while True: print( 'xxx' ) try: nextline = p.stdout.readline().decode() except IOError: time.sleep(0.2) # 子进程还在运行就继续轮询 if p.poll() is None: continue # 子进程退出后读取缓冲区残留内容后退出 try: remain = p.stdout.read().decode() if remain: sys.stdout.write(remain) sys.stdout.flush() except IOError: pass print( 'yyy' ) break if nextline == '' and p.poll() is not None: print( 'yyy' ) break sys.stdout.write(nextline) sys.stdout.flush() # 显式关闭句柄避免累积 p.stdout.close() print( 'completed: ' + curFilePath.split('/')[-1] )
内容的提问来源于stack exchange,提问作者Keith
相关产品推荐
相关产品推荐

