Skip to content

Implement handling POSIX signals #1954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 2, 2025
Merged

Conversation

BCSharp
Copy link
Member

@BCSharp BCSharp commented May 1, 2025

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 of SIGINT 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 prefer PosixSignalRegistration 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 and SIGINFO 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 syscall signal (in Mono.Unix aliased to sys_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 actions Default and Ignore. It builds on top of sys_signal so it should work well with signal above. By itself it does not allow setting custom signal handlers, so it is insufficient to implement Python signal 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 instantiating Mono.Unix.UnixSignal, which is a subclass of WaitHandle. 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 Python signal, 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 registered UnixSignals. There basically two ways of doing it.

  1. One thread that waits on all handlers registered so far (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 which WaitAny is waiting, the thread wait has to be interrupted, and this is easier said than done. Despite UnixSignal deriving from WaitHandle, it does not play by regular WaitHandle rules and is not compatible with them. In practice it means that the only way it can wait on several UnixSignal handles is by using a custom WaitAny static method that only accepts UnixSignals. It is not possible to use Task.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.
  2. One thread per signal. The management of registrations becomes simple, but this is pretty inefficient in terms of resources. On Linux, there can be something like 50 different signal types, so this would mean 50 threads created, all sleeping on a pipe read.

Finally, this method doesn't seem to work well together with SetSignalAction, since SetSignalAction overrides the low level handler, and UnixSignal 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 one UnixSignal is unregistered and another one is immediately registered, the new UnixSignal 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.

if (ReferenceEquals(value, sig_dfl)) {
_signalRegistrations[signalnum] = null;
} else if (ReferenceEquals(value, sig_ign)) {
_signalRegistrations[signalnum] = PosixSignalRegistration.Create((PosixSignal)signalnum,
Copy link
Contributor

@slozier slozier May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the PosixSignal enum match up with the signalnum values? Looking at the docs the .NET enum values are negative while the signalnum are positive?

Edit: nvm, looks like they do say you can cast raw values to the PosixSignal enum in the remarks section. Guess that's why the enum is negative...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the enum values are negative and platform-independent, the positive values are actual platform-dependent signal numbers. Platform-dependent signal numbers is what CPython uses (at least on POSIX, I am still a little bit puzzled by what is going on on Windows).

I think it is a better scheme than what Mono.Unix uses with its enum values, since it is future-proof (the enum values do not cover, nor do they need to cover all available signals on a given platform).

@BCSharp BCSharp merged commit 00fdda4 into IronLanguages:main May 2, 2025
17 checks passed
@BCSharp BCSharp deleted the signal_posix branch May 2, 2025 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants