PEP 324 – 子进程 - 新的进程模块
- 作者:
- Peter Astrand <astrand at lysator.liu.se>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2003年11月19日
- Python 版本:
- 2.4
- 发布历史:
摘要
本 PEP 描述了一个用于启动进程并与之通信的新模块。
动机
在任何编程语言中,启动新进程都是一个常见的任务,在像 Python 这样的高级语言中更是如此。良好地支持这项任务是必要的,因为
- 不恰当的进程启动函数可能意味着安全风险:如果程序通过 shell 启动,并且参数中包含 shell 元字符,结果可能会是灾难性的。[1]
- 它使 Python 成为一个更好的替代语言,用于替换过于复杂的 shell 脚本。
目前,Python 有大量不同的函数用于创建进程。这使得开发者难以选择。
subprocess 模块提供了比以前函数以下增强功能
- 一个“统一”的模块提供了以前所有函数的功能。
- 跨进程异常:子进程在开始执行新进程之前发生的异常会在父进程中重新抛出。这意味着可以很容易地处理 exec()失败,例如。例如,使用 popen2,无法检测执行是否失败。
- 一个用于在 fork 和 exec 之间执行自定义代码的钩子。这可以用于,例如,更改 uid。
- 不隐式调用 /bin/sh。这意味着无需转义危险的 shell 元字符。
- 文件描述符重定向的所有组合都是可能的。例如,“python-dialog” [2] 需要生成一个进程并重定向 stderr,但不是 stdout。这在不使用临时文件的情况下,使用当前函数是不可能实现的。
- 使用 subprocess 模块,可以控制在执行新程序之前是否应关闭所有打开的文件描述符。
- 支持连接多个子进程(shell“管道”)。
- 通用换行符支持。
- 一个 communicate()方法,它使得发送 stdin 数据和读取 stdout 和 stderr 数据变得容易,而不会有死锁的风险。大多数人都了解与子进程通信相关的流量控制问题,但并非所有人都有耐心或技能编写一个完全正确且无死锁的 select 循环。这意味着许多 Python 应用程序包含竞态条件。标准库中的communicate()方法解决了这个问题。
基本原理
以下几点总结了设计
- subprocess 基于 popen2,后者经过了尝试和测试。
- popen2 中的工厂函数已被移除,因为我认为类构造函数同样易于使用。
- popen2 包含多个工厂函数和类,用于不同组合的重定向。然而,subprocess 包含一个单一的类。由于 subprocess 模块支持 12 种不同的重定向组合,为每一种组合提供一个类或函数将是繁琐且不直观的。即使使用 popen2,这也是一个可读性问题。例如,许多人在不使用文档的情况下无法区分 popen2.popen2 和 popen2.popen4。
- 提供了一个小型实用函数:subprocess.call()。它旨在增强os.system(),同时仍非常易于使用- 它不使用具有限制的标准 C 函数 system()。
- 它不隐式调用 shell。
- 无需引用;使用参数列表。
- 返回值更容易处理。
 call()实用函数接受一个“args”参数,就像Popen类构造函数一样。它会等待命令完成,然后返回returncode属性。实现非常简单def call(*args, **kwargs): return Popen(*args, **kwargs).wait() 引入 call()函数的动机很简单:启动一个进程并等待它完成是一项常见的任务。Popen支持广泛的选项,但许多用户有简单的需求。许多人今天正在使用os.system(),主要是因为它提供了一个简单的接口。考虑这个例子os.system("stty sane -F " + device) 使用 subprocess.call(),这看起来像subprocess.call(["stty", "sane", "-F", device]) 或者,如果通过 shell 执行 subprocess.call("stty sane -F " + device, shell=True) 
- “preexec”功能使得在 fork 和 exec 之间运行任意代码成为可能。有人可能会问为什么有特殊的参数用于设置环境和当前目录,但没有用于例如设置 uid 的参数。答案是- 更改环境和工作目录被认为是相当常见的。
- 像 spawn()这样的旧函数支持“env”参数。
- env 和 cwd 被认为是跨平台的:即使在 Windows 上它们也有意义。
 
- 在 POSIX 平台上,不需要扩展模块:该模块使用 os.fork()、os.execvp()等。
- 在 Windows 平台上,该模块需要 Mark Hammond 的 Windows 扩展 [5],或一个名为 _subprocess 的小型扩展模块。
规范
此模块定义了一个名为 Popen 的类
class Popen(args, bufsize=0, executable=None,
            stdin=None, stdout=None, stderr=None,
            preexec_fn=None, close_fds=False, shell=False,
            cwd=None, env=None, universal_newlines=False,
            startupinfo=None, creationflags=0):
