Python or not, almost nothing is safe inside a signal handler.
The ones related to application logic must only be handled with signalfd if you want any semblance of reliability.
That's a bit of an overstatement? There's a list of things that you _can_ call, and fairly useful ones too like `write`
If the signal occurs other than as the result of calling abort(), raise(),
[CX] [Option Start] kill(), pthread_kill(), or sigqueue(), [Option End] the
behavior is undefined if the signal handler refers to any object with static
storage duration other than by assigning a value to an object declared as
volatile sig_atomic_t, or if the signal handler calls any function in the
standard library other than one of the functions listed in Signal Concepts.
You literally can't read any global/static variables and you can only write to global/static variables that are declared to be volatile sig_atomic_t. This tremendously shrinks the amount of useful work you can do with the signal-safe functions from the standard library.In fact, here is another, a very fresh, example from POSIX: [0]. There is an example at how to use SIGWINCH signal handler with tcgetwinsize(). Just look at this thing of terrible beauty, notice that SIG_ATOMIC_MAX is not required to bigger than a byte's worth of data, and also read the whole of "APPLICATION USAGE" section. "Multi-threaded applications should avoid the signal handler idiom in general", gee, I wonder why. And of course, the signal may never be generated in the first place, so "[s]uch processes must periodically poll the current terminal window size if needed". What a solid technical foundation to build race-free, bug-free applications on top of.
[0] https://pubs.opengroup.org/onlinepubs/9799919799/functions/t...
That's how most hardware interrupts would also work.
Should you use signal handlers with eBPF?
Well, I'd probably just get rid of them entirely? For example, Windows functions just fine without them: SIGINT is emulated by the kernel doing essentially pthread_create(..., ®istered_ctrl_c_handler) — which neatly sidesteps the question of "what existing thread receives the signal" by answering "none of them" — SIGILL/SIGSEGV are handled with SEH because that's essentially what they are, the exceptions; SIGSTOP/SIGKILL are instead proper process-management "syscalls" SuspendProcess/TerminateProcess (seriously, why is what essentially are ioctls on a pidfd was bolted onto signals, it makes zero sense), and most of other asynchronous signals are just done via different channels.
Honestly, the most useful things about the signals is that they will interrupt your syscalls with EINTR so your code may have a chance to look around and notice things that happened while it was blocked inside the kernel.
> a complete set of patterns for safe concurrency?
Pfft, that's easy: synchronous rendezvous and message queues; alternatively, CSP. We've known this answer since the early 70s.
Let's call them crippled interrupts. As long as you don't think of them as a message queue...
In their defense, the original signals were there mostly for "handle this or I'll terminate you" conditions. Then stuff got bolted on...
[1] Possibly LLM hallucinated translation from a Romanian poet. Although it did give me a link to a paywalled essay that I couldn't verify.
The other one that plays nicely with threads is blocking the signals everywhere with pthread_sigmask and parking one dedicated thread in sigwait(). Both are in the stdlib on Unix.
signalfd is nicer than either but it's Linux only, which is why set_wakeup_fd usually wins if you care about portability.
If your program generates signals, it could generate a flood unintentionally. If you put a print statement in the handler, you can get this result.
It's not terribly surprising, but good to know.
The C printf function is not async signal safe and is one of the examples in the manual: https://man7.org/linux/man-pages/man7/signal-safety.7.html