PEP 324 -- subprocess 新的进程模块(subprocess - New process module)

英文原文:https://www.python.org/dev/peps/pep-0324/

采集日期:2021-05-13

PEP: 324

Title: subprocess - New process module

Version: $Revision$

Author: Peter Astrand astrand@lysator.liu.se

Status: Final

Type: Standards Track

Created: 19-Nov-2003

Python-Version: 2.4

Post-History:

目录

摘要(Abstract)


本文描述了一个新模块,用于启动进程并与之通讯。

动机(Motivation)


不管用什么编程语言,启动新的进程都是一项常见的任务,特别是 Python 这种高级语言更是十分常见。为此提供支持是很有必要的,原因如下:

  • 用不合适的函数启动进程,可能会存在安全风险:如果程序是通过 shell 启动的,并且参数中包含了shell 元(meta)字符,结果可能会比较惨。[^注1]

  • 这让 Python 成为了更好的替代语言,替换过于复杂的shell脚本。

当前 Python 有很多不同的函数用于创建进程,让开发人员难以选择。

subprocess模块比以前函数的改进之处:

  • 用一个“统一”的模块,提供了以前函数的所有功能。
  • 支持跨进程异常:在开始执行新的进程之前,发生在子进程中的异常会在父进程中再次触发。这就意味着对exec()执行失败就很容易处理。而比如用popen2就无法检测执行是否失败。
  • 在 fork 和 exec 之间提供了钩子(hook),以便执行自定义代码。可被用于改变 uid 之类的操作。
  • 不会隐式调用 /bin/sh。这就意味着不必对危险的 shell 元字符进行转义了。
  • 允许对文件描述符进行所有的重定向组合。比如,“python-dialog”[^注2]需要生成一个进程,并对 stderr 做重定向,但不对 stdout 做重定向。如果不使用临时文件,用目前的函数是不可能做到的。
  • 利用 subprocess 模块,能够在启动新程序前控制是否关闭所有打开的文件描述符。
  • 支持将多个子进程连接起来(shell 的管道“pipe”)
  • 为换行符提供统一的支持。
  • 提供了communicate()方法,使得发送 stdin 数据及读取 stdout、stderr 数据变得容易,且没有死锁的风险。大多数人都知道要注意子进程通信时的流控问题,但不是所有人都有耐心和技巧编写出完全正确且无死锁的循环选择过程(select loop)。 这意味着会有很多 Python 应用程序带有竞态条件。标准库中有个communicate()方法可解决这个难题。

原由(Rationale)


设计思路汇总如下:

  • subprocess 基于 popen2 实现,因其已久经考验。

  • popen2 的工厂方法已被去除,因为类构造函数用起来同样简单。

  • popen2包含很多工厂方法和类,用于各种重定向组合。而 subprocess 中只有1个类。因为 subprocess 模块支持12种不同的重定向组合,再为每种组合提供1个类或函数就有点累赘,也不太直观。即便是针对 popen2,清晰度也存在问题。比如离开了文档,很多人说不出 popen2.popen2 和popen2.popen4 的区别。

  • 提供一个工具小函数:subprocess.call()。目标是os.system()的增强版,易用性仍旧很好。

    • 不采用标准的C函数system(),因其有缺陷。
    • 不会隐式调用 shell。
    • 无需用引号,而是采用参数列表。
    • 返回值更易于处理。

    正如Popen类构造函数那样,工具函数call()可接受1个 'args' 参数。等待命令完成,并返回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 扩展模块。

特性(Specification)


本模块定义了一个名为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是个序列,则第1个数据项就是命令,其他数据项则都被视为shell参数。

    在Windows平台:Popen类采用CreateProcess()执行子程序,其只认字符串。如果args是个序列,则会用list2cmdline方法转换为字符串。请注意,并不是所有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为 True,则在执行子进程之前,除 0、1、2 之外的所有文件描述符都会关闭。

  • 如果shell为 True,命令将会通过shell执行。

  • 如果cwd不为None,则在子进程执行之前,当前目录将会改为cwd。

  • 如果env不为None,则将其定义为新进程的环境变量。

  • 如果universal_newlines为 True,文件对象stdout和stderr将打开为文本文件,但每行将由以下任一符号结束: Unix 行结束符\n、Macintosh 行结束符\r或 Windows 行结束符\r\n。这些符号都将被 Python 视为\n。请注意,仅当Python编译时带上通用换行支持(默认)时,该特性才会生效。此外,文件对象 stdout、stdin 和 stderr 的换行符属性不会被communication()方法更新。

  • 如果给出了startupinfo和creationflags,则会传给底层的CreateProcess()函数。可用于指定主窗口的外观和新进程的优先级等。(仅限 Windows)

