web-dev-qa-db-fra.com

Comment gérer le signal dans python sur la machine Windows

J'essaie le code collé ci-dessous sur Windows, mais au lieu de gérer le signal, il tue le processus. Cependant, le même code fonctionne dans Ubuntu.

import os, sys
import time
import signal
def func(signum, frame):
    print 'You raised a SigInt! Signal handler called with signal', signum

signal.signal(signal.SIGINT, func)
while True:
    print "Running...",os.getpid()
    time.sleep(2)
    os.kill(os.getpid(),signal.SIGINT)
20
Ramu

Python os.kill encapsule deux API non liées sous Windows. Il appelle GenerateConsoleCtrlEvent lorsque le paramètre sig est CTRL_C_EVENT Ou CTRL_BREAK_EVENT. Dans ce cas, le paramètre pid est un ID de groupe de processus. Si ce dernier appel échoue, et pour toutes les autres valeurs sig, il appelle OpenProcess puis TerminateProcess . Dans ce cas, le paramètre pid est un ID de processus et la valeur sig est transmise comme code de sortie. Mettre fin à un processus Windows revient à envoyer SIGKILL à un processus POSIX. En règle générale, cela doit être évité car il ne permet pas au processus de se terminer proprement.

Notez que les documents pour os.kill Prétendent à tort que "kill () prend en outre les poignées de processus à tuer", ce qui n'a jamais été vrai. Il appelle OpenProcess pour obtenir un descripteur de processus.

La décision d'utiliser WinAPI CTRL_C_EVENT Et CTRL_BREAK_EVENT, Au lieu de SIGINT et SIGBREAK, est regrettable pour le code multiplateforme. Il n'est pas non plus défini ce que fait GenerateConsoleCtrlEvent lorsqu'il reçoit un ID de processus qui n'est pas un ID de groupe de processus. L'utilisation de cette fonction dans une API qui prend un ID de processus est au mieux douteuse et potentiellement très mauvaise.

Pour vos besoins particuliers, vous pouvez écrire une fonction d'adaptateur qui rend os.kill Un peu plus convivial pour le code multiplateforme. Par exemple:

import os
import sys
import time
import signal

if sys.platform != 'win32':
    kill = os.kill
    sleep = time.sleep
else: 
    # adapt the conflated API on Windows.
    import threading

    sigmap = {signal.SIGINT: signal.CTRL_C_EVENT,
              signal.SIGBREAK: signal.CTRL_BREAK_EVENT}

    def kill(pid, signum):
        if signum in sigmap and pid == os.getpid():
            # we don't know if the current process is a
            # process group leader, so just broadcast
            # to all processes attached to this console.
            pid = 0
        thread = threading.current_thread()
        handler = signal.getsignal(signum)
        # work around the synchronization problem when calling
        # kill from the main thread.
        if (signum in sigmap and
            thread.name == 'MainThread' and
            callable(handler) and
            pid == 0):
            event = threading.Event()
            def handler_set_event(signum, frame):
                event.set()
                return handler(signum, frame)
            signal.signal(signum, handler_set_event)                
            try:
                os.kill(pid, sigmap[signum])
                # busy wait because we can't block in the main
                # thread, else the signal handler can't execute.
                while not event.is_set():
                    pass
            finally:
                signal.signal(signum, handler)
        else:
            os.kill(pid, sigmap.get(signum, signum))

    if sys.version_info[0] > 2:
        sleep = time.sleep
    else:
        import errno

        # If the signal handler doesn't raise an exception,
        # time.sleep in Python 2 raises an EINTR IOError, but
        # Python 3 just resumes the sleep.

        def sleep(interval):
            '''sleep that ignores EINTR in 2.x on Windows'''
            while True:
                try:
                    t = time.time()
                    time.sleep(interval)
                except IOError as e:
                    if e.errno != errno.EINTR:
                        raise
                interval -= time.time() - t
                if interval <= 0:
                    break

def func(signum, frame):
    # note: don't print in a signal handler.
    global g_sigint
    g_sigint = True
    #raise KeyboardInterrupt

