web-dev-qa-db-fra.com

Performances de subprocess.check_output vs subprocess.call

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?

25
greenlaw

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:

appel

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

communiquer

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.

29
Joseph8th

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.

3
Clarus