4

PEP 324:子进程管理 subprocess

 9 months ago
source link: https://www.biaodianfu.com/pep-324-subprocess-new-process-module.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

器→工具, 编程语言

PEP 324:子进程管理 subprocess

钱魏Way · 2023-12-15 · 1 次浏览

PEP 324,全称为 “subprocess – New process module”,是 Python 编程语言中的一项重要提案。这个提案由 Peter Astrand 在 2003 年提出,最终被包含在 Python 2.4 版本中。PEP 324 引入了 subprocess 模块,这个模块是用来创建和管理外部进程的。以下是关于 PEP 324 的详细介绍:

PEP 324 的主要内容

subprocess 模块:

  • 提供了一个更强大、灵活的接口来替代旧的进程创建和管理函数,如system() 和 os.spawn*()。
  • Popen 类:是该模块的核心,用于创建和管理外部进程。
  • 执行外部命令和程序。
  • 管理输入输出流,允许对子进程的标准输入、输出和错误进行重定向。
  • 支持复杂的进程间通信。
  • 提供了统一的接口来替代多个老旧的模块和函数。
  • 增加了跨平台的一致性。
  • 提供了更好的错误检查和异常处理能力。

PEP 324 的目的

  • 简化进程创建和管理:通过 subprocess 模块,简化了在 Python 中创建和管理外部进程的方式。
  • 提高安全性和可靠性:相比于旧的方法,subprocess 模块提供了更安全和可靠的方式来执行外部命令。
  • 增强功能和灵活性:提供了更多的控制选项和功能,如流重定向和进程间通信。

实际应用

  • subprocess 模块被广泛应用于需要执行和管理外部命令或脚本的 Python 程序中。它的应用范围从简单的命令执行到复杂的进程间通信都有涉及。
subprocess.png

摘要(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.popen4 的区别。
  • 提供一个工具小函数:call()。目标是os.system()的增强版,易用性仍旧很好。
    • 不采用标准的C函数system(),因其有缺陷。
    • 不会隐式调用 shell。
    • 无需用引号,而是采用参数列表。
    • 返回值更易于处理。

正如Popen类构造函数那样,工具函数call()可接受1个 ‘args’ 参数。等待命令完成,并返回returncode。实现代码非常简单:

def call(*args, **kwargs):
return Popen(*args, **kwargs).wait()
def call(*args, **kwargs):
    return Popen(*args, **kwargs).wait()

call() 函数的设计初衷很简单:启动进程并等待其·完成,这是一种很常见的任务。

而 Popen 则支持很多可选参数,很多用户需要简洁的形式。目前还有很多人在用 os.system(),主要原因就是它的接口比较简洁。比如:

os.system("stty sane -F " + device)
os.system("stty sane -F " + device)

采用subprocess.call()可能就会是以下方式:

subprocess.call(["stty", "sane", "-F", device])
subprocess.call(["stty", "sane", "-F", device])

或者,如果要通过 shell 执行,则如下:

subprocess.call("stty sane -F " + device, shell=True)
subprocess.call("stty sane -F " + device, shell=True)
  • 提供“预执行(preexec)”能力,以便在 fork 和 exec 之间执行任何代码。也许有人会问,为什么特地有参数用于设置环境变量和目录,却没有设置 uid 之类的参数,答案就是:
    • 修改环境变量和工作目录相当常用。
    • 类似spawn()之类的传统函数已经支持了“env”参数。
    • env和cwd很大程度上被视为跨平台的,在Windows平台中也能生效。
  • 在POSIX平台中,不需要用到扩展模块:只用到了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):
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类采用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"])
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]
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]
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)
sts = os.system("mycmd" + " myarg")
==>
p = Popen("mycmd" + " myarg", shell=True)
sts = os.waitpid(p.pid, 0)
  • 通常没有必要通过 shell 调用程序。
  • 查看 returncode 属性要比查看退出状态更为容易。

现实中的代码实例可能会如下所示:

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
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
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"])
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.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.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)
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)
(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一样,除了:

  • 如果执行失败,Popen将触发异常。
  • capturestderr参数将用 stderr 替换。
  • stdin=PIPE和stdout=PIPE必须指定。
  • popen2默认会关闭所有文件描述符,而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/

参考链接:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK