Python应用因子进程变得无响应
我写了一个Python应用,使用Flask框架,搭建了一个简单的网站,可以在我的树莓派(微型计算机)上播放流媒体视频。简单来说,这个应用让我可以用手机或平板当遥控器。
我在Mac OS上测试了这个应用,一切正常。把它部署到树莓派(安装了Raspbian这个Debian的变种)后,网站也能正常访问,播放视频也没问题。但是,停止播放却失败了。
相关的代码可以在这里找到:https://github.com/lcvisser/mlbviewer-remote/blob/master/remote/mlbviewer-remote.py
子进程是这样启动的:
cmd = 'python2.7 mlbplay.py v=%s j=%s/%s/%s i=t1' % (team, mm, dd, yy)
player = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=sys.argv[1])
这个启动方式没问题。
子进程本该在这个之后停止:
player.send_signal(signal.SIGINT)
player.communicate()
在Mac OS上这个是有效的,但在树莓派上却不行:应用会一直卡住,直到子进程(以cmd
启动)自己结束。看起来SIGINT
信号没有被发送或者没有被子进程接收到。
有没有什么想法?
(我也在这里发了这个问题:https://unix.stackexchange.com/questions/133946/application-becomes-non-responsive-to-requests-on-raspberry-pi,因为我不知道这是操作系统的问题,还是Python/Flask相关的问题。)
更新:
尝试使用player.communicate()
,正如下面的Jan Vlcinsky建议的那样(在看到警告后这里),并没有帮助。
我在考虑使用Jan Vlcinsky提出的解决方案,但如果Flask根本没有收到请求,我觉得这也解决不了问题。
更新2: 昨晚我有幸遇到一个情况,能够准确定位问题。更新了问题,添加了相关代码。
我觉得Jan Vlcinsky的解决方案只是把问题转移到另一个应用上,这样Flask应用会保持响应,但新的应用可能会卡住。
更新3: 我编辑了问题的原始部分,去掉了我现在知道不相关的内容。
更新4: 在@shavenwarthog的评论后,以下信息可能非常相关:
在Mac上,mlbplay.py是这样启动的:
rmtpdump <some_options_and_url> | mplayer -
当发送SIGINT
给mlbplay.py时,它会终止这个管道命令创建的进程组(如果我理解得没错)。
在树莓派上,我使用omxplayer,但为了避免更改mlbplay.py的代码(这不是我的),我写了一个叫mplayer的脚本,内容如下:
#!/bin/bash
MLBTV_PIPE=mlbpipe
if [ ! -p $MLBTV_PIPE ]
then
mkfifo $MLBTV_PIPE
fi
cat <&0 > $MLBTV_PIPE | omxplayer -o hdmi $MLBTV_PIPE
我现在猜测最后一行启动了一个新的进程组,而这个进程组并没有被SIGINT
信号终止,因此导致我的应用卡住。如果是这样的话,我应该以某种方式获取这个进程组的ID,以便能够正确终止它。有人能确认这一点吗?
更新5: omxplayer确实处理SIGINT
:
https://github.com/popcornmix/omxplayer/blob/master/omxplayer.cpp#L131
更新6: 结果发现我的SIGINT在某个环节变成了SIGTERM。SIGTERM没有被omxplayer正确处理,这似乎是导致程序一直卡住的问题。我通过实现一个shell脚本来管理信号,并将其转换为正确的omxplayer命令解决了这个问题(有点像Jan建议的简化版)。
解决方案: 问题出在player.send_signal()
上。信号在命令链中没有被正确处理,导致父应用卡住。解决方案是为那些处理信号不好的命令实现包装器。
另外:使用Popen(cmd.split())
而不是shell=True
。这样在发送信号时效果好多了!
2 个回答
还有一个关键点:proc.terminate()
和 send_signal
的区别。
下面的代码会创建一个“玩家”(在这个例子中就是一个运行了 sleep
的简单程序),然后打印它的进程信息。接着,它会等一会儿,使用 terminate
来结束这个玩家,最后确认这个进程已经不存在了,也就是说它已经停止运行了。
感谢 @Jan Vlcinsky 为代码添加了 proc.communicate()
。
(我在运行 Linux Mint LMDE,这是 Debian 的一个变种。)
源代码
# pylint: disable=E1101
import subprocess, time
def show_procs(pid):
print 'Process Details:'
subprocess.call(
'ps -fl {}'.format(pid),
shell=True,
)
cmd = '/bin/sleep 123'
player = subprocess.Popen(cmd, shell=True)
print '* player started, PID',player.pid
show_procs(player.pid)
time.sleep(3)
print '\n*killing player'
player.terminate()
player.communicate()
show_procs(player.pid)
输出结果
* player started, PID 20393
Process Details:
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S johnm 20393 20391 0 80 0 - 1110 wait 17:30 pts/4 0:00 /bin/sh -c /bin/sleep 123
*killing player
Process Details:
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
下面的代码片段标记了问题:
@app.route('/watch/<year>/<month>/<day>/<home>/<away>/')
def watch(year, month, day, home, away):
global session
global watching
global player
# Select video stream
fav = config.get('favorite')
if fav:
fav = fav[0] # TODO: handle multiple favorites
if fav in (home, away):
# Favorite team is playing
team = fav
else:
# Use stream of home team
team = home
else:
# Use stream of home team
team = home
# End session
session = None
# Start mlbplay
mm = '%02i' % int(month)
dd = '%02i' % int(day)
yy = str(year)[-2:]
cmd = 'python2.7 mlbplay.py v=%s j=%s/%s/%s' % (team, mm, dd, yy)
# problem is here ----->
player = subprocess.Popen(cmd, shell=True, cwd=sys.argv[1])
# < ------problem is here
# Render template
game = {}
game['away_code'] = away
game['away_name'] = TEAMCODES[away][1]
game['home_code'] = home
game['home_name'] = TEAMCODES[home][1]
watching = game
return flask.render_template('watching.html', game=game)
你在启动一个新的进程来执行命令,但没有等它完成。你似乎认为这个命令行进程是唯一的,但你的前端并没有处理这个问题,可能会轻易地启动另一个进程。
另一个问题是,你没有调用 player.communicate()
,这样的话,如果 stdout
或 stderr
被输出填满,你的进程可能会被阻塞。
建议的解决方案 - 将进程控制器与网页应用分开
你想创建一个用户界面来控制播放器。为了实现这个目的,最好把你的解决方案分成前端和后端。后端可以作为播放器的控制器,提供一些方法,比如:
- 开始
- 停止
- 正在播放
为了将前端和后端整合在一起,有多种选择,其中之一是 zerorpc
,具体可以参考这里: https://stackoverflow.com/a/23944303/346478
这样做的好处是,你可以很容易地创建其他类型的前端(比如命令行界面,甚至是远程控制的)。