处理外部进程

3 投票
1 回答
727 浏览
提问于 2025-04-15 23:03

我最近在开发一个图形界面应用程序,需要管理一些外部进程。处理外部进程会遇到很多问题,这让程序员的工作变得很麻烦。我觉得这个应用的维护时间太长了,所以我开始列出一些让处理外部进程变得困难的原因,希望能找到一些解决办法。这篇文章变成了我的一些抱怨,我想在这里分享一下,看看大家有什么反馈,也给那些想要进入这个复杂领域的人一些建议。以下是我总结的一些问题:

  1. 子进程的输出可能会和父进程的输出混在一起。这会让输出变得难以理解,搞不清楚哪个是哪个。当事情是异步进行时,更是难以判断发生了什么。举个例子:

    import textwrap, os, time
    from subprocess import Popen
    test_path = 'test_file.py'
    
    with open(test_path, 'w') as file:
        file.write(textwrap.dedent('''
            import time
            for i in range(3):
                print 'Hello %i' % i
                time.sleep(1)'''))
    
    proc = Popen('python -B "%s"' % test_path)
    
    for i in range(3):
        print 'Hello %i' % i
        time.sleep(1)
    
    os.remove(test_path)
    

    输出:

    Hello 0
    Hello 0
    Hello 1
    Hello 1
    Hello 2
    Hello 2
    

    我可以让子进程把输出写入一个文件,但每次想查看打印结果时都要打开文件,这实在是太麻烦了。

    如果我有子进程的代码,可以加个标签,比如 print 'child: Hello %i',但每次打印都这样做也很烦。而且这会让输出变得杂乱。如果我没有代码的访问权限,那就更麻烦了。

    我可以手动管理进程的输出,但这会涉及到线程、轮询等复杂问题。

    一个简单的解决办法是把进程当作同步函数来处理,也就是说,进程完成之前不执行后面的代码。换句话说,让进程阻塞。但如果你在做图形界面应用,这个方法就不适用了。这就引出了下一个问题……

  2. 阻塞进程会导致图形界面变得无响应。

    import textwrap, sys, os
    from subprocess import Popen
    
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    
    test_path = 'test_file.py'
    with open(test_path, 'w') as file:
        file.write(textwrap.dedent('''
            import time
            for i in range(3):
                print 'Hello %i' % i
                time.sleep(1)'''))
    
    app = QApplication(sys.argv)
    button = QPushButton('Launch process')
    def launch_proc():
        # Can't move the window until process completes
        proc = Popen('python -B "%s"' % test_path)
        proc.communicate()
    button.connect(button, SIGNAL('clicked()'), launch_proc)
    button.show()
    app.exec_() 
    os.remove(test_path)
    

    Qt 提供了一个叫 QProcess 的进程封装,可以帮助解决这个问题。你可以将函数连接到信号,以相对简单地捕获输出。这是我现在使用的方法。但我发现这些信号的行为有点像 goto 语句,容易导致代码混乱。我想通过让 QProcess 的 'finished' 信号调用一个包含所有后续代码的函数来实现某种阻塞行为。我觉得这样应该可行,但具体细节我还不太清楚……

  3. 当从子进程返回到父进程时,堆栈跟踪会被中断。如果一个普通函数出错,你会得到一个完整的堆栈跟踪,包含文件名和行号。如果是子进程出错,你可能连输出都得不到。每次出问题时,你都得做更多的侦探工作。

  4. 说到这个,处理外部进程时输出常常会消失。比如,如果你通过 Windows 的 'cmd' 命令运行某个程序,控制台会弹出,执行代码,然后在你来不及查看输出时就消失了。你得加上 /k 标志才能让它保持打开。类似的问题似乎总是会出现。

    我想问题 3 和 4 的根源是一样的:缺乏异常处理。异常处理是用在函数上的,处理进程时不太管用。也许有办法为进程实现类似异常处理的机制?我想这就是 stderr 的作用吧?但处理两个不同的输出流本身就很麻烦。也许我应该更深入地研究这个……

  5. 进程可能会挂起,悄悄地在后台运行,而你却没有意识到。结果你会对着电脑大喊,因为它变得很慢,直到你打开任务管理器,发现有 30 个同样的进程在后台挂着。

    而且,挂起的后台进程可能会以各种有趣的方式干扰其他进程,比如因为占用文件句柄而导致权限错误。

    看起来一个简单的解决办法是让父进程在退出时杀掉子进程,如果子进程没有自己关闭。但如果父进程崩溃,清理代码可能不会被调用,子进程就会挂着。

    另外,如果父进程在等待子进程完成,而子进程在无限循环中,你就会遇到两个进程都挂着的情况。

    这个问题还可能和问题 2 结合在一起,导致你的图形界面完全无响应,只能通过任务管理器强制结束所有进程。

  6. 该死的引号

    参数通常需要传递给进程,这本身就是个麻烦。尤其是处理文件路径时。比如说…… 'C:/My Documents/whatever/'。如果没有引号,字符串通常会在空格处被拆分,变成两个参数。如果需要嵌套引号,可以用 ' 和 "。但如果需要超过两层的引号,就得进行一些麻烦的转义,比如: "cmd /k 'python \'path 1\' \'path 2\''"。

    一个好的解决办法是将参数作为列表传递,而不是作为单个字符串。子进程允许你这样做。

  7. 无法轻松地从子进程返回数据。

    当然可以使用 stdout。但如果你想在里面打印一些调试信息呢?这会搞乱父进程,如果它期望输出格式是特定的。在函数中,你可以打印一个字符串,返回另一个,所有的事情都能正常运作。

  8. 晦涩的命令行标志和糟糕的基于终端的帮助系统。

    这些问题我在使用操作系统级应用时经常遇到。比如我提到的 /k 标志,用来保持 cmd 窗口打开,这是谁想出来的?Unix 应用在这方面也不太友好。希望你能用谷歌或 StackOverflow 找到需要的答案。但如果找不到,你就得进行大量无聊的阅读和令人沮丧的反复尝试。

  9. 外部因素。

    这个问题有点模糊。但当你离开自己脚本的相对安全的港湾,去处理外部进程时,你会发现自己不得不更大程度地应对“外部世界”。这真是个可怕的地方,各种事情都可能出错。举个随机的例子:进程运行的当前工作目录可能会改变它的行为。

可能还有其他问题,但这些是我目前写下来的。你们还有什么想补充的吗?有什么建议可以帮助解决这些问题吗?

1 个回答

2

可以看看subprocess模块。这个模块应该能帮助你分开输出内容。我觉得要么就得用不同的输出流,要么就得在同一个流里加点标签来区分输出。

关于进程挂起的问题也挺复杂的。我能想到的唯一解决办法就是给外部进程加个定时器,如果它在规定时间内没返回,就把它杀掉。这种方法粗糙又麻烦,如果有人有更好的办法,我很想听听,这样我也能用上。

为了处理完全不受控制的关闭问题,你可以保持一个pid文件的目录。每次启动一个外部进程时,就在这个目录里写一个文件,文件名用这个进程的pid。等你确认进程正常退出后,再删除这个pid文件。你可以利用这个pid目录里的内容来帮助处理崩溃或重启时的清理工作。

这可能没有提供什么令人满意或有用的答案,但也许这是一个开始。

撰写回答