Python subprocess 卡住不返回:PIPE 缓冲写满导致的死锁与三种规避法

举报
yd_291384559 发表于 2026/11/04 16:30:41 2026/11/04
【摘要】 子进程把 PIPE 写满后,父子进程会互相等待。本文用一个最小复现说明 subprocess 卡死的成因,给出 communicate、重定向落盘、流式读取三种写法,并指出 timeout、双管道顺序读、shell 参数三个相邻的坑。

现象:手敲秒回,脚本里卡死

把一条命令搬进 Python 脚本后,进程既不输出也不退出,只能 kill 掉;同一条命令在终端里执行却一秒钟就返回。这类问题九成不是子进程慢,而是父子进程在管道上互相等待,属于死锁而不是性能问题。

根因:管道不是无限缓冲

用 PIPE 接管子进程的 stdout 或 stderr 时,内核为每一端分配一个有上限的环形缓冲区,Linux 上常见 64 KiB。子进程把缓冲写满后 write 调用阻塞,父进程如果只调用 wait 而不读管道,就会等子进程结束;子进程又在等父进程腾出缓冲。两边的等待条件互为前提,谁也不会先动。

最小复现

让子进程输出十万行,父进程只等退出码、不读管道:

p = subprocess.Popen(["seq","1","100000"], stdout=subprocess.PIPE)

接着 p.wait(),进程就停在那里。把 seq 换成任何日志量大的构建、备份或转码命令,现象完全一致。决定因素是输出总量是否超过管道容量,与命令本身快慢无关,所以本地跑得快不代表线上不出问题。

怎么定案

先用 py-spy dump 看父进程栈:停在 waitpid 或 os.wait 基本可以确认方向。再看子进程的 /proc/PID/wchan,若停在 pipe_write,说明写端被堵。用 lsof 分别看父子进程的 fd,能看到两端指向同一个 pipe inode,这是最直接的证据。顺手看一下当时输出了多少字节,通常刚好卡在容量边界附近。

三种写法

最省事的是交给 run 并开启捕获,内部用 communicate 并发读两个管道,不会写满:

r = subprocess.run(["cmd"], capture_output=True, text=True, timeout=300)

输出量很大时不要把它全留在内存,直接把 stdout 指到文件句柄,父进程只等退出码,日志留给磁盘:

subprocess.run(["cmd"], stdout=open("out.log","wb"), stderr=subprocess.STDOUT)

需要边跑边看日志时,用线程或 asyncio 持续消费管道,读完再 wait。核心约束只有一条:只要用了 PIPE,就必须有一个读者在持续消费。

几个相邻的坑

第一,timeout 只在 communicate 路径上可靠;直接调 wait 再传超时,超时后子进程可能变成孤儿,还得自己收尸。第二,把 stdout 和 stderr 分成两个 PIPE 又单线程顺序读,同样会死锁,应先读完一个再读另一个,或者直接合并成 STDOUT。第三,shell 参数只是多套一层 shell,管道容量限制一点没变,反而多一个要清理的进程,能用列表参数就别开它。

结论

判断标准很简单:用了 PIPE,就必须有人在读。拿不准输出量时,优先 communicate 或直接落盘,不要在 wait 上赌对方的日志有多少。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。