本模块还定义了两个便捷函数:

  • call(*args, **kwargs)带上实参运行某命令。等待命令运行结束,然后返回returncode属性。

    参数与Popen的构造函数相同。例如:

      retcode = call(["ls", "-l"])

异常(Exceptions)


在开始执行新程序之前,子进程中触发的异常将会在父进程中重新触发。此外,异常对象将带有一个名为“child_traceback”的额外属性,这是一个字符串,包含了从子进程角度看到的回调信息。

最常见的异常就是OSErrors。例如,在尝试执行的文件不存在时就会发生这种情况。应用程序应对OSErrors有所准备。

如果 popen 的参数非法,则会触发ValueError。

安全性(Security)

与其他一些 popen 函数不同,此处代码永远不会隐式调用 /bin/sh。这意味着所有字符,包括 shell 元字符,都可以安全地传给子进程。

Popen 对象(Popen objects)


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 模块替换旧函数(Replacing older functions with the subprocess module)


本节中的“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"])

Vector 示例:

    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才行。

未决议题(Open Issues)


有些特性已有人提出要求,但尚未实现。包括:

  • 对子进程族的管理功能。
  • “守护”进程的管理功能。
  • 提供杀死子进程的内置方法。

当然这些特性是很有用,预计后续添加也毫无问题。

  • 貌似需要的功能,包括 pty 的支持。

pty 功能高度依赖于平台,这是一个难题。并且已有其他模块提供了该类功能 [^注6]。

向下兼容性(Backwards Compatibility)


这是一个新模块,估计不会出现重大的向下兼容问题。模块名称“subprocess”可能会与以前的同名模块[^注3]发生冲突,但名称“subprocess”似乎是迄今为止最好的名称。本模块的第一个名字是“popen5”,但觉得太不直观了。有一段时间,本模块被称为“process”,但已被 Trent Mick 的模块[^注4]用掉了。

为了保持向下兼容,预计在未来很长一段时间内,本模块试图替换的函数和模块(os.system、os.spawn*、os.popen*、popen2.*、commands.*)在后续 Python 版本中依然可用。

参考实现代码(Reference Implementation)


http://www.lysator.liu.se/~astrand/popen5/

参考文献


[注1] Linux 和 Unix 安全编程指南,第 8.3 节。http://www.dwheeler.com/secure-programs/

[注2] Python Dialog http://pythondialog.sourceforge.net/

[注3] http://www.iol.ie/~padraiga/libs/subProcess.py

[注4] http://starship.python.net/crew/tmick/

[注5] http://starship.python.net/crew/mhammond/win32/

[注6] http://www.lysator.liu.se/~ceder/pcl-expect/

版权(Copyright)

本文已于公共区域发布。

