Implement handling POSIX signals #1954
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This implementation of handling signals on POSIX uses
PosixSignalRegistration
, which is aviavilable since .NET 6. It is not available on Mono, so on Mono, signal handling is unchanged (i.e. only handling ofSIGINT
is supported). Unfortunately, it also means no full POSIX signal handling on .NET Standard. Mono does provide its own API for handling signals (actually, three different approaches), but all of them come with some deficiencies (see below), so even if I had used them, I would still preferPosixSignalRegistration
on .NET, hence this is the version I submit here.Since it works quite fine on .NET, it is quite unlikely I will be working on an alternative implementation that runs on Mono. Therefore I am writing my findings about the Mono API here just in case it might be useful in the future.
Although both IronPython and CPython require the handler be re-entrant, the thread affinity differs: CPython calls the handler always on the main thread, and IronPython never on the main thread. Probably something worth explaining in "Differences with CPython".
Mono.Unix has three different API schemes to handle signals. All three use the same pattern as the rest of Mono.Unix, that the constants are provided as platform-independent enums, which are then translated to platform specific constants by Mono.Unix through hard-coded switch statements. This means that only those constants are supported that were "known" at the time Mono.Unix was compiled.
Currently IronPython uses Mono.Unix version 7.1.0-final.1.21458.1. This is a pretty old version that is not aware of all signal constants. On macOS 15 it means that
SIGEMT
andSIGINFO
are not supported. It looks like on Linux all signals are supported, but I haven't checked thoroughly.signal
The first way of handling signals with Mono.Unix is with
Mono.Unix.Native.Stdlib.signal
. This function is a thin wrapper over a direct syscallsignal
(in Mono.Unix aliased tosys_signal
), and only adds a little bit of conversions between Mono-speak and the types that the systcall expects (e.g. marshalling). Unfortunately, since it is such a low level call, it puts the burden for all race conditions, thread blocking, and safety on the caller. It is also marked with[Obsolete("This is not safe; use Mono.Unix.UnixSignal for signal delivery or SetSignalAction()")]
, and it looks to me with a good reason.SetSignalAction
Function
Mono.Unix.Native.Stdlib.SetSignalAction
is simple and safe to use, but it only allows to set actionsDefault
andIgnore
. It builds on top ofsys_signal
so it should work well withsignal
above. By itself it does not allow setting custom signal handlers, so it is insufficient to implement Pythonsignal
module.UnixSignal
This is a higher-level API presumably intended to replace the obsolete
signal
. It takes away a number of race conditions, although a few still remain (see below). Registering a custom handler for a signal happens by instantiatingMono.Unix.UnixSignal
, which is a subclass ofWaitHandle
. The user code then can wait on that handle which is signalled when a signal occurs. Disposing it unregisters the handler for that signal. In this way, the user code is in full control on which thread it waits ant processes the signal, and this thread is fully decoupled from the low-level C thread reacting to the system signal. Passing the occurence of a signal from the C thread to the .NET thread is implemented by writing-to/waiting-on a set of Unix pipes, one per signal number.This API is pretty convenient if the goal is to register a handler, wait for a signal, and unregister directly afterwards (e.g. with
using
). Unfortunately, to implement Pythonsignal
, we need something that permanently waits on those handles (permanently = until registration changes), so it requires creating a background thread that waits on a set of registeredUnixSignal
s. There basically two ways of doing it.WaitAny
). This means that signals can only be handled sequentially (arguably a small price to pay). The bigger problem is changing the registrations (e.g. adding/removing a handler for another signal). To change the array on whichWaitAny
is waiting, the thread wait has to be interrupted, and this is easier said than done. DespiteUnixSignal
deriving fromWaitHandle
, it does not play by regularWaitHandle
rules and is not compatible with them. In practice it means that the only way it can wait on severalUnixSignal
handles is by using a customWaitAny
static method that only acceptsUnixSignal
s. It is not possible to useTask.WaitAny
so it is not possible to wake up the signal handling thread by using an extra regular "control" handle. So the solution would require misusing one of the system signals to allow reconfiguration of the handle array. Can be done but it is a pretty clumsy way.Finally, this method doesn't seem to work well together with
SetSignalAction
, sinceSetSignalAction
overrides the low level handler, andUnixSignal
saves and restores the pre-existing handle in an internal structure. To make those two schemes work together, the sequence to switch from one to another must be first remove the old one, then set the new one, which leaves a short moment in between when the handler is set to the default action. For most signals the default action is to terminate the process, so if a signal arrives just in this short period, it will result with unexpected behaviour.Another race condition with
UnixSignal
is that when oneUnixSignal
is unregistered and another one is immediately registered, the newUnixSignal
may incorrectly receive arriving signals intended for the old one. This condition is acknowledged in the code comments but qualified as "unlikely". Maybe so, but still.