Python应用因子进程变得无响应

2 投票
2 回答
884 浏览
提问于 2025-04-18 08:13

我写了一个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 个回答

0

还有一个关键点: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
1

下面的代码片段标记了问题:

@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(),这样的话,如果 stdoutstderr 被输出填满,你的进程可能会被阻塞。

建议的解决方案 - 将进程控制器与网页应用分开

你想创建一个用户界面来控制播放器。为了实现这个目的,最好把你的解决方案分成前端和后端。后端可以作为播放器的控制器,提供一些方法,比如:

  • 开始
  • 停止
  • 正在播放

为了将前端和后端整合在一起,有多种选择,其中之一是 zerorpc,具体可以参考这里: https://stackoverflow.com/a/23944303/346478

这样做的好处是,你可以很容易地创建其他类型的前端(比如命令行界面,甚至是远程控制的)。

撰写回答