Производительность subprocess.check_output vs subprocess.call

Я использовал subprocess.check_output() в течение некоторого времени для захвата вывода из подпроцессов, но при определенных обстоятельствах столкнулся с некоторыми проблемами производительности. Я запускаю это на машине RHEL6.

Вызывающая среда Python скомпилирована в Linux и 64-битная. Подпроцессом, который я выполняю, является оболочка script, которая в конечном итоге запускает процесс python.exe Windows через Wine (почему эта глупость требуется, это еще одна история). В качестве вклада в оболочку script, я пишу небольшую часть кода Python, который передается в файл python.exe.

В то время как система находится под умеренной/большой нагрузкой (от 40 до 70% загрузки процессора), я заметил, что использование subprocess.check_output(cmd, shell=True) может привести к значительной задержке (до ~ 45 секунд) после завершения выполнения подпроцесса возвращается команда check_output. Глядя на вывод из ps -efH в течение этого времени, вызывается вызываемый подпроцесс как sh <defunct>, пока он не вернется с нормальным нулевым статусом выхода.

И наоборот, использование subprocess.call(cmd, shell=True) для запуска той же команды при той же средней/тяжелой нагрузке приведет к немедленному возврату подпроцесса без задержки, все выходные данные будут напечатаны в STDOUT/STDERR (а не возвращены из вызова функции).

Почему существует такая значительная задержка только тогда, когда check_output() перенаправляет вывод STDOUT/STDERR в его возвращаемое значение, а не когда call() просто возвращает его обратно родительскому STDOUT/STDERR?

Ответы

Ответ 1

Чтение документов, как subprocess.call, так и subprocess.check_output являются примерами использования subprocess.Popen. Одно из незначительных отличий заключается в том, что check_output вызывает ошибку Python, если подпроцесс возвращает ненулевой статус выхода. Большая разница подчеркивается в бит около check_output (мой акцент):

Полнофункциональная подпись во многом такая же, как и у конструктора Popen, за исключением того, что stdout не разрешается, поскольку он используется внутри. Все остальные аргументы передаются непосредственно в конструктор Popen.

Итак, как stdout "используется внутри"? Давайте сравним call и check_output:

Вызов

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

check_output

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

общаются h2 >

Теперь мы должны посмотреть на Popen.communicate. Выполняя это, мы замечаем, что для одного канала communicate выполняет несколько действий, которые просто занимают больше времени, чем просто возврат Popen().wait(), как это делает call.

С одной стороны, communicate обрабатывает stdout=PIPE, установлен ли shell=True или нет. Ясно, что call нет. Это просто позволяет вашей оболочке использовать все, что... угроза безопасности, как здесь описывает Python.

Во-вторых, в случае check_output(cmd, shell=True) (всего один канал)... независимо от того, какой ваш подпроцесс отправляет на stdout, обрабатывается потоком в методе _communicate. И Popen должен присоединиться к потоку (ждать на нем), прежде чем дожидаться завершения самого подпроцесса!

Плюс, более тривиально, он обрабатывает stdout как list, который затем должен быть присоединен к строке.

Короче говоря, даже при минимальных аргументах check_output тратит намного больше времени на процессы Python, чем call делает.

Ответ 2

Посмотрите на код. У .check_output следующее ожидание:

    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 ждет, используя следующий код:

    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 и инициировали обратную трассировку, когда вы запускаете условие, в котором вы находитесь испытывают.