处理外部进程
我最近在开发一个图形界面应用程序,需要管理一些外部进程。处理外部进程会遇到很多问题,这让程序员的工作变得很麻烦。我觉得这个应用的维护时间太长了,所以我开始列出一些让处理外部进程变得困难的原因,希望能找到一些解决办法。这篇文章变成了我的一些抱怨,我想在这里分享一下,看看大家有什么反馈,也给那些想要进入这个复杂领域的人一些建议。以下是我总结的一些问题:
子进程的输出可能会和父进程的输出混在一起。这会让输出变得难以理解,搞不清楚哪个是哪个。当事情是异步进行时,更是难以判断发生了什么。举个例子:
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'
,但每次打印都这样做也很烦。而且这会让输出变得杂乱。如果我没有代码的访问权限,那就更麻烦了。我可以手动管理进程的输出,但这会涉及到线程、轮询等复杂问题。
一个简单的解决办法是把进程当作同步函数来处理,也就是说,进程完成之前不执行后面的代码。换句话说,让进程阻塞。但如果你在做图形界面应用,这个方法就不适用了。这就引出了下一个问题……
阻塞进程会导致图形界面变得无响应。
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' 信号调用一个包含所有后续代码的函数来实现某种阻塞行为。我觉得这样应该可行,但具体细节我还不太清楚……当从子进程返回到父进程时,堆栈跟踪会被中断。如果一个普通函数出错,你会得到一个完整的堆栈跟踪,包含文件名和行号。如果是子进程出错,你可能连输出都得不到。每次出问题时,你都得做更多的侦探工作。
说到这个,处理外部进程时输出常常会消失。比如,如果你通过 Windows 的 'cmd' 命令运行某个程序,控制台会弹出,执行代码,然后在你来不及查看输出时就消失了。你得加上 /k 标志才能让它保持打开。类似的问题似乎总是会出现。
我想问题 3 和 4 的根源是一样的:缺乏异常处理。异常处理是用在函数上的,处理进程时不太管用。也许有办法为进程实现类似异常处理的机制?我想这就是 stderr 的作用吧?但处理两个不同的输出流本身就很麻烦。也许我应该更深入地研究这个……
进程可能会挂起,悄悄地在后台运行,而你却没有意识到。结果你会对着电脑大喊,因为它变得很慢,直到你打开任务管理器,发现有 30 个同样的进程在后台挂着。
而且,挂起的后台进程可能会以各种有趣的方式干扰其他进程,比如因为占用文件句柄而导致权限错误。
看起来一个简单的解决办法是让父进程在退出时杀掉子进程,如果子进程没有自己关闭。但如果父进程崩溃,清理代码可能不会被调用,子进程就会挂着。
另外,如果父进程在等待子进程完成,而子进程在无限循环中,你就会遇到两个进程都挂着的情况。
这个问题还可能和问题 2 结合在一起,导致你的图形界面完全无响应,只能通过任务管理器强制结束所有进程。
该死的引号
参数通常需要传递给进程,这本身就是个麻烦。尤其是处理文件路径时。比如说…… 'C:/My Documents/whatever/'。如果没有引号,字符串通常会在空格处被拆分,变成两个参数。如果需要嵌套引号,可以用 ' 和 "。但如果需要超过两层的引号,就得进行一些麻烦的转义,比如: "cmd /k 'python \'path 1\' \'path 2\''"。
一个好的解决办法是将参数作为列表传递,而不是作为单个字符串。子进程允许你这样做。
无法轻松地从子进程返回数据。
当然可以使用 stdout。但如果你想在里面打印一些调试信息呢?这会搞乱父进程,如果它期望输出格式是特定的。在函数中,你可以打印一个字符串,返回另一个,所有的事情都能正常运作。
晦涩的命令行标志和糟糕的基于终端的帮助系统。
这些问题我在使用操作系统级应用时经常遇到。比如我提到的 /k 标志,用来保持 cmd 窗口打开,这是谁想出来的?Unix 应用在这方面也不太友好。希望你能用谷歌或 StackOverflow 找到需要的答案。但如果找不到,你就得进行大量无聊的阅读和令人沮丧的反复尝试。
外部因素。
这个问题有点模糊。但当你离开自己脚本的相对安全的港湾,去处理外部进程时,你会发现自己不得不更大程度地应对“外部世界”。这真是个可怕的地方,各种事情都可能出错。举个随机的例子:进程运行的当前工作目录可能会改变它的行为。
可能还有其他问题,但这些是我目前写下来的。你们还有什么想补充的吗?有什么建议可以帮助解决这些问题吗?
1 个回答
可以看看subprocess模块。这个模块应该能帮助你分开输出内容。我觉得要么就得用不同的输出流,要么就得在同一个流里加点标签来区分输出。
关于进程挂起的问题也挺复杂的。我能想到的唯一解决办法就是给外部进程加个定时器,如果它在规定时间内没返回,就把它杀掉。这种方法粗糙又麻烦,如果有人有更好的办法,我很想听听,这样我也能用上。
为了处理完全不受控制的关闭问题,你可以保持一个pid文件的目录。每次启动一个外部进程时,就在这个目录里写一个文件,文件名用这个进程的pid。等你确认进程正常退出后,再删除这个pid文件。你可以利用这个pid目录里的内容来帮助处理崩溃或重启时的清理工作。
这可能没有提供什么令人满意或有用的答案,但也许这是一个开始。