杀死子进程后Shell挂起

5 投票
2 回答
3121 浏览
提问于 2025-04-18 01:37

我知道在StackOverflow上有很多类似的问题,比如这个这个,还有其他一些,但这些似乎都不适合我现在的情况。而且我对subprocess.Popen()是怎么回事也不太明白,这让我更困惑。

我想要实现的目标是:启动一个子进程(一个命令行的收音机播放器),这个播放器不仅能把数据输出到终端,还能接收输入——等一会儿——然后结束这个子进程——最后退出命令行。我现在在OSX 10.9上运行的是Python 2.7。

情况 1.
这个代码启动了收音机播放器(但只播放音频!),然后结束了这个进程,最后退出。

import subprocess
import time

p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
                     stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=False,
                     stderr=subprocess.STDOUT)
time.sleep(5)
p.kill()

情况 2.
这个代码启动了收音机播放器,能输出一些信息,比如电台名称、歌曲、比特率等等,还能接收输入。它结束了子进程,但却没有退出命令行,终端也变得无法使用,即使按了'Ctrl-C'也没用。

p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
                     shell=False)
time.sleep(5)
p.kill()

有没有什么办法可以做到这一点?我甚至在考虑如果没有其他选择的话,是否可以为子进程打开一个从属终端(当然这也是我完全不懂的事情)。谢谢!

2 个回答

2

我想要实现的目标是:启动一个子进程(一个命令行的广播播放器),它可以在终端输出数据,同时也能接收输入——等一会儿——结束这个子进程——退出命令行。我在OSX 10.9上运行Python 2.7。

在我的系统上,mplayer可以接受键盘命令,比如用q来停止播放并退出:

#!/usr/bin/env python
import shlex
import time
from subprocess import Popen, PIPE

cmd = shlex.split("mplayer http://www.swissradio.ch/streams/6034.m3u")
p = Popen(cmd, stdin=PIPE)
time.sleep(5)
p.communicate(b'q')

这个命令会启动mplayer,调到公共领域的古典音乐;等5秒;然后让mplayer退出,并等待它完成退出。输出会显示在终端上(也就是Python脚本输出的地方)。

我还尝试过p.kill()p.terminate()p.send_signal(signal.SIGINT)(也就是按Ctrl + C)。使用p.kill()会让人觉得进程卡住了。可能的原因是:p.kill()会留下某些管道没有关闭,比如如果stdout=PIPE,那么你的Python脚本可能会在p.stdout.read()这里卡住,也就是说它杀掉了父进程mplayer,但可能还有一个子进程在占用这些管道。使用p.terminate()p.send_signal(signal.SIGINT)就不会有这种情况——mplayer会正常退出。我尝试的这些方法都不需要用到reset


我该怎么做才能同时接收来自Python和键盘的输入?我需要两个不同的子进程吗?如何将键盘输入重定向到PIPE?

其实只要去掉stdin=PIPE,然后调用p.terminate(); p.wait(),就比用p.communicate(b'q')简单多了。

如果你想保留stdin=PIPE,那么一般的原则是:从sys.stdin读取数据,写入到p.stdin,直到超时。因为mplayer只需要单个字母的命令,所以你需要能够一次从sys.stdin读取一个字符。写入的部分很简单:p.stdin.write(c)(设置bufsize=0以避免在Python端的缓冲。mplayer不对它的stdin进行缓冲,所以你不需要担心这个)。

你不需要两个不同的子进程。要实现timeout,你可以使用threading.Timer(5, p.stdin.write, [b'q']).start(),或者在sys.stdin上使用select.select来设置超时。

我想知道使用老式的raw_input是否有关系?

raw_input()不适合mplayer,因为它是读取整行的,而mplayer只需要一次一个字符的输入。

5

看起来 mplayer 是用 curses 这个库来工作的,当你用 kill()terminate() 来结束它时,出于某种原因,它没有正确清理这个库的状态。

要恢复终端的状态,你可以使用 reset 命令。

演示:

import subprocess, time

p = subprocess.Popen(['mplayer', 'http://173.239.76.147:8090'])
time.sleep(5)
p.terminate()
p.wait()  # important!

subprocess.Popen(['reset']).wait()

print('Hello, World!')

原则上,你也可以使用 stty sane,但对我来说效果不太好。


正如 Sebastian 指出的那样,上面的代码缺少了一个 wait() 调用(现在已经加上了)。有了这个 wait() 调用 并且 使用 terminate(),终端就不会出现混乱(所以就不需要使用 reset 了)。

如果没有 wait(),我有时会遇到 Python 进程和 mplayer 输出混合的问题。

另外,Sebastian 还指出了一个针对 mplayer 的解决方案,就是向 mplayer 的标准输入发送一个 q 来退出它。

我保留了使用 reset 的代码,因为它适用于 任何 使用 curses 库的程序,无论它是否正确关闭这个库,因此在其他情况下如果无法干净退出时可能会很有用。

撰写回答