You need to enable JavaScript to run this app. MacOS 11.2下Python 3.8.2的subprocess.poll()调用约四五十次后挂起
优惠活动
大模型
产品
解决方案
定价
更多

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系统升级带来的两个行为变更触发了原代码的边界问题:

  1. 管道缓冲区行为调整:原代码用阻塞模式读取子进程输出,当子进程输出没有带换行、或者输出内容攒满系统默认管道缓冲区时,readline()会永久处于阻塞等待状态,哪怕子进程已经执行完毕,也不会返回空字符串触发循环退出判断
  2. 管道句柄累积:原代码跑完每个子进程后没有显式关闭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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.05 16:54:04