PEP 324 subprocess 新的进程模块 -- Python官方文档译文 [原创]的更多相关文章

  1. 别开心太早,Python 官方文档的翻译差远了

    近几天,很多公众号发布了 Python 官方文档的消息.然而,一个特别奇怪的现象就发生了,让人啼笑皆非. Python 文档的中文翻译工作一直是“默默无闻”,几个月前,我还吐槽过这件事<再聊聊P ...

  2. [Python3]Python官方文档-Python Manuals

    简介 一般情况下,初学者都不愿意直接去浏览Python Manuals,即Python自带的官方文档.尤其是只有英文版的情况下,初学者更加不会去使用该官方文档了. 在这里笔者强力推荐初学者经常学会使用 ...

  3. 阅读Python官方文档心得

    我会每天都阅读一些python的官方文档,并每天更新心得体会. -------------------------------------------------2016.12.08--------- ...

  4. 通读Python官方文档之wsgiref(未完成)

    wsgirf-WSGI功能及参考实现 源码:Lib/wsgiref Web服务器网关接口(Web Server Gateway Interface, WSGI),是用Python写的一个服务器软件和w ...

  5. Python 官方文档解读(2):threading 模块

    使用 Python 可以编写多线程程序,注意,这并不是说程序能在多个 CPU 核上跑.如果你想这么做,可以看看关于 Python 并行计算的,比如官方 Wiki. Python 线程的主要应用场景是一 ...

  6. 通读Python官方文档之cgi

    cgi 通用网关接口 前驱知识 网关协议学习:CGI.FastCGI.WSGI 简单点说: web服务器接受请求,启动CGI:CGI接受请求,处理,返回给服务器:服务器返回给用户 cgi效率不高,每次 ...

  7. Python 官方文档解读(1):66 个内置函数

    Python 解释器 (CPython 3.7)内置有 66 个函数,这些函数在任何时刻都是可用的.此文是为了对这 66 个函数进行简单的梳理,便于以后可能用到它们时能想到. 1. abs(x) 返回 ...

  8. reactor官方文档译文(2)Reactor-core模块

    You should never do your asynchronous work alone. — Jon Brisbin 完成Reactor 1后写到 You should never do y ...

  9. Python 官方文档:入门教程

    https://pythoncaff.com/docs/tutorial/3.7.0 官方入门教程,从这里开始你的 Python 之旅,将长久维护 基础信息 翻译说明 关于本教程 已完成 正文 1. ...

随机推荐

  1. 『动善时』JMeter基础 — 8、JMeter主要元件介绍

    目录 1.测试计划(Test Plan) 2.线程组 3.取样器(sampler) 4.逻辑控制器(Logic Controller) 5.配置元件(Config Element) 6.定时器(Tim ...

  2. QQ可以登录,网页无法访问

    之前以为是网络的问题,但是,后来发现QQ却可以登录,这是第二次碰到这个情况,所以总结一下 一般我们电脑上都会安装一个杀毒软件,比如火绒等等.我电脑上用的联想自带的电脑管家 我们可以在工具箱中找到网络诊 ...

  3. JavaScript 原始值与包装对象

    前言 随着 JavaScript 越来越流行,越来越多地开发者开始接触并使用 JavaScript. 同时我也发现,有不少开发者对于 JavaScript 最基本的原始值和包装对象都没有很清晰的理解. ...

  4. Windows进程间通讯(IPC)----WM_COPYDATA

    WM_COPYDATA通讯思路 通过向其他进程的窗口过程发送WM_COPYDATA消息可以实现进程间通讯. 只能通过SendMessage发送WM_COPYDATA消息,而不能通过PostMessag ...

  5. Codeforces Beta Round #73(Div2)

    A - Chord 题意:就是环中有12个字符,给你三个字符,判断他们之间的间隔,如果第一个和第二个间隔是3并且第二个和第三个间隔是4,那么就输出minor,如果第一个和第二个间隔是4并且第二个和第三 ...

  6. JVM内存溢出后服务还能运行吗

    文章开篇问一个问题吧,一个java程序,如果其中一个线程发生了OOM,那进程中的其他线程还能运行吗? 接下来做实验,看看JVM的六种OOM之后程序还能不能访问. 在这里我用的是一个springboot ...

  7. 论文翻译:Conv-TasNet: Surpassing Ideal Time–Frequency Magnitude Masking for Speech Separation

    我醉了呀,当我花一天翻译完后,发现已经网上已经有现成的了,而且翻译的比我好,哎,造孽呀,但是他写的是论文笔记,而我是纯翻译,能给读者更多的思想和理解空间,并且还有参考文献,也不错哈,反正翻译是写给自己 ...

  8. 『动善时』JMeter基础 — 17、JMeter配置元件【HTTP请求默认值】

    目录 1.HTTP请求默认值介绍 2.HTTP请求默认值界面 3.HTTP请求默认值的使用 (1)用于演示的项目说明 (2)测试计划内包含的元件 (3)说明HTTP请求默认值用法 4.总结 5.拓展知 ...

  9. [bug] org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 2

    原因 SpringBoot启动加载yml配置文件出现编码格式错误 参考 https://www.pianshen.com/article/2431144034/

  10. [刷题] 3 Longest Substring Without Repeating Character

    要求 在一个字符串中寻找没有重复字母的最长子串 举例 输入:abcabcbb 输出:abc 细节 字符集?字母?数字+字母?ASCII? 大小写是否敏感? 思路 滑动窗口 如果当前窗口没有重复字母,j ...