python windows signals

Cómo manejar la señal en Python en Windows.



signals (1)

Estoy intentando el código pegado a continuación en Windows, pero en lugar de manejar la señal, está matando el proceso. Sin embargo, el mismo código funciona en 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)


os.kill de Python envuelve dos APIs no relacionadas en Windows. Llama a GenerateConsoleCtrlEvent cuando el parámetro sig es CTRL_C_EVENT o CTRL_BREAK_EVENT . En este caso, el parámetro pid es un ID de grupo de proceso. Si la última llamada falla, y para todos los demás valores sig , llama a OpenProcess y luego a TerminateProcess . En este caso, el parámetro pid es un ID de proceso, y el valor sig se pasa como el código de salida. Terminar un proceso de Windows es similar a enviar SIGKILL a un proceso POSIX. En general, esto debe evitarse ya que no permite que el proceso salga limpiamente.

Tenga en cuenta que los documentos para os.kill afirman erróneamente que "kill () además requiere que los manejadores de procesos sean eliminados", lo que nunca fue cierto. Llama a OpenProcess para obtener un identificador de proceso.

La decisión de usar WinAPI CTRL_C_EVENT y CTRL_BREAK_EVENT , en lugar de SIGINT y SIGBREAK , es desafortunada para el código multiplataforma. Tampoco se define qué hace GenerateConsoleCtrlEvent cuando se pasa una ID de proceso que no es una ID de grupo de proceso. El uso de esta función en una API que toma un ID de proceso es dudoso en el mejor de los casos, y potencialmente muy incorrecto.

Para sus necesidades particulares, puede escribir una función de adaptador que haga que os.kill sea ​​un poco más amigable para el código multiplataforma. Por ejemplo:

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'')

Discusión

Windows no implementa señales a nivel del sistema [*]. El tiempo de ejecución de C de Microsoft implementa las seis señales requeridas por el estándar C: SIGINT , SIGABRT , SIGTERM , SIGSEGV , SIGILL y SIGFPE .