signal.signal(signal.SIGINT, func)

g_kill = False
while True:
    g_sigint = False
    g_kill = not g_kill
    print('Running [%d]' % os.getpid())
    sleep(2)
    if g_kill:
        kill(os.getpid(), signal.SIGINT)
    if g_sigint:
        print('SIGINT')
    else:
        print('No SIGINT')

Discussion

Windows n'implémente pas de signaux au niveau du système [*]. Le runtime C de Microsoft implémente les six signaux requis par le standard C: SIGINT, SIGABRT, SIGTERM, SIGSEGV, SIGILL et SIGFPE.

SIGABRT et SIGTERM sont implémentés uniquement pour le processus actuel. Vous pouvez appeler le gestionnaire via C raise . Par exemple (dans Python 3.5):

>>> import signal, ctypes
>>> ucrtbase = ctypes.CDLL('ucrtbase')
>>> c_raise = ucrtbase['raise']
>>> foo = lambda *a: print('foo')
>>> signal.signal(signal.SIGTERM, foo)
<Handlers.SIG_DFL: 0>
>>> c_raise(signal.SIGTERM)
foo
0

SIGTERM est inutile.

Vous ne pouvez pas non plus faire grand-chose avec SIGABRT en utilisant le module de signal car la fonction abort tue le processus une fois que le gestionnaire revient, ce qui se produit immédiatement lors de l'utilisation du module de signal gestionnaire interne (il déclenche un indicateur pour le Python appelable à appeler dans le thread principal). Pour Python 3, vous pouvez utiliser à la place le faulthandler module. Ou appelez la fonction signal du CRT via ctypes pour définir un rappel ctypes comme gestionnaire.

Le CRT implémente SIGSEGV, SIGILL et SIGFPE en définissant un Windows gestionnaire d'exceptions structuré pour les exceptions Windows correspondantes:

STATUS_ACCESS_VIOLATION          SIGSEGV
STATUS_ILLEGAL_INSTRUCTION       SIGILL
STATUS_PRIVILEGED_INSTRUCTION    SIGILL
STATUS_FLOAT_DENORMAL_OPERAND    SIGFPE
STATUS_FLOAT_DIVIDE_BY_ZERO      SIGFPE
STATUS_FLOAT_INEXACT_RESULT      SIGFPE
STATUS_FLOAT_INVALID_OPERATION   SIGFPE
STATUS_FLOAT_OVERFLOW            SIGFPE
STATUS_FLOAT_STACK_CHECK         SIGFPE
STATUS_FLOAT_UNDERFLOW           SIGFPE
STATUS_FLOAT_MULTIPLE_FAULTS     SIGFPE
STATUS_FLOAT_MULTIPLE_TRAPS      SIGFPE

L'implémentation de ces signaux par le CRT est incompatible avec la gestion des signaux par Python. Le filtre d'exception appelle le gestionnaire enregistré et renvoie ensuite EXCEPTION_CONTINUE_EXECUTION . Cependant, le gestionnaire de Python déclenche un indicateur pour que l'interpréteur appelle l'appelable enregistré quelque temps plus tard dans le thread principal. Ainsi, le code errant qui a déclenché l'exception continuera de se déclencher dans une boucle sans fin. Dans Python 3, vous pouvez utiliser le module de gestionnaire de fautes pour ces signaux basés sur des exceptions.

Cela laisse SIGINT, auquel Windows ajoute le non-standard SIGBREAK. Les processus console et non console peuvent raise ces signaux, mais seul un processus console peut les recevoir d'un autre processus. Le CRT implémente cela en enregistrant un gestionnaire d'événements de contrôle de console via SetConsoleCtrlHandler .

