classification
Title: Windows: signal doc should state certains signals can't be registered
Type: Stage:
Components: Versions:
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: eryksun, giampaolo.rodola, haypo
Priority: normal Keywords:

Created on 2016-02-12 14:01 by giampaolo.rodola, last changed 2016-02-12 21:48 by serhiy.storchaka.

Messages (3)
msg260179 - (view) Author: Giampaolo Rodola' (giampaolo.rodola) * (Python committer) Date: 2016-02-12 14:01
I'm not sure whether this is a bug with signal.signal doc or with the function itself, anyway, here goes. On UNIX I'm able to register a signal handler for SIGTERM which is executed when the signal is received. On Windows I'm able to register it but it will never be executed:


import os, signal

def foo(*args):
    print("foo")  # this never gets printed on Windows

signal.signal(signal.SIGTERM, foo)
os.kill(os.getpid(), signal.SIGTERM)


I think signal module doc should be more clear about this. In details, if it is possible to execute a function on SIGTERM if should explain how. If not (and AFAIK it's not possible) it should state that "signal.signal(signal.SIGTERM, foo)" on Windows is a NOOP.

Note: I'm probably missing something but the same thing applies for SIGINT and possibly (all) other signals, so I'm not sure why Windows has signal.signal in the first place. What's its use case on Windows?
msg260181 - (view) Author: STINNER Victor (haypo) * (Python committer) Date: 2016-02-12 14:03
See also issue #23948.
msg260201 - (view) Author: Eryk Sun (eryksun) * Date: 2016-02-12 21:01
All of the implemented signals can be registered, and the associated handler can at least be executed by calling the C raise() function. However, for pure-Python code only SIGINT and SIGBREAK are really useful.

FYI, Windows doesn't implement POSIX signals at the system level. The closest analog is an asynchronous procedure call (APC). You can queue a user-mode APC to a thread via QueueUserAPC. It executes when the thread enters an alertable wait (e.g. WaitForSingleObjectEx or SleepEx with bAlertable as TRUE). Kernel-mode APCs get dispatched immediately (when the IRQL is below APC_LEVEL). They're typically used by the I/O manager to complete asynchronous I/O Request Packets (e.g. copying data to a user-mode buffer). 

In principle, NT could implement POSIX-like signals using APCs, but in practice Windows uses other means for the same ends, such as exceptions and structured exception handling (e.g. __try, __except, __finally, __leave, RaiseException, AddVectoredExceptionHandler), dispatch objects (e.g. SetEvent and SetWaitableTimer, which queues an APC), and window messages (e.g. WM_QUIT, WM_TIMER). 

On Windows, the C runtime implements the six signals that are required by standard C: SIGINT, SIGABRT, SIGTERM, SIGSEGV, SIGILL, and SIGFPE. 

SIGABRT and SIGTERM are implemented just for the current process. You can call the handler via C raise(). 

    >>> 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 is useless. 

You also can't do much with SIGABRT using the signal module because the abort() function kills the process once the handler returns, which happens immediately when using the signal module's internal handler (it trips a flag for the Python callable to be called later). Instead use the faulthandler module. Or call the CRT's signal() function via ctypes to set a ctypes callback as the handler.

The CRT implements SIGSEGV, SIGILL, and SIGFPE by setting a Windows structured exception handler (SEH) for the corresponding Windows exceptions:

    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

Use the faulthandler module for these exception-based signals. The way they're implemented is incompatible with Python's signal handler. The exception filter calls the registered handler and then returns EXCEPTION_CONTINUE_EXECUTION. Python's handler only trips a flag for the interpreter to call the registered Python callable at a later time. So the code that triggered the exception will trigger again, and so on in an endless loop. 

That leaves SIGINT, to which Windows adds the non-standard SIGBREAK. Both console and non-console processes can raise() one of these signals, but only a console process can receive them from another process. 

The CRT sets a console control event handler via SetConsoleCtrlHandler. When the console sends the process a CTRL_C_EVENT or CTRL_BREAK_EVENT, the CRT's handler calls the associated SIGINT or SIGBREAK handler. 

Note that this is implemented by creating a new thread in the process that begins executing at kernel32!CtrlRoutine. Unlike SIGINT on POSIX, the handler does not execute on the main thread (hijacking a thread is taboo in Windows). This can lead to synchronization problems that Python 3 attempts to work around by using a Windows event object.

You can send a control event to all processes attached to the current console via GenerateConsoleCtrlEvent. You can target a subset of processes that belong to a process group, or send the event to all processes by targeting process group 0. 

What the console does when the target ID isn't a process group ID is undefined. It basically acts like the target is group 0, but that shouldn't be relied on. (It's most likely a bug.) It can also mess up the console's list of attached processes (i.e. GetConsoleProcessList) by adding non-console processes. 

The docs for os.kill clearly state that you can only send signal.CTRL_C_EVENT and signal.CTRL_BREAK_EVENT on Windows. Any other value is passed to TerminateProcess as the exit code, which kills the process without notice (like POSIX SIGKILL). 

It also states that "[t]he Windows version of kill() additionally takes process handles to be killed", which I don't think was ever true. That line needs to be removed.

It also fails to clarify that the target has to be a process group ID, and what that is. Every process in a Windows session belongs to a process group, even if it's just the wininit.exe group (services session) or winlogon.exe group (interactive session). A new group is created by passing the creation flag CREATE_NEW_PROCESS_GROUP when creating a new process. The group ID is the process ID of the created process. AFAIK, the console is the only system that uses the process group, and that's just for GenerateConsoleCtrlEvent. 

Don't rely on being able to send CTRL_C_EVENT to anything but group 0, since it's initially disabled in a new process group. It's not impossible to send this event to a new group, but the target process first has to enable CTRL_C_EVENT by calling SetConsoleCtrlHandler(NULL, FALSE).

CTRL_BREAK_EVENT is all you can depend on since it can't be disabled. Sending this event is a simple way to gracefully kill a child process that was started with CREATE_NEW_PROCESS_GROUP, assuming it has a Windows CTRL_BREAK_EVENT or C SIGBREAK handler. If not, the default handler will terminate the process, setting the exit code to STATUS_CTRL_C_EXIT. For example:

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

Notice that CTRL_BREAK_EVENT wasn't sent to the current process, because I targeted the process group of the child process (including all of its child processes that are attached to the console, and so on). If I had used group 0, the current process would have been killed as well since I didn't define a SIGBREAK handler. Let's try that, but with a handler set:

    >>> c_break = lambda *a: print('^BREAK')
    >>> signal.signal(signal.SIGBREAK, c_break)
    <Handlers.SIG_DFL: 0>
    >>> os.kill(0, signal.CTRL_BREAK_EVENT)
    ^BREAK
History
Date User Action Args
2016-02-12 21:48:24serhiy.storchakasettitle: Windoes: signal doc should state certains signals can't be registered -> Windows: signal doc should state certains signals can't be registered
2016-02-12 21:01:57eryksunsetnosy: + eryksun
messages: + msg260201
2016-02-12 14:03:19hayposetmessages: + msg260181
2016-02-12 14:02:43hayposetnosy: + haypo
2016-02-12 14:01:43giampaolo.rodolacreate