关于python:subprocess.check_output与subprocess.call的性能

Performance of subprocess.check_output vs subprocess.call

我已经使用subprocess.check_output()一段时间来捕获子进程的输出,但在某些情况下会遇到一些性能问题。我在RHEL6机器上运行它。

调用Python环境是linux编译的64位。我正在执行的子进程是一个shell脚本,最终通过Wine触发Windows python.exe进程(为什么这个愚蠢是另一个故事)。作为shell脚本的输入,我在一小段Python代码中输入了传递给python.exe的代码。

虽然系统处于中等/重负载(CPU利用率为40%到70%),但我注意到在check_output命令之前子进程完成执行后,使用subprocess.check_output(cmd, shell=True)会导致显着延迟(最多约45秒)回报。在此期间查看ps -efH的输出会将被调用的子进程显示为sh ,直到它最终以正常的零退出状态返回。

相反,使用subprocess.call(cmd, shell=True)在相同的中等/重负载下运行相同的命令将导致子进程立即返回而没有延迟,所有输出都打印到STDOUT / STDERR(而不是从函数调用返回)。

为什么只有当check_output()将STDOUT / STDERR输出重定向到其返回值时才有这么大的延迟,而不是call()只是将它打印回父级的STDOUT / STDERR时?


阅读文档,subprocess.callsubprocess.check_output都是subprocess.Popen的用例。一个小的区别是,如果子进程返回非零退出状态,check_output将引发Python错误。关于check_output(我强调)的一点强调了更大的区别:

The full function signature is largely the same as that of the Popen constructor, except that stdout is not permitted as it is used internally. All other supplied arguments are passed directly through to the Popen constructor.

那么stdout"如何在内部使用"?让我们比较callcheck_output

呼叫

1
2
def call(*popenargs, **kwargs):
    return Popen(*popenargs, **kwargs).wait()

check_output

1
2
3
4
5
6
7
8
9
10
11
12
def check_output(*popenargs, **kwargs):
    if 'stdout' in kwargs:
        raise ValueError('stdout argument not allowed, it will be overridden.')
    process = Popen(stdout=PIPE, *popenargs, **kwargs)
    output, unused_err = process.communicate()
    retcode = process.poll()
    if retcode:
        cmd = kwargs.get("args")
        if cmd is None:
            cmd = popenargs[0]
        raise CalledProcessError(retcode, cmd, output=output)
    return output

通信

现在我们还要查看Popen.communicate。这样做,我们注意到对于一个管道,communicate做了几件事,只需要花费更多时间而不是简单地返回Popen().wait(),就像call那样。

首先,communicate处理stdout=PIPE是否设置shell=True。显然,call没有。正如Python在此描述的那样,它只是让你的shell喷出任何东西......这会带来安全风险。

其次,在check_output(cmd, shell=True)(只有一个管道)的情况下......无论您的子进程发送到stdout的是什么,都由_communicate方法中的线程处理。并且Popen必须加入线程(等待它),然后再等待子进程本身终止!

另外,更简单地说,它将stdout处理为list,然后必须将其连接成一个字符串。

简而言之,即使参数最小,check_output在Python进程中花费的时间也比call多。


我们来看看代码吧。 .check_output具有以下等待:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
            _WNOHANG=os.WNOHANG, _os_error=os.error, _ECHILD=errno.ECHILD):
       """Check if child process has terminated.  Returns returncode
        attribute.

        This method is called by __del__, so it cannot reference anything
        outside of the local scope (nor can any methods it calls).

       """

        if self.returncode is None:
            try:
                pid, sts = _waitpid(self.pid, _WNOHANG)
                if pid == self.pid:
                    self._handle_exitstatus(sts)
            except _os_error as e:
                if _deadstate is not None:
                    self.returncode = _deadstate
                if e.errno == _ECHILD:
                    # This happens if SIGCLD is set to be ignored or
                    # waiting for child processes has otherwise been
                    # disabled for our process.  This child is dead, we
                    # can't get the status.
                    # http://bugs.python.org/issue15756
                    self.returncode = 0
        return self.returncode

.call使用以下代码等待:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    def wait(self):
       """Wait for child process to terminate.  Returns returncode
        attribute."""

        while self.returncode is None:
            try:
                pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
            except OSError as e:
                if e.errno != errno.ECHILD:
                    raise
                # This happens if SIGCLD is set to be ignored or waiting
                # for child processes has otherwise been disabled for our
                # process.  This child is dead, we can't get the status.
                pid = self.pid
                sts = 0
            # Check the pid and loop as waitpid has been known to return
            # 0 even without WNOHANG in odd situations.  issue14396.
            if pid == self.pid:
                self._handle_exitstatus(sts)
        return self.returncode

请注意与internal_poll相关的错误。可在http://bugs.python.org/issue15756查看。几乎就是你遇到的问题。

编辑:.call和.check_output之间的另一个潜在问题是.check_output实际上关心stdin和stdout,并将尝试对两个管道执行IO。如果您遇到一个自身进入僵尸状态的进程,则对处于已解除状态的管道进行读取可能会导致您遇到的挂起。

在大多数情况下,僵尸状态会很快得到清理,但是,如果他们在系统调用中被中断(例如读取或写入),则不会。当然,一旦IO不能再执行,读/写系统调用本身就会被中断,但是,你可能会遇到某种竞争条件,在这种情况下,事情会在错误的顺序中被杀死。

在这种情况下,我能想到确定哪个是原因的唯一方法是,您可以将调试代码添加到子流程文件中,或者在遇到遇到的情况时调用python调试器并启动回溯。