如何终止使用shell=True启动的Python子进程

431

我正在使用以下命令启动一个子进程:

p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)

然而,当我尝试使用以下方式杀死进程时:

p.terminate()
或者
p.kill()

这个命令在后台一直运行,我想知道如何终止该进程。

需要注意的是,当我使用以下命令运行时:

p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)

当执行p.terminate()时,它会成功地终止。


你的 cmd 长什么样子?它可能包含一个触发多个进程启动的命令。因此,不清楚你指的是哪个进程。 - Robert Siemer
3
没有使用 shell=True 会有很大的区别吗? - Charlie Parker
11个回答

539

使用进程组,以便向组内所有进程发送信号。为此,您应该将会话 ID附加到生成/子进程的父进程上,这在您的情况下是一个 shell。这将使其成为进程组的组长。因此,现在当向进程组领导者发送信号时,它将传递到此组的所有子进程。

以下是代码:

import os
import signal
import subprocess

# The os.setsid() is passed in the argument preexec_fn so
# it's run after the fork() and before  exec() to run the shell.
pro = subprocess.Popen(cmd, stdout=subprocess.PIPE, 
                       shell=True, preexec_fn=os.setsid) 

os.killpg(os.getpgid(pro.pid), signal.SIGTERM)  # Send the signal to all the process groups

8
subprocess.CREATE_NEW_PROCESS_GROUP与此有何关联? - Piotr Dobrogost
15
我们的测试表明setsid!=setpgid,并且os.pgkill仅杀死仍具有相同进程组ID的子进程。已更改进程组的进程不会被杀死,即使它们可能仍具有相同的会话ID... - hwjp
3
CREATE_NEW_PROCESS_GROUP可以在Windows上用来模拟start_new_session=True - jfs
7
我不建议使用os.setsid(),因为它会产生其他影响。其中之一是断开控制TTY并使新进程成为进程组长。请参见http://www.win.tue.nl/~aeb/linux/lk/lk-10.html。 - parasietje
11
你想知道如何在Windows操作系统中实现这个功能?“setsid”命令只能在*nix系统上使用。 - HelloGoodbye
显示剩余12条评论

137
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
p.kill()

p.kill() 会结束 shell 进程,但 cmd 仍在运行。

我发现一个方便的解决方法:

p = subprocess.Popen("exec " + cmd, stdout=subprocess.PIPE, shell=True)

这将导致cmd继承shell进程,而不是启动子进程,该子进程不会被杀死。 p.pid 将成为您的cmd进程的ID。

p.kill() 应该可以工作。

我不知道这对您的管道会产生什么影响。


2
很好的*nix轻量级解决方案,谢谢!适用于Linux,应该也适用于Mac。 - MarSoft
非常好的解决方案。如果您的CMD恰好是其他某个东西的Shell脚本包装器,请在那里也使用exec调用最终二进制文件,以便只有一个子进程。 - Nicolinux
2
这真是太好了。我一直在尝试弄清楚如何在Ubuntu上为每个工作区生成和终止一个子进程。这个答案帮了我很大的忙。希望我可以多次点赞它。 - Sergiy Kolodyazhnyy
2
如果在cmd中使用分号,则此方法无效。 - gnr
13
@speedyrazor - 在Windows10上无法使用。我认为操作系统特定的答案应该明确标注。 - DarkLight
显示剩余2条评论

100
如果你可以使用 psutil,那么这个工具会完美地运作:
import subprocess

import psutil


def kill(proc_pid):
    process = psutil.Process(proc_pid)
    for proc in process.children(recursive=True):
        proc.kill()
    process.kill()


proc = subprocess.Popen(["infinite_app", "param"], shell=True)
try:
    proc.wait(timeout=3)
except subprocess.TimeoutExpired:
    kill(proc.pid)

3
在安装 psutil 时出现错误信息 AttributeError: 'Process' object has no attribute 'get_children' - d33tah
4
我认为 get_children() 应该改成 children()。但是在 Windows 上这样做对我来说没有起作用,进程仍然存在。 - Godsmith
3
@Godsmith - psutil API已更改,你是正确的:children()现在执行了与get_children()相同的操作。如果在Windows上无法工作,那么你可能需要在GitHub上创建一个错误票据。 - Jovik
由于某些原因,其他解决方案在多次尝试后对我无效,但这个解决方案有效了! - roshambo
这是一个可在Windows上运行的解决方案。 - Jean-François Fabre
显示剩余2条评论

41
我可以使用以下方法来完成它:
from subprocess import Popen

process = Popen(command, shell=True)
Popen("TASKKILL /F /PID {pid} /T".format(pid=process.pid))

这导致 cmd.exe 和我给命令的程序崩溃了。

(在Windows系统上)


或者使用 进程名称Popen("TASKKILL /F /IM " + process_name),如果您没有它,可以从 command 参数获取它。 - zvi

20

shell=True 时,shell 是子进程,而命令是它的孩子进程。所以任何 SIGTERMSIGKILL 会杀死 shell,但不会杀死其子进程,我记不清有什么好方法可以解决这个问题。 我能想到的最好的方法是使用 shell=False,否则当你杀死父级 shell 进程时,它会留下一个僵尸 shell 进程。


为了避免僵尸进程,可以像以下stackoverflow答案中的“@SomeOne Maybe”所述那样终止进程,然后等待并轮询进程以确保它们已终止:https://dev59.com/KnE85IYBdhLWcg3wejZO - Keivan

18

对于我来说,这些答案都不起作用,所以我留下了成功运行的代码。在我的情况下,即使使用 .kill() 杀死了进程并获得了一个 .poll() 返回码,该进程仍未终止。

根据 subprocess.Popen文档

"...为了正确清理,良好的应用程序应该杀死子进程并完成通信..."

proc = subprocess.Popen(...)
try:
    outs, errs = proc.communicate(timeout=15)
except TimeoutExpired:
    proc.kill()
    outs, errs = proc.communicate()

在我的情况下,在调用proc.kill()之后我缺少了proc.communicate()。这会清理进程的标准输入、输出...并终止该进程。

在我的情况下,我调用了proc.kill()但是忘记了在之后调用proc.communicate()。这个方法会清理进程的输入和输出,并且终止进程。


这个解决方案在我的Linux和Python 2.7上不起作用。 - user9869932
@xyz 在 Linux 和 Python 3.5 中对我有效。请查看 Python 2.7 的文档。 - epinal
@espinal,谢谢,是的。可能是一个Linux问题。它是在Raspberry 3上运行的Raspbian Linux。 - user9869932
@mouad和Bryant的答案对我来说没问题,如果您按照您所说的在调用kill后通信,谢谢。 - mhanuel
这在Linux中对我起作用。 - Ayan Mitra

9

正如Sai所说,shell是子进程,因此信号被它截获。我发现最好的方法是使用shell=False,并使用shlex来拆分命令行:

if isinstance(command, unicode):
    cmd = command.encode('utf8')
args = shlex.split(cmd)

p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

然后,p.kill() 和 p.terminate() 应该按照您的预期工作。


1
在我的情况下,这并没有真正帮助,因为cmd是“cd path && zsync等等”。所以这实际上会导致命令失败! - user175259
5
请使用绝对路径而非修改目录...可以选择使用os.chdir(...)函数切换到该目录... - Matt Billenstein
更改子进程的工作目录的能力是内置的。只需将 cwd 参数传递给 Popen - Jonathon Reinhart
我使用了shlex,但问题仍然存在,kill命令无法杀死子进程。 - hungryWolf

3

有一种非常简单的方法适用于Python 3.5或更高版本(实际测试在Python 3.8上)

import subprocess, signal, time
p = subprocess.Popen(['cmd'], shell=True)
time.sleep(5) #Wait 5 secs before killing
p.send_signal(signal.CTRL_C_EVENT)

如果你的代码中有键盘输入检测等,则有可能在某个时刻崩溃。此时,在出错的代码行或函数上,只需使用:

try:
    FailingCode #here goes the code which is raising KeyboardInterrupt
except KeyboardInterrupt:
    pass

这段代码的作用是向正在运行的进程发送“CTRL+C”信号,这会导致该进程被终止。

1
注意:这是特定于Windows的。在Mac或Linux的signal实现中没有定义CTRL_C_EVENT。可以在此处找到一些替代代码(我尚未测试)。 - Jeff Wright

2
向组中的所有进程发送信号。
    self.proc = Popen(commands, 
            stdout=PIPE, 
            stderr=STDOUT, 
            universal_newlines=True, 
            preexec_fn=os.setsid)

    os.killpg(os.getpgid(self.proc.pid), signal.SIGHUP)
    os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM)

1

适用于我的解决方案

if os.name == 'nt':  # windows
    subprocess.Popen("TASKKILL /F /PID {pid} /T".format(pid=process.pid))
else:
    os.kill(process.pid, signal.SIGTERM)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接