【问题标题】:Python application becomes non-responsive due to subprocess由于子进程,Python 应用程序变得无响应
【发布时间】:2014-06-01 20:09:00
【问题描述】:

我使用 Flask 编写了一个 Python 应用程序,它提供了一个简单的网站,我可以使用它在我的 Raspberry Pi(微型计算机)上开始播放流视频。从本质上讲,该应用程序允许将我的手机或平板电脑用作遥控器。

我在 Mac OS 上测试了该应用程序,它运行良好。将其部署到 Raspberry Pi(安装了 Debian 的 Raspbian 变体)后,它可以很好地为网站提供服务,并且开始播放也可以按预期工作。但是,停止播放失败。

相关代码托管在这里: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 上工作,但在 Raspberry Pi 上工作:应用程序挂起,直到子进程(以 cmd 启动)完成本身。似乎SIGINT 没有被子进程发送或接收。

有什么想法吗?


(我也在这里发布了这个问题:https://unix.stackexchange.com/questions/133946/application-becomes-non-responsive-to-requests-on-raspberry-pi,因为我不知道这是操作系统问题还是 Python/Flask 相关问题。)

更新: 按照下面 Jan Vlcinsky 的建议尝试使用 player.communicate()(在最终看到警告 here 之后)没有帮助。

我正在考虑使用 Jan Vlcinsky 提出的解决方案,但如果 Flask 甚至没有收到请求,我认为这不会收到问题。

更新 2: 昨天晚上,我很幸运地遇到了能够准确指出问题的情况。用相关代码更新了问题。

我觉得 Jan Vlcinsky 的解决方案只是将问题转移到另一个应用程序,这将使 Flask 应用程序保持响应,但会让新应用程序挂起。

更新 3: 我编辑了问题的原始部分以删除我现在知道不相关的内容。

更新 4:在@shavenwarthog 的 cmets 之后,以下信息可能非常相关:

在 Mac 上,mlbplay.py 会这样启动:

rmtpdump <some_options_and_url> | mplayer -

当发送SIGINT 到mlbplay.py 时,它会终止这个管道命令创建的进程组(如果我understood 正确的话)。

在树莓派上,我用的是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。 omxplayer 没有正确处理 SIGTERM,这似乎是事情一直挂起的问题。我通过实现一个 shell 脚本来解决这个问题,该脚本管理信号并将它们转换为适当的 omxplayer 命令(有点像 Jan 建议的蹩脚版本)。

解决方案:问题出在player.send_signal()。该信号未在命令链中正确处理,导致父应用程序挂起。解决方案是为不能很好地处理信号的命令实现包装器。

另外:使用Popen(cmd.split())而不是使用shell=True。这在发送信号时效果更好!

【问题讨论】:

  • 你能分享一些代码吗?我会对您的 Flask 代码感兴趣(理想情况下是展示问题的完整工作示例,但至少是 subprocess.Popen 的一部分)。
  • 完整代码托管在这里:github.com/lcvisser/mlbviewer-remote/blob/master/remote/… 子进程在第 142 行开始,在第 163 行停止

标签: python linux raspberry-pi


【解决方案1】:

问题标记在下面的sn-p中:

@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)

您正在启动执行 shell 命令的新进程,但不要等到它完成。您似乎依赖于一个事实,即命令行进程本身是单一的,但是您的前端没有处理它并且可以轻松地启动另一个。

另一个问题可能是,您不调用player.communicate(),如果stdoutstderr 被某些输出填充,您的进程可能会阻塞。

建议的解决方案 - 从 Web 应用中拆分进程控制器

您正在尝试创建用于控制播放器的 UI。为此,将您的解决方案拆分为前端和后端是切实可行的。后端将充当播放器控制器,并提供像

这样的方法
  • 开始
  • 停止
  • 正在播放

要集成前端和后端,有多种选择,其中之一是zerorpc,如下所示:https://stackoverflow.com/a/23944303/346478

优势在于,您可以非常轻松地创建其他前端(例如命令行一,甚至远程一)。

【讨论】:

  • 我同意将控制器从界面中分离出来是个好主意(尽管对于这个小项目来说这似乎有点矫枉过正)。尽管如此:a)为什么调用其他进程的子进程会成为问题? (顺便说一句,它确实启动了一堆其他进程)。 b) 如果std* 缓冲区确实被填满(我猜这些缓冲区可能在 Raspberry Pi 上比在 Mac 上小),为什么它还会阻止 Flask 应用程序?这不会破坏子流程的全部目的吗?
【解决方案2】:

还有一个难题:proc.terminate()send_signal

下面的代码派生了一个“播放器”(在这种情况下只是一个带有sleep 的shell),然后打印它的进程信息。它等待片刻,terminates 播放器,然后验证该进程不再存在,它已经停止。

感谢@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

【讨论】:

  • 我更愿意将SIGINT 发送到进程,因为此信号被明确处理为优雅退出我正在尝试运行的程序的一种方式。发送SIGTERM 对我来说似乎有点太严格了。
  • @ludo 我明白你的意思。注意SIGTERM 允许优雅退出; SIGKILL 是不可屏蔽的。我期待解决方案!
  • 不幸的是,mlbplay 不是我的程序,所以我无法添加SIGTERM 处理程序。此外,如果SIGINT 没有到达,为什么SIGTERM 会到达?
  • SIGINT/SIGTERM 应该相同;它们都是可屏蔽的。如果您发送 SIGTERM 会发生什么,它的行为是否与 SIGINT 不同?如果您使用以下 mlbplay.py,它没有 any 信号处理程序。 Python proc 可能会收到信号,但子 proc 不会让它死掉。 nullege.com/codes/show/src@m@l@mlbviewer-HEAD@trunk@mlbplay.py
  • 链接指向旧版本;这是最新的:sourceforge.net/p/mlbviewer/code/HEAD/tree/trunk/mlbplay.py 在第 384 行它捕获 KeyboardInterrupt 以停止播放。 “但是一个子进程不会让它死掉。” -> 这可能是一个好点。在 Mac OS 上,我使用的播放器与 Raspberry Pi 不同。也许树莓派上的那个不喜欢通过SIGINT退出...!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-14
相关资源
最近更新 更多