サブプロセスからの出力をキャプチャするために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に出力を戻すのではなく、このような大きな遅延があるのですか?
ドキュメントを読むと、_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()
_
_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つのパイプに対して、communicate
はcall
のように単にPopen().wait()
を返すよりも時間がかかるいくつかの処理を実行することに気付きます。
まず、communicate
は_stdout=PIPE
_を設定したかどうかに関係なく_Shell=True
_を処理します。明らかに、call
はそうではありません。それはあなたのシェルに何でも噴出させるだけです...それをセキュリティリスクにします as Pythonここで説明します 。
次に、check_output(cmd, Shell=True)
(パイプが1つだけ)の場合...サブプロセスがstdout
に送信するものはすべて、thread__communicate
_メソッド内。そしてPopen
は、サブプロセス自体が終了するのをさらに待機する前に、スレッドに参加する(待機する)必要があります!
さらに、もっと簡単に言えば、stdout
をlist
として処理してから、文字列に結合する必要があります。
つまり、最小限の引数を使用しても、_check_output
_はcall
よりもPythonプロセスで多くの時間を費やします。
コードを見てみましょう。 .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デバッガーを呼び出して、バックトレースを開始することです。あなたが経験している状態に遭遇します。