参数是
- args应该是一个字符串,或一个程序参数序列。要执行的程序通常是 args 序列或字符串中的第一项,但可以通过使用 executable 参数明确设置。- 在 UNIX 上,当 - shell=False(默认)时:在这种情况下,- Popen类使用- os.execvp()执行子程序。- args通常应该是一个序列。字符串将被视为一个序列,其中字符串是唯一项(要执行的程序)。- 在 UNIX 上,当 - shell=True时:如果- args是一个字符串,它指定通过 shell 执行的命令字符串。如果- args是一个序列,第一项指定命令字符串,任何附加项都将被视为附加的 shell 参数。- 在 Windows 上: - Popen类使用- CreateProcess()执行子程序,它对字符串进行操作。如果- args是一个序列,它将使用- list2cmdline方法转换为字符串。请注意,并非所有 MS Windows 应用程序都以相同的方式解释命令行:- list2cmdline是为使用与 MS C 运行时相同规则的应用程序设计的。
- bufsize,如果给出,其含义与内置- open()函数的相应参数相同:0 表示无缓冲,1 表示行缓冲,任何其他正值表示使用(大约)该大小的缓冲区。负的- bufsize表示使用系统默认值,通常表示完全缓冲。- bufsize的默认值为 0(无缓冲)。
- stdin、- stdout和- stderr分别指定了已执行程序的标准输入、标准输出和标准错误文件句柄。有效值为- PIPE、现有文件描述符(正整数)、现有文件对象和- None。- PIPE表示应为子进程创建一个新管道。使用- None时,不会发生重定向;子进程的文件句柄将从父进程继承。此外,- stderr可以是 STDOUT,表示应用程序的 stderr 数据应捕获到与 stdout 相同的文件句柄中。
- 如果 preexec_fn设置为可调用对象,则该对象将在子进程执行之前在子进程中调用。
- 如果 close_fds为真,则在子进程执行之前,除了 0、1 和 2 之外的所有文件描述符都将关闭。
- 如果 shell为真,则指定的命令将通过 shell 执行。
- 如果 cwd不为None,则在子进程执行之前,当前目录将更改为 cwd。
- 如果 env不为None,它定义了新进程的环境变量。
- 如果 universal_newlines为真,则文件对象 stdout 和 stderr 将以文本文件打开,但行可能由 Unix 行尾约定\n、Macintosh 约定\r或 Windows 约定\r\n中的任何一个终止。所有这些外部表示形式都被 Python 程序视为\n。注意:此功能仅在 Python 以通用换行符支持构建时可用(默认)。此外,文件对象 stdout、stdin 和 stderr 的 newlines 属性不会被communicate()方法更新。
- startupinfo和- creationflags(如果给定)将传递给底层的- CreateProcess()函数。它们可以指定诸如主窗口外观和新进程的优先级等信息。(仅限 Windows)
此模块还定义了两个快捷函数
- call(*args, **kwargs):
- 运行带参数的命令。等待命令完成,然后返回 returncode属性。参数与 Popen 构造函数相同。示例 retcode = call(["ls", "-l"]) 
 