SIGABRT y SIGTERM se implementan solo para el proceso actual. Puedes llamar al manejador a través de C raise . Por ejemplo (en 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 es inútil.

Tampoco se puede hacer mucho con SIGABRT usando el módulo de señal porque la función de abort mata el proceso una vez que el manejador regresa, lo que sucede inmediatamente cuando se usa el manejador interno del módulo de señal (dispara una marca para que se llame al Python llamado en el Hilo principal). Para Python 3, en su lugar puede usar el módulo faulthandler . O llame a la función de signal del CRT a través de ctypes para establecer una devolución de llamada de ctypes como el manejador.

El CRT implementa SIGSEGV , SIGILL y SIGFPE al configurar un controlador de excepciones estructurado de Windows para las excepciones de Windows correspondientes:

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

La implementación de estas señales por parte del CRT es incompatible con el manejo de señales de Python. El filtro de excepción llama al controlador registrado y luego devuelve EXCEPTION_CONTINUE_EXECUTION . Sin embargo, el controlador de Python solo dispara una bandera para que el intérprete llame al timbre registrado en algún momento más adelante en el hilo principal. Por lo tanto, el código errante que activó la excepción continuará activándose en un bucle sin fin. En Python 3 puede usar el módulo faulthandler para estas señales basadas en excepciones.

Eso deja a SIGINT , a lo que Windows agrega el SIGBREAK no estándar. Tanto los procesos de consola como los que no son de consola pueden raise estas señales, pero solo un proceso de consola puede recibirlas de otro proceso. El CRT implementa esto registrando un controlador de eventos de control de consola a través de SetConsoleCtrlHandler .

La consola envía un evento de control creando un nuevo hilo en un proceso adjunto que comienza a ejecutarse en CtrlRoutine en kernel32.dll o kernelbase.dll (no documentado). Que el controlador no se ejecute en el subproceso principal puede provocar problemas de sincronización (por ejemplo, en el REPL o con la input ). Además, un evento de control no interrumpirá el subproceso principal si está bloqueado mientras espera en un objeto de sincronización o espera a que se complete la E / S síncrona. Se debe tener cuidado para evitar el bloqueo en el hilo principal si debe ser interrumpible por SIGINT . Python 3 intenta solucionar esto mediante el uso de un objeto de evento de Windows, que también puede usarse en esperas que SIGINT debería poder interrumpir.

Cuando la consola envía al proceso un CTRL_C_EVENT o CTRL_BREAK_EVENT , el controlador del CRT llama al controlador SIGINT o SIGBREAK registrado, respectivamente. También se SIGBREAK controlador SIGBREAK para el CTRL_CLOSE_EVENT que la consola envía cuando se cierra su ventana. Python utiliza de forma predeterminada el manejo de SIGINT al escribir un KeyboardInterrupt en el hilo principal. Sin embargo, SIGBREAK es inicialmente el controlador predeterminado CTRL_BREAK_EVENT , que llama a ExitProcess(STATUS_CONTROL_C_EXIT) .

Puede enviar un evento de control a todos los procesos conectados a la consola actual a través de GenerateConsoleCtrlEvent . Esto puede apuntar a un subconjunto de procesos que pertenecen a un grupo de procesos, o grupo de destino 0 para enviar el evento a todos los procesos conectados a la consola actual.

Los grupos de procesos no son un aspecto bien documentado de la API de Windows. No hay una API pública para consultar el grupo de un proceso, pero cada proceso en una sesión de Windows pertenece a un grupo de proceso, incluso si es solo el grupo wininit.exe (sesión de servicios) o el grupo winlogon.exe (sesión interactiva). Se crea un nuevo grupo al pasar el indicador de creación CREATE_NEW_PROCESS_GROUP al crear un nuevo proceso. El ID de grupo es el ID de proceso del proceso creado. Que yo sepa, la consola es el único sistema que usa el grupo de procesos, y eso es solo para GenerateConsoleCtrlEvent .

Lo que hace la consola cuando el ID de destino no es un ID de grupo de proceso no está definido y no se debe confiar en él. Si tanto el proceso como su proceso principal están vinculados a la consola, entonces el envío de un evento de control actúa básicamente como si el objetivo fuera el grupo 0. Si el proceso principal no está vinculado a la consola actual, entonces GenerateConsoleCtrlEvent falla y os.kill llamadas TerminateProcess . Extrañamente, si se dirige al proceso del "Sistema" (PID 4) y su proceso secundario smss.exe (administrador de la sesión), la llamada se realiza correctamente pero no sucede nada excepto que el objetivo se agrega por error a la lista de procesos adjuntos (es decir, GetConsoleProcessList ). Probablemente se deba a que el proceso principal es el proceso "Inactivo", que, dado que es PID 0, se acepta implícitamente como el PGID de difusión. La regla de proceso principal también se aplica a los procesos que no son de consola. Dirigirse a un proceso secundario que no sea de consola no hace nada, excepto corromper erróneamente la lista de procesos de la consola al agregar el proceso sin adjuntar. Espero que quede claro que solo debe enviar un evento de control al grupo 0 oa un grupo de proceso conocido que haya creado a través de CREATE_NEW_PROCESS_GROUP .

No confíe en poder enviar CTRL_C_EVENT a cualquier cosa que no sea el grupo 0, ya que inicialmente está deshabilitado en un nuevo grupo de procesos. No es imposible enviar este evento a un nuevo grupo, pero el proceso objetivo primero tiene que habilitar CTRL_C_EVENT llamando a SetConsoleCtrlHandler(NULL, FALSE) .

CTRL_BREAK_EVENT es todo lo que puede confiar ya que no se puede deshabilitar. Enviar este evento es una forma sencilla de eliminar con gracia un proceso hijo que se inició con CREATE_NEW_PROCESS_GROUP , asumiendo que tiene un controlador CTRL_BREAK_EVENT Windows o C SIGBREAK . De lo contrario, el controlador predeterminado terminará el proceso y establecerá el código de salida en STATUS_CONTROL_C_EXIT . Por ejemplo:

>>> 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

Tenga en cuenta que CTRL_BREAK_EVENT no se envió al proceso actual, porque el ejemplo se dirige al grupo de procesos del proceso hijo (incluidos todos los procesos secundarios que están conectados a la consola, etc.). Si el ejemplo hubiera usado el grupo 0, el proceso actual también se habría eliminado ya que no SIGBREAK un controlador SIGBREAK . Intentemos eso, pero con un conjunto de controladores:

>>> 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 tiene llamadas de procedimiento asíncrono (APC) para poner en cola una función de destino a un hilo. Consulte el artículo de la Llamada al Procedimiento Asíncrono de NT para obtener un análisis en profundidad de las APC de Windows, especialmente para aclarar la función de las APC en modo kernel. Puede poner en cola un APC en modo usuario en un subproceso a través de QueueUserAPC . También se ponen en cola por ReadFileEx y WriteFileEx para la rutina de finalización de E / S.

Un APC en modo usuario se ejecuta cuando el subproceso entra en una espera de alerta (por ejemplo, WaitForSingleObjectEx o SleepEx con bAlertable como TRUE ). Los APC en modo kernel, por otro lado, se envían inmediatamente (cuando el IRQL está por debajo de APC_LEVEL ). Normalmente, el administrador de E / S los utiliza para completar los paquetes de solicitud de E / S asíncronas en el contexto del subproceso que emitió la solicitud (p. Ej., Copiar datos del IRP a un búfer de modo de usuario). Consulte Esperas y APC para ver una tabla que muestra cómo los APC afectan a las esperas con alerta y sin alerta. Tenga en cuenta que los APC en modo kernel no interrumpen una espera, sino que se ejecutan internamente mediante la rutina de espera.

Windows podría implementar señales similares a POSIX utilizando APC, pero en la práctica utiliza otros medios para los mismos fines. Por ejemplo:

Los mensajes de la ventana se pueden enviar y publicar en todos los subprocesos que comparten el escritorio del subproceso de la llamada y que se encuentran en el mismo nivel de integridad o inferior. El envío de un mensaje de ventana lo coloca en una cola del sistema para llamar al procedimiento de ventana cuando el hilo llama a PeekMessage o GetMessage . La publicación de un mensaje lo agrega a la cola de mensajes del hilo, que tiene una cuota predeterminada de 10,000 mensajes. Un hilo con una cola de mensajes debe tener un bucle de mensajes para procesar la cola a través de GetMessage y DispatchMessage . Los subprocesos en un proceso de solo consola generalmente no tienen una cola de mensajes. Sin embargo, el proceso de host de la consola, conhost.exe, obviamente lo hace. Cuando se hace clic en el botón de cerrar, o cuando el proceso principal de una consola se taskkill.exe través del administrador de tareas o taskkill.exe , se taskkill.exe un mensaje WM_CLOSE en la cola de mensajes del hilo de la ventana de la consola. La consola, a su vez, envía un CTRL_CLOSE_EVENT a todos sus procesos adjuntos. Si un proceso maneja el evento, tiene 5 segundos para salir con gracia antes de que termine por la fuerza.