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)
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')
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:
__try
, __except
, __finally
, __leave
, RaiseException
, - AddVectoredExceptionHandler
.Objets du répartiteur du noya (c'est-à-dire Objets de synchronisation ), par ex. SetEvent
, SetWaitableTimer
.
Messages de fenêtre, par ex. SendMessage
(vers une procédure de fenêtre), PostMessage
(vers la file d'attente de messages d'un thread à envoyer à une procédure de fenêtre), - PostThreadMessage
(dans la file d'attente de messages d'un thread), WM_CLOSE
, WM_TIMER
.
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.