异常
在子进程中、在新程序开始执行之前引发的异常,将在父进程中重新引发。此外,异常对象将有一个名为“child_traceback”的额外属性,它是一个包含子进程视角的跟踪信息字符串。
最常见的异常是 OSError。例如,当尝试执行一个不存在的文件时,就会发生这种情况。应用程序应该为 OSError 做好准备。
如果使用无效参数调用 Popen,将引发 ValueError。
安全
与其他一些 popen 函数不同,此实现永远不会隐式调用 /bin/sh。这意味着所有字符,包括 shell 元字符,都可以安全地传递给子进程。
Popen 对象
Popen 类的实例具有以下方法
- poll()
- 检查子进程是否已终止。返回 returncode属性。
- wait()
- 等待子进程终止。返回 returncode属性。
- communicate(input=None)
- 与进程交互:向 stdin 发送数据。从 stdout 和 stderr 读取数据,直到文件结束。等待进程终止。可选的 stdin 参数应该是一个要发送给子进程的字符串,或者如果不需要发送数据给子进程,则为 None。communicate()返回一个元组(stdout, stderr)。注意:读取的数据缓存在内存中,因此如果数据量大或无限,请勿使用此方法。 
还提供以下属性
- stdin
- 如果 stdin参数是PIPE,则此属性是一个文件对象,向子进程提供输入。否则,它为None。
- stdout
- 如果 stdout参数是PIPE,则此属性是一个文件对象,从子进程提供输出。否则,它为None。
- stderr
- 如果 stderr参数是PIPE,则此属性是一个文件对象,从子进程提供错误输出。否则,它为None。
- pid
- 子进程的进程 ID。
- returncode
- 子进程返回码。值为 None表示进程尚未终止。负值 -N 表示子进程被信号 N 终止(仅限 UNIX)。
使用 subprocess 模块替换旧函数
在本节中,“a ==> b”表示 b 可以用作 a 的替代品。
注意:本节中的所有函数在执行的程序找不到时都会(或多或少)静默失败;此模块会引发 OSError 异常。
在以下示例中,我们假设 subprocess 模块已通过 from subprocess import * 导入。
替换 /bin/sh shell 反引号
output=`mycmd myarg`
==>
output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]
替换 shell 管道线
output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
替换 os.system()
sts = os.system("mycmd" + " myarg")
==>
p = Popen("mycmd" + " myarg", shell=True)
sts = os.waitpid(p.pid, 0)
注意
- 通常不需要通过 shell 调用程序。
- 检查 returncode 属性比检查退出状态更容易。
一个更真实的例子是这样的
try:
    retcode = call("mycmd" + " myarg", shell=True)
    if retcode < 0:
        print >>sys.stderr, "Child was terminated by signal", -retcode
    else:
        print >>sys.stderr, "Child returned", retcode
except OSError, e:
    print >>sys.stderr, "Execution failed:", e
替换 os.spawn*
P_NOWAIT 示例
pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
==>
pid = Popen(["/bin/mycmd", "myarg"]).pid
P_WAIT 示例
retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
==>
retcode = call(["/bin/mycmd", "myarg"])
向量示例
os.spawnvp(os.P_NOWAIT, path, args)
==>
Popen([path] + args[1:])
环境示例
os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
==>
Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})
替换 os.popen*
pipe = os.popen(cmd, mode='r', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout
pipe = os.popen(cmd, mode='w', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin
(child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdin, child_stdout) = (p.stdin, p.stdout)
(child_stdin,
 child_stdout,
 child_stderr) = os.popen3(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
(child_stdin,
 child_stdout,
 child_stderr) = (p.stdin, p.stdout, p.stderr)
(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)
替换 popen2.*
注意:如果 popen2 函数的 cmd 参数是一个字符串,则命令通过 /bin/sh 执行。如果它是一个列表,则命令直接执行。
(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
==>
p = Popen(["somestring"], shell=True, bufsize=bufsize
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)
(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode)
==>
p = Popen(["mycmd", "myarg"], bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)
popen2.Popen3 和 popen3.Popen4 基本与 subprocess.Popen 的工作方式相同,除了
- subprocess.Popen在执行失败时会引发异常
- capturestderr参数被 stderr 参数取代。
- 必须指定 stdin=PIPE和stdout=PIPE。
- popen2默认关闭所有文件描述符,但您必须在- subprocess.Popen中指定- close_fds=True。
未解决的问题
已提出一些功能请求,但尚未实现。这包括
- 支持管理一整群子进程
- 支持管理“守护”进程
- 用于终止子进程的内置方法
虽然这些都是有用的功能,但预计这些功能可以在以后无问题地添加。
- 类似 expect 的功能,包括 pty 支持。
pty 支持高度依赖平台,这是一个问题。此外,已经有其他模块提供了这类功能 [6]。
向后兼容性
由于这是一个新模块,预计不会出现主要的向后兼容性问题。模块名称“subprocess”可能与以前其他同名模块 [3] 冲突,但“subprocess”似乎是目前建议的最佳名称。该模块的第一个名称是“popen5”,但这个名称被认为过于不直观。有一段时间,该模块被称为“process”,但这个名称已被 Trent Mick 的模块 [4] 使用。
为了保持向后兼容性,预计该新模块试图替换的函数和模块(os.system、os.spawn*、os.popen*、popen2.*、commands.*)将在未来的 Python 版本中长期可用。
参考实现
参考实现可从 http://www.lysator.liu.se/~astrand/popen5/ 获取。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0324.rst