The perils of pause(2) (2023)
cipht.net
cipht.net
The whole signals infrastructure in Unix has always been a bit dubious IMHO -- my impression from reading the Sixth Edition sources in the Lions book was that it looked like it had basically been put in to handle "signals are fundamentally fatal" cases like ctrl-C of a process from the terminal or SIGSEGV and friends, with the signal handler really only being there to do cleanup before the process exited. If that's all signals are doing then none of the race condition stuff matters (including the traditional "when a signal handler is called it resets to SIG_DFL" race that was fixed by BSD-style signals) and nor do you need to care about the enormous mountain of pain that is having to handle EINTR return values from system calls. But once the mechanism was there it got used for other things, many of which involved "the process receives and deals with signals as part of its normal continuing execution".
1. SigPipe and similar synchronous signals can be disabled. You already get an error code from the IO operation anyways, so they're redundant. It's kind of useful for traditional unix command line tools, since it'll just terminate the process when the output pipe gets closed.
2. SigInt and similar messages from other processes, should be handled via signalfd or the self-pipe trick. That way they look like any other asynchronous message that might come in via a pipe or socket.
3. SigSegV and other severe errors can just terminate the process
Windows chose a different approach:
SigSegV is a (structured) exception which causes unwinding. Unwinding has its own set of challenges, especially as language boundaries are crossed.
SigInt calls a registered callback on a separate thread, so the handler is not subjected in the same limitations as a signal of an existing thread, and can use the same synchronization primitives as any other multi threaded code. GUI applications have a message queue per window, which is used for similar purposes (sometimes it makes sense for a console application to create a hidden window to receive such messages).
Handling the actual signals themselves is annoying, but in concept it's similar to any kind of async code - the only big catch is that understanding which functions you can or cannot call in a signal handler is pretty messy. Some signals also shouldn't be bothered to try and recover from, Ex. SIGSEGV. Some other signals are also just poorly designed for a multi-threaded environment and there's better alternatives to watching for them. Only some like SIGINT are really worth supporting IMO, and the signal handler should do little more than set a flag to be handled elsewhere in the regular program flow.
There is, but it requires a level of rigor and overhead that tends to make them poor choices for practical engineering. Signals are, at their core, 1:1 isomorphic to interrupts being delivered to a physical CPU. The stop execution and transfer it to a different location at an arbitrary/uncontrollable time. Operating systems deal with this problem robustly all the time, they have to.
What you typically do as the fundamental building block is provide some way of "masking" the interrupts (e.g. sigaction() with appropriate flags for every signal you might receive) so that they won't occur while you're inspecting some critical state. On hardware this is generally cheap, but with signals it's a whole system call (or many, if there are multiple signals to deal with). So generally this isn't the common technique.
So you fall back to the other OS technique, typically used for non-maskable interrupts: don't touch anything. You can't call OS APIs safely (because they might have been preempted in arbitrary states), so you're limited to doing lockless algorithms that can safely update the shared data in a way that can be inspected later. Then maybe you might queue up an "event" or something to get the interrupted context to re-inspect state "soon", etc...
The second option generally is what unix processes try to do. Except that it's really hard (because it's isomorphic to lockless coding!), and easy to get wrong. So signals ended up with the reuptation of being fundamentally broken.
They're not. They have a home and can be used robustly. Just... don't, unless you really really need them for some reason.
The best way to deal with them that I've found is to turn them off by masking out every signal. That lets me reliably switch to Linux's signalfd with epoll or io_uring which finally makes it sane. The BSDs seem to have similar mechanisms.
A write syscall is permitted in a signal handler and now you can (p)select/poll the signals like any other file descriptor
> There's also the classic self-pipe trick.
It's a divisive article, but I generally agree, saving you a thread is basically what it mostly does, and there's already enough boilerplate out there to do that. Plus you sacrifice portability.
The first concept is that of asynchronous external events that the process would like to react to in a timely manner (e.g. SIGINT): those can be processed on a dedicated thread, maybe it could be created anew every time a signal is delivered a la Windows [0], maybe it could be created once at the moment of execve and ELF format should get a "secondary entry point" field for it, whatever.
The second concept is that of synchronous internal events that the process has caused itself (e.g. SIGSEGV, SIGILL): well, those are just low-level exceptions, and could be processed on the thread that caused them. Some sort of structural exception handling [1] (only less awkward) could work, I think?
An interesting mix of those two kinds of signals is SIGALRM, which is an asynchronous internal event. Can be quite useful for implementing lighter-weighted/cooperative multitasking, I guess: it basically makes the OS to regularly punt your instruction pointer somewhere else where you could e.g. flush some memory and raise semaphores so a sibling thread could notice and report the progress to the user, while the main thread returns back to number crunching, or you could use it to to force coroutines/fibers to yield or something. To be fair, not sure what the proper interface for this should look like.
And the third concept is that of the control operations acting on the process itself, the operations that the process gets no chance to react to: SIGKILL, SIGSTOP, SIGCONT. They are not really process signals, they are the API of the process manager/scheduler that were implemented with the signals machinery because what was the other choice? Introducing new system calls?
[0] https://learn.microsoft.com/en-us/windows/console/handlerrou...
[1] https://learn.microsoft.com/en-us/windows/win32/debug/struct...
There are lots of solutions to this. Synchronizing along a "queue" a-la file descriptors or semaphores is the most common. The generalization of this "suspend until" problem is captured by the "condition variable" abstraction, which combines the sleep with a lock that provides mutual exclusion and can be released atomically on suspend.
But the pause() system call didn't understand any of that, so it's unresolvably buggy without some extra layer somewhere to act as a backstop and wake up incorrectly-sleeping processes.
I mean, yes, there are circumstances where you don't need a full condition variable. But those tend to be fragile and not subject to "just follow this one rule" design principles, and so should be avoided. Broadly I stand by the point: pause() as designed was a mistake and you shouldn't touch it. If you want signals to integrate into your main loop use signalfd() instead.
Granted, this isn't a very common situation, but I can imagine a few use-cases.
This could have big implications, for example most classic coroutine implementations using setjmp do some procmask dance.