14.5 执行其他(非Python)程序
在Python程序里我们也可以执行非Python程序。这些程序包括了二进制可执行文件,其他的shell脚本等。所有的要求只是一个有效的执行环境,比如,允许文件访问和执行,脚本文件必须能访问它们的解释器(perl、bash等),二进制必须是可访问的(和本地机器的构架兼容)。
最终,程序员必须考虑Python脚本是否必须和其他将要执行的程序通信。有些程序需要输入,而有的程序返回输出以及执行完成时的错误代码,也许有的两者都做。针对不同的环境,Python提供了各种执行非Python程序的方法。在本节讨论的所有函数都可以在os模块中找到。在表14.6中,我们做了总结(我们会对那些只适合特定平台的函数进行标注),作为对本节剩余部分的介绍。
随着越来越接近软件的操作系统层面,你会发现执行跨平台程序(甚至是Python脚本)的一致性开始有些不确定了。上面我们提到在这个小节中描述的程序是在os模块中的。事实上,有多个os模块。比如说,基于Unix衍生系统(例如Linux、MacOS X、Solaris、BSD等)的模块是posix模块,windows的是nt(无论你现在用的是哪个版本的windows; dos用户有dos模块),旧的macOS为mac模块。不用担心,当你调用import os的时候,Python会装载正确的模块。你不需要直接导入特定的操作系统模块。
在我们看看每个模块函数之前,对于Python2.4或者更新版本的用户,这里有个subprocess模块,可以作为上面所有函数很好的替代品。我们本章稍后部分演示如何使用这些函数,然后在最后给出subprocess.Popen类和subprocess.call()函数的等价使用方法。
14.5.1 os.system()
我们列表中的第一个函数是system(),一个非常简单的函数,接收字符串形式的系统命令并执行它。当执行命令的时候,Python的运行是挂起的。当我们的执行完成之后,将会以system()的返回值形式给出退出状态,Python的执行也会继续。
system()保留了现有的标准文件,包括标准的输出,意味着执行任何命令和程序显示输出都会传到标准输出上。这里要当心,因为特定应用程序比如公共网关接口(common gateway interface, CGI),如果将除了有效的超文本标记语言(HTML)字符串之外的输出,经过标准输出发送回客户端,会引起Web浏览器错误。system()通常和不会产生输出的命令一起使用,其中的一些命令包括了压缩或转换文件的程序,挂载磁盘到系统的程序,或其他执行特定任务的命令——通过退出状态显示成功或失败而不是通过输入和/或输出通信。通常的约定是利用退出状态,0表示成功,非0表示其他类型的错误。
作为例子,我们执行了两个从交互解释器中获取程序输入的命令,这样你便可以观察system()是如何工作的。
可以看到两个命令的输出和它们执行的退出状态,我们将其保存到result变量中。下面是一个执行 dos命令的例子:
14.5.2 os.popen()
popen()函数是文件对象和system()函数的结合。它工作方式和system()相同,但它可以建立一个指向那个程序的单向连接,然后像访问文件一样访问这个程序。如果程序要求输入,那么你要用‘w’模式写入那个命令来调用popen()。你发送给程序的数据会通过标准输入接收到。同样,‘r’模式允许spawn命令,那么当它写入标准输出的时候,你就可以通过类文件句柄使用熟悉的file对象的read*()方法来读取输入。就像对于文件,当使用完毕以后,你应当close()连接。在上面其中一个使用system()的例子中,我们调用了unix程序uname来给我们提供机器和使用的操作系统的相关信息。该命令产生了一行输出,并直接写到屏幕上。如果想要把该字符串读入变量中并执行内部操作或者把它存储到日志文件中,我们可以使用popen()。实际上,代码如下所示:
如你所见,popen()返回一个类文件对象;注意readline(),往往保留输入文本行尾的newline字符。
14.5.3 os.fork()、os.exec()、os.wait()
本小节我们不会对操作系统理论做详尽的介绍,只是稍稍地介绍一下进程(process)。fork()采用称为进程的单一执行流程控制,如果你喜欢的话,可称之为创建“岔路口”。有趣的事情发生了:用户系统同时接管了两个岔路口——也就是说让用户拥有了两个连续且并行的程序(不用说,它们运行的是同一个程序,因为两个进程都是紧跟在fork()调用后的下一行代码开始执行的)。调用fork()的原始进程称为父进程,而作为该调用结果新创建的进程则称为子进程。当子进程返回的时候,其返回值永远是0;当父进程返回时,其返回值永远是子进程的进程标识符(又称进程ID,或PID)(这样父进程就可以监控所有的子进程了)PID (process ID)也是唯一可以区分他们的方式!我们提到了两个进程会在调用fork()后立刻运行。因为代码是相同的,如果没有其他的动作,我们将会看到同样的执行结果。而这通常不是我们想要的结果。创建另外一个进程的主要目的是为了运行其他程序,所以我们必须在父进程和子进程返回时采取分流措施。正如上面我们所说,它们的PID是不同的,而这正是我们区分它们的方法。
对于那些有进程管理经验的人来说,接下来的这段代码是再熟悉不过了。但是,如果你是新手的话,一开始就弄懂它是如何工作的可能就有点困难了,但是一旦你懂了,就会体会到其中的奥妙。
在代码第一行便调用了fork()。现在子进程和父进程同时在运行。子进程本身有虚拟内存地址空间的拷贝,以及一份父进程地址空间的原样拷贝——是的,两者几乎都是相同的。fork()返回两次,意味着父进程和子进程都返回了。你或许会问,如果它们两个同时返回,如何区分两者呢?当父亲返回的时候,会带有进程的PID。而当子进程返回的时候,其返回值为0。这就是区分两个进程的方法。
利用if-else语句,我们能给子进程(比如,if子句)和父进程(else子句)指定各自的执行代码。在子进程的代码中,我们可以调用任何exec*()函数来运行完全不同的程序,或者同一个程序中的其他的函数(只要子进程和父进程用不同的路径执行)。普遍做法是让子进程做所有的脏活,而父进程耐心等来子进程完成任务,或继续执行,稍后再来检查子进程是否正常结束。
所有的exec*()函数装载文件或者命令,并用参数列表(分别给出或作为参数列表的一部分)来执行它。如果适用的话,也可以给命令提供环境变量字典。这些变量普遍用于给程序提供对当前执行环境的精确描述。其中一些著名的变量包括用户的名字、搜索路径、现在的shell、终端类型、本地化语言、机器类型、操作系统名字等。
所有版本的exec()都会用给定文件作为现在要执行的程序取代当前(子)进程的Python解释器。和system()不一样,对于Python来说没有返回值(因为Python已经被替代了)。如果因为某种原因,程序不能执行,那么exec()就会失败,进而导致引发异常。
接下来的代码在子进程中开始了一个称为“xbill”的可爱小巧的游戏,而父进程继续运行Python解释器。因为子进程从不返回,所以无需去顾虑调用exec*()后的子进程代码。注意该命令也是参数列表中的必须的第一个参数。
在这段代码中,还可以看到对wait()的调用。当子进程执行完毕,需要它们的父进程进行扫尾工作。这个任务,称为“收获孩子”(reaping a child),可以用wati()函数完成。紧跟在fork()之后,父进程可以等待子进程完成并在那进行扫尾。父进程也可以继续运行,稍后再扫尾,同样也是用wait()函数中的一个。
不管父进程选择了那个方法,该工作都必须进行。当子进程完成执行,还没有被收获的时候,它进入了闲置状态,变成了著名的僵尸进程。在系统中,应该尽量把僵尸进程的数目降到最少,因为在这种状态下的子进程仍保留着在存活时期分配给它们的系统资源,而这些资源只能在父进程收获它们之后才能释放掉。
调用wait()会挂起执行(比如,waits),直到子进程(其他的子进程)正常执行完毕或通过信号终止。 wait()将会收获子进程,释放所有的资源。如果子进程已经完成,那么wait()只是进行些收获的过程。 waitpid()具有和wait()相同的的功能,但是多了一个参数PID(指定要等待子进程的进程标识符),以及选项(通常是零或用“OR”组成的可选标志集合)。
14.5.4 os.spawn*()
函数spawn()家族和fork, exec()相似,因为它们在新进程中执行命令;然而,你不需要分别调用两个函数来创建进程,并让这个进程执行命令。你只需调用一次spawn()家族。由于其简单性,你放弃了“跟踪”父进程和子进程执行的能力;该模型类似于在线程中启动函数。还有点不同的是你必须知道传入spawn()的魔法模式参数。在其他的操作系统中(尤其是嵌入式实时操作系统(RTOS)),spawn()比fork()快很多。不是这种情况的操作系统通常使用写实拷贝(copy-on-write)技术。参阅Python库参考手册来获得更多spanw()的资料。各种spanw*()家族成员是在1.5和1.6(含1.6)之间加入的。
14.5.5 subprocess模块
在Python 2.3出来之后,一些关于popen5模块的工作开始展开。一开始该命名继承了先前popen*()函数的传统,但是并没有延续下来,该模块最终被命名为subproess,其中一个类叫Popen,集中了我们在这章讨论的大部分面向进程的函数。同样也有名为call()的便捷函数,可以轻易地取代了os.system()。在Python 2.4中,subprocess初次登场。下面就是演示该模块的例子:
Linux上的例子:
Win32例子
取代os.popen()
创建Popen()实例的语法只比调用os.popen()函数复杂了一点
14.5.6 相关函数
表14.7列出了可以执行上述任务的函数(及其模块)。