J'utilise subprocess.check_output()
depuis un certain temps pour capturer la sortie des sous-processus, mais j'ai rencontré des problèmes de performances dans certaines circonstances. J'exécute cela sur une machine RHEL6.
L'appel Python est compilé sous Linux et 64 bits. Le sous-processus que j'exécute est un script Shell qui déclenche finalement un processus Windows python.exe via Wine (pourquoi cette folie est requise est une autre histoire.) En entrée du script Shell, je suis en train de pipeter un petit peu de code Python qui est transmis à python.exe.
Alors que le système est sous une charge modérée/lourde (40 à 70% d'utilisation du processeur), j'ai remarqué que l'utilisation de subprocess.check_output(cmd, Shell=True)
peut entraîner un retard important (jusqu'à ~ 45 secondes) après la fin de l'exécution du sous-processus avant le retour de la commande check_output. L'examen de la sortie de ps -efH
Pendant ce temps montre le sous-processus appelé comme sh <defunct>
, Jusqu'à ce qu'il revienne finalement avec un état de sortie zéro normal.
Inversement, l'utilisation de subprocess.call(cmd, Shell=True)
pour exécuter la même commande sous la même charge modérée/lourde entraînera le retour immédiat du sous-processus sans délai, toutes les sorties imprimées dans STDOUT/STDERR (plutôt que renvoyées par l'appel de fonction).
Pourquoi y a-t-il un retard si important uniquement lorsque check_output()
redirige la sortie STDOUT/STDERR dans sa valeur de retour, et non pas lorsque la call()
la réimprime simplement dans le STDOUT/STDERR du parent?
En lisant les documents, subprocess.call
Et subprocess.check_output
Sont des cas d'utilisation de subprocess.Popen
. Une différence mineure est que check_output
Générera une erreur Python si le sous-processus renvoie un état de sortie non nul. La plus grande différence est soulignée dans le bit sur check_output
(c'est moi qui souligne):
La signature de fonction complète est en grande partie la même que celle du constructeur Popen, sauf que stdout n'est pas autorisé car il est utilisé en interne . Tous les autres arguments fournis sont transmis directement au constructeur Popen.
Alors, comment stdout
"est-il utilisé en interne"? Comparons call
et 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
Maintenant, nous devons également regarder Popen.communicate
. Ce faisant, nous remarquons que pour un tube, communicate
fait plusieurs choses qui prennent simplement plus de temps que de simplement renvoyer Popen().wait()
, comme call
.
D'une part, communicate
traite stdout=PIPE
Que vous définissiez Shell=True
Ou non. De toute évidence, call
ne fonctionne pas. Il laisse simplement votre Shell jaillir quoi que ce soit ... ce qui en fait un risque pour la sécurité, comme Python décrit ici .
Deuxièmement, dans le cas de check_output(cmd, Shell=True)
(un seul canal) ... tout ce que votre sous-processus envoie à stdout
est traité par un thread dans la méthode _communicate
. Et Popen
doit rejoindre le thread (attendre dessus) avant d'attendre que le sous-processus lui-même se termine!
De plus, plus trivialement, il traite stdout
comme un list
qui doit ensuite être joint en une chaîne.
En bref, même avec un minimum d'arguments, check_output
Passe beaucoup plus de temps dans les processus Python que call
.
Regardons le code. Le .check_output a l'attente suivante:
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
L'appel attend avec le code suivant:
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
Notez ce bogue lié à internal_poll. Il est visible sur http://bugs.python.org/issue15756 . À peu près exactement le problème que vous rencontrez.
Edit: L'autre problème potentiel entre .call et .check_output est que .check_output se soucie réellement de stdin et stdout et essaiera d'effectuer IO contre les deux canaux. Si vous exécutez un processus qui se met lui-même dans un état zombie, il est possible qu'une lecture contre un tube dans un état défunt provoque le blocage que vous rencontrez.
Dans la plupart des cas, les états zombies sont nettoyés assez rapidement, mais ils ne le seront pas si, par exemple, ils sont interrompus lors d'un appel système (comme en lecture ou en écriture). Bien sûr, l'appel système en lecture/écriture doit lui-même être interrompu dès que le IO ne peut plus être exécuté, mais il est possible que vous rencontriez une sorte de condition de concurrence où les choses s'améliorent). tué dans un mauvais ordre.
La seule façon à laquelle je peux penser pour déterminer quelle est la cause dans ce cas est que vous ajoutiez du code de débogage au fichier de sous-processus ou que vous invoquiez le débogueur python et que vous lanciez une trace lorsque vous courir dans la condition que vous rencontrez.