Python脚本中通过subprocess调用Ollama命令导致僵尸进程:使用Popen和wait的异步处理方案

文章导读
僵尸进程的根源不在Ollama命令本身,而是subprocess.Popen启动子进程后,父进程一直没调用wait或communicate去回收子进程退出状态。如果你的脚本里用subprocess.run或call,通常不会遇到这个问题,因为它们在等待子进程结束时会自动回收。只有当Popen启动子进程后放任不管,同时父进程继续运行,子进程结束后才会留成僵尸。
📋 目录
  1. 先确认僵尸进程怎么来的
  2. 最小修复:调用wait或communicate
  3. 异步处理:不想阻塞主流程
  4. 验证是否已清除僵尸进程
A A

僵尸进程的根源不在Ollama命令本身,而是subprocess.Popen启动子进程后,父进程一直没调用wait或communicate去回收子进程退出状态。如果你的脚本里用subprocess.run或call,通常不会遇到这个问题,因为它们在等待子进程结束时会自动回收。只有当Popen启动子进程后放任不管,同时父进程继续运行,子进程结束后才会留成僵尸。

核心判断:用Popen调用Ollama命令后,父进程必须主动wait或communicate,否则子进程状态会残留成僵尸进程。异步处理就是把“等待回收”的过程挪到后台,不阻塞主流程。建议检查脚本中对Popen对象的生命周期管理,并用ps命令验证Z状态是否消失。

先确认僵尸进程怎么来的

你可以用下面命令查看当前是否有僵尸进程,以及它的父进程是谁:

ps -eo stat,pid,ppid,cmd | grep 'Z'

如果STAT为Z,说明是僵尸。再看PPID是否指向你的Python脚本进程。如果指向脚本,说明脚本还没退出,也没对子进程做wait。子进程本身已经终止,只是资源没被回收。

最小修复:调用wait或communicate

如果调用Ollama命令后,脚本本来就要等结果,不要只用Popen就结束,至少要补上wait:

import subprocess
p = subprocess.Popen(['ollama', 'show', 'my-model'])
p.wait()
print('退出码:', p.returncode)

如果需要拿到标准输出和错误输出,直接改用communicate:

Python脚本中通过subprocess调用Ollama命令导致僵尸进程:使用Popen和wait的异步处理方案
import subprocess
p = subprocess.Popen(['ollama', 'show', 'my-model'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
print('退出码:', p.returncode)

communicate内部会等待进程结束并回收状态,比wait再多做输出收集。这两种方式都会清掉僵尸进程。

异步处理:不想阻塞主流程

脚本在等Ollama命令时还要处理其他任务,就需要把wait放到后台。有两种常见做法。

方案A:线程池里执行wait

把“启动+等待”封装成一个函数,用ThreadPoolExecutor丢给线程:

import subprocess
from concurrent.futures import ThreadPoolExecutor

def run_ollama(cmd):
    p = subprocess.Popen(cmd)
    p.wait()
    return p.returncode

with ThreadPoolExecutor(max_workers=2) as executor:
    future = executor.submit(run_ollama, ['ollama', 'show', 'my-model'])
    # 这里可以继续做其他事情
    code = future.result()
    print('ollama命令退出码:', code)

这样可以同时发起多个调用,但要注意线程安全。适合脚本里已经有线程池或需要并发的场景。

方案B:用asyncio的事件循环

如果你的脚本已经在用asyncio,可以直接用asyncio.create_subprocess_exec,它底层也是Popen,但由事件循环管理wait,不会阻塞主循环:

Python脚本中通过subprocess调用Ollama命令导致僵尸进程:使用Popen和wait的异步处理方案
import asyncio

async def run_ollama_async(cmd):
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()
    return proc.returncode, stdout, stderr

async def main():
    code, out, err = await run_ollama_async(['ollama', 'show', 'my-model'])
    print(code, out.decode())

asyncio.run(main())

注意:create_subprocess_exec和communicate都是异步的,等待期间事件循环可以继续执行其他协程。这是更“异步”的方案,适合已有asyncio框架的脚本。

验证是否已清除僵尸进程

修复后,再次运行上面的ps命令,应该看不到新的Z状态。也可以用Python的os.waitpid配合WNOHANG做探测:

import os, subprocess
p = subprocess.Popen(['ollama', 'show', 'my-model'])
# 非阻塞探测
pid, status = os.waitpid(p.pid, os.WNOHANG)
print(pid, status)

如果子进程还没结束,pid为0。如果已结束,pid就是子进程PID,waitpid会回收状态。注意waitpid调用后不能再调wait,否则会报错。常规做法还是直接使用communicate或wait最稳妥。

边界:如果父进程异常退出,僵尸进程会被init进程收养并自动清理,所以不一定要处理到极致。但如果脚本是长期驻留的服务,不回收就会累积僵尸进程。另外,使用Popen时尽量避免加shell=True,同时注意在finally中回收,防止异常时漏掉wait。