La console envoie un événement de contrôle en créant un nouveau thread dans un processus attaché qui commence à s'exécuter à CtrlRoutine dans kernel32.dll ou kernelbase.dll (non documenté). Le fait que le gestionnaire ne s'exécute pas sur le thread principal peut entraîner des problèmes de synchronisation (par exemple dans le REPL ou avec input). De plus, un événement de contrôle n'interrompra pas le thread principal s'il est bloqué en attendant un objet de synchronisation ou en attendant la fin des E/S synchrones. Il faut prendre soin d'éviter le blocage dans le thread principal s'il doit être interruptible par SIGINT. Python 3 essaie de contourner ce problème en utilisant un objet événement Windows, qui peut également être utilisé dans les attentes qui devraient être interruptibles par SIGINT.

Lorsque la console envoie au processus un CTRL_C_EVENT Ou CTRL_BREAK_EVENT, Le gestionnaire du CRT appelle respectivement le gestionnaire enregistré SIGINT ou SIGBREAK. Le gestionnaire SIGBREAK est également appelé pour le CTRL_CLOSE_EVENT Que la console envoie lorsque sa fenêtre est fermée. Python utilise par défaut la gestion de SIGINT en effaçant un KeyboardInterrupt dans le thread principal. Cependant, SIGBREAK est initialement la valeur par défaut CTRL_BREAK_EVENT, qui appelle ExitProcess(STATUS_CONTROL_C_EXIT).

Vous pouvez envoyer un événement de contrôle à tous les processus attachés à la console actuelle via GenerateConsoleCtrlEvent. Cela peut cibler un sous-ensemble de processus qui appartiennent à un groupe de processus ou le groupe cible 0 pour envoyer l'événement à tous les processus attachés à la console actuelle.

Les groupes de processus ne sont pas un aspect bien documenté de l'API Windows. Il n'y a pas d'API publique pour interroger le groupe d'un processus, mais chaque processus d'une session Windows appartient à un groupe de processus, même s'il ne s'agit que du groupe wininit.exe (session de services) ou du groupe winlogon.exe (session interactive). Un nouveau groupe est créé en passant le drapeau de création CREATE_NEW_PROCESS_GROUP Lors de la création d'un nouveau processus. L'ID de groupe est l'ID de processus du processus créé. À ma connaissance, la console est le seul système qui utilise le groupe de processus, et c'est juste pour GenerateConsoleCtrlEvent.

Ce que fait la console lorsque l'ID cible n'est pas un ID de groupe de processus n'est pas défini et ne doit pas être utilisé. Si à la fois le processus et son processus parent sont attachés à la console, l'envoi d'un événement de contrôle agit essentiellement comme si la cible est le groupe 0. Si le processus parent n'est pas attaché à la console actuelle, GenerateConsoleCtrlEvent échoue et os.kill appelle TerminateProcess. Bizarrement, si vous ciblez le processus "Système" (PID 4) et son processus enfant smss.exe (gestionnaire de session), l'appel réussit mais rien ne se passe sauf que la cible est ajoutée par erreur à la liste des processus attachés (ie GetConsoleProcessList ). C'est probablement parce que le processus parent est le processus "inactif", qui, puisqu'il s'agit du PID 0, est implicitement accepté comme PGID de diffusion. La règle de processus parent s'applique également aux processus non-console. Le ciblage d'un processus enfant non console ne fait rien - sauf qu'il endommage par erreur la liste des processus console en ajoutant le processus non attaché. J'espère qu'il est clair que vous ne devez envoyer un événement de contrôle qu'au groupe 0 ou à un groupe de processus connu que vous avez créé via CREATE_NEW_PROCESS_GROUP .

Ne comptez pas sur la possibilité d'envoyer CTRL_C_EVENT À autre chose qu'au groupe 0, car il est initialement désactivé dans un nouveau groupe de processus. Il n'est pas impossible d'envoyer cet événement à un nouveau groupe, mais le processus cible doit d'abord activer CTRL_C_EVENT En appelant SetConsoleCtrlHandler(NULL, FALSE).

