web-dev-qa-db-ja.com

subprocess.check_outputとsubprocess.callのパフォーマンス

サブプロセスからの出力をキャプチャするためにsubprocess.check_output()をしばらく使用してきましたが、特定の状況下でパフォーマンスの問題が発生しました。これをRHEL6マシンで実行しています。

呼び出しPython環境はlinuxでコンパイルされ、64ビットです。私が実行しているサブプロセスはシェルスクリプトであり、最終的にWineを介してWindows python.exeプロセスを起動します(なぜこの愚かさが必要なのか)もう1つの話です。シェルスクリプトへの入力として、Python python.exeに渡されるコードを少しパイプで入力しています。

システムの負荷が中程度/重い(CPU使用率が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に出力を戻すのではなく、このような大きな遅延があるのですか?

25
greenlaw

ドキュメントを読むと、_subprocess.call_と_subprocess.check_output_の両方が_subprocess.Popen_のユースケースです。 1つの小さな違いは、サブプロセスがゼロ以外の終了ステータスを返す場合、_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
_

通信する

次に、_Popen.communicate_も確認する必要があります。これを行うと、1つのパイプに対して、communicatecallのように単にPopen().wait()を返すよりも時間がかかるいくつかの処理を実行することに気付きます。

まず、communicateは_stdout=PIPE_を設定したかどうかに関係なく_Shell=True_を処理します。明らかに、callはそうではありません。それはあなたのシェルに何でも噴出させるだけです...それをセキュリティリスクにします as Pythonここで説明します

次に、check_output(cmd, Shell=True)(パイプが1つだけ)の場合...サブプロセスがstdoutに送信するものはすべて、thread__communicate_メソッド内。そしてPopenは、サブプロセス自体が終了するのをさらに待機する前に、スレッドに参加する(待機する)必要があります!

さらに、もっと簡単に言えば、stdoutlistとして処理してから、文字列に結合する必要があります。

つまり、最小限の引数を使用しても、_check_output_はcallよりもPythonプロセスで多くの時間を費やします。

29
Joseph8th

コードを見てみましょう。 .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デバッガーを呼び出して、バックトレースを開始することです。あなたが経験している状態に遭遇します。

3
Clarus