在KeyboardInterrupt时终止子进程链

4 投票
1 回答
582 浏览
提问于 2025-04-18 14:19

我在写一个脚本来启动本地的 JBoss 实例时遇到了一个奇怪的问题。

我的代码大概是这样的:

with open("/var/run/jboss/jboss.pid", "wb") as f:
    process = subprocess.Popen(["/opt/jboss/bin/standalone.sh", "-b=0.0.0.0"])
    f.write(str(process.pid))

    try:
        process.wait()
    except KeyboardInterrupt:
        process.kill()

这个代码应该挺简单的,就是在程序运行的时候把进程 ID 写到一个文件里,当我按下 KeyboardInterrupt(通常是 Ctrl+C)的时候,就结束这个子进程。

问题是,当我发送结束信号的时候,JBoss 仍然在后台运行,似乎这个信号没有传递到由 standalone.sh 启动的 Java 进程。

我喜欢用 Python 来写系统管理脚本,但像这样的奇怪情况很多,如果我用 Bash 来写,所有东西就会顺利运行™。

我该怎么做才能在收到 KeyboardInterrupt 的时候结束整个子进程树呢?

1 个回答

3

你可以使用psutil这个库来实现这个功能:

import psutil

#..

proc = psutil.Process(process.pid)
for child in proc.children(recursive=True):
    child.kill()

proc.kill()

据我所知,subprocess模块并没有提供获取子进程的API函数,os模块也是如此。


杀掉进程的一个更好的方法可能是这样的:

proc = psutil.Process(process.pid)
procs = proc.children(recursive=True)
procs.append(proc)

for proc in procs:
    proc.terminate()

gone, alive = psutil.wait_procs(procs, timeout=1)
for p in alive:
    p.kill()

这样做可以给进程一个正确结束的机会,当超时时间到时,剩下的进程会被强制结束。


需要注意的是,psutil还提供了一个Popen类,它的接口和subprocess.Popen是一样的,但多了psutil.Process的额外功能。你可能会想直接使用这个,而不是subprocess.Popen。这个方法也更安全,因为psutil会检查进程ID(PID)在进程结束后不会被重复使用,而subprocess则没有这个检查。

撰写回答