Python subprocess 卡住不返回:PIPE 缓冲写满导致的死锁与三种规避法
现象:手敲秒回,脚本里卡死
把一条命令搬进 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 上赌对方的日志有多少。
- 点赞
- 收藏
- 关注作者
评论(0)