CTRL_BREAK_EVENT Est tout ce sur quoi vous pouvez compter car il ne peut pas être désactivé. L'envoi de cet événement est un moyen simple de tuer avec élégance un processus enfant qui a été démarré avec CREATE_NEW_PROCESS_GROUP, En supposant qu'il possède un gestionnaire Windows CTRL_BREAK_EVENT Ou C SIGBREAK. Sinon, le gestionnaire par défaut mettra fin au processus, en définissant le code de sortie sur STATUS_CONTROL_C_EXIT. Par exemple:

>>> import os, signal, subprocess
>>> p = subprocess.Popen('python.exe',
...         stdin=subprocess.PIPE,
...         creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
>>> os.kill(p.pid, signal.CTRL_BREAK_EVENT)
>>> STATUS_CONTROL_C_EXIT = 0xC000013A
>>> p.wait() == STATUS_CONTROL_C_EXIT
True

Notez que CTRL_BREAK_EVENT N'a pas été envoyé au processus en cours, car l'exemple cible le groupe de processus du processus enfant (y compris tous ses processus enfants attachés à la console, etc.). Si l'exemple avait utilisé le groupe 0, le processus actuel aurait également été tué puisque je n'avais pas défini de gestionnaire SIGBREAK. Essayons cela, mais avec un ensemble de gestionnaires:

>>> ctrl_break = lambda *a: print('^BREAK')
>>> signal.signal(signal.SIGBREAK, ctrl_break)
<Handlers.SIG_DFL: 0>
>>> os.kill(0, signal.CTRL_BREAK_EVENT)
^BREAK

[*]

Windows a appels de procédure asynchrones (APC) pour mettre en file d'attente une fonction cible sur un thread. Voir l'article Inside NT's Asynchronous Procedure Call pour une analyse approfondie des APC Windows, en particulier pour clarifier le rôle des APC en mode noyau. Vous pouvez mettre en file d'attente un APC en mode utilisateur sur un thread via QueueUserAPC . Ils sont également mis en file d'attente par ReadFileEx et WriteFileEx pour la routine d'achèvement des E/S.

Un APC en mode utilisateur s'exécute lorsque le thread entre dans une attente d'alerte (par exemple WaitForSingleObjectEx ou SleepEx avec bAlertable as TRUE). Les APC en mode noyau, en revanche, sont envoyés immédiatement (lorsque l'IRQL est inférieur à APC_LEVEL). Ils sont généralement utilisés par le gestionnaire d'E/S pour terminer les paquets de demande d'E/S asynchrones dans le contexte du thread qui a émis la demande (par exemple, copier des données de l'IRP vers un tampon en mode utilisateur). Voir Attentes et APC pour un tableau qui montre comment les APC affectent les attentes alertables et non alertables. Notez que les APC en mode noyau n'interrompent pas une attente, mais sont plutôt exécutés en interne par la routine d'attente.

Windows pourrait implémenter des signaux de type POSIX en utilisant des APC, mais en pratique, il utilise d'autres moyens pour les mêmes fins. Par exemple:

Des messages de fenêtre peuvent être envoyés et publiés à tous les threads qui partagent l'appel bureau du thread et qui sont au même niveau d'intégrité ou inférieur. L'envoi d'un message de fenêtre le place dans une file d'attente système pour appeler la procédure de fenêtre lorsque le thread appelle PeekMessage ou GetMessage . La publication d'un message l'ajoute à la file d'attente de messages du thread, qui a un quota par défaut de 10 000 messages. Un thread avec une file d'attente de messages doit avoir une boucle de message pour traiter la file d'attente via GetMessage et DispatchMessage . Les threads d'un processus uniquement sur console n'ont généralement pas de file d'attente de messages. Cependant, le processus hôte de la console, conhost.exe, le fait évidemment. Lorsque le bouton de fermeture est cliqué, ou lorsque le processus principal d'une console est tué via le gestionnaire de tâches ou taskkill.exe , un message WM_CLOSE Est publié dans la file d'attente de messages de la console fil de la fenêtre. La console envoie à son tour un CTRL_CLOSE_EVENT À tous ses processus attachés. Si un processus gère l'événement, il lui est accordé 5 secondes pour se terminer correctement avant de se terminer avec force.

64
Eryk Sun