Hacker Timesnew | past | comments | ask | show | jobs | submitlogin
The perils of pause(2) (2023) (cipht.net)
60 points by g0xA52A2A on March 4, 2024 | hide | past | favorite | 21 comments


Is there any case where anything remotely related to signals is not full of perils? UNIX has plenty of rough edges, but signals must be one of the worst..


Is there even a way to correctly deal with signals in the first place? The more posts like this I read, the more I start to feel that there is no 100% correct way of handling signals, you can at best minimize the probability of getting hit by one at a wrong moment. Which is OK in real life, but feels bad for a fundamental part of an OS.


There definitely are correct patterns for handling signals -- the problem is more that there are also quite a lot of "looks plausible but has a race condition" ways to write code.

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


I think the right choice depends on the kind of signal:

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


I think so, it's just typically a pain. For example using `ppoll()` or `pselect()` is not that bad really, but it requires you to have centered your program around such a call and have access to it (IE. It's not buried in a UI framework or network library somewhere). Beyond that, leaving signals masked for the majority of your program makes things a lot simpler (if you're multi-threaded, I'd just create a completely separate thread for signals and mask them on all the others, or use `signalfd` if you can).

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.


> Is there even a way to correctly deal with signals in the first place?

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.


Nope. There is essentially no way to handle Unix signals correctly. Signals are fundamentally broken.

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 much cleaner and portable solution to the races triggered by duplicated and early signals is to set up a pipe and have signal handlers write the signal number to that pipe.

A write syscall is permitted in a signal handler and now you can (p)select/poll the signals like any other file descriptor


It's mentioned in the article:

> There's also the classic self-pipe trick.


And ideally, have a thread for that that you spawn in the beginning, then block signals on the main thread before spawning any other thread or initializing any library that might potentially do so.


I'll take "Things that the OS should probably be doing itself but instead we end up re-doing it in the userspace, poorly, for historical reasons" for 400, Alex.



Why people still didn't deprecate signal handlers (except for 15)?


Obligatory https://ldpreload.com/blog/signalfd-is-useless?reposted-on-r... post. :)

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.


One of the root problems with the UNIX signals IMHO is that they conflate 3 different useful concepts together.

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


This seems like another instance of the same general race condition often referred to as the lost/missed wakeup problem.


This isn't really a signals problem. This is a fundamentally racy API. Any decision of the form "go to sleep until this condition is true" can't be done reliably unless the code that decides "this condition" is atomic with respect to the entrance to the sleep state. That is, if the condition might change after you've decided it's false but before you suspend, you might never wake up.

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.


pause() is fine, as long as you don't do it conditionally.


What's a "non-conditional" pause though? Doing it in a main loop is still adjacent to a "while(something_to_do)" which isn't synchronized.

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.


If everything you wish to achieve in response to a signal can be achieved within the signal handler, you do not need a condition. Just `while (1) pause();`.

Granted, this isn't a very common situation, but I can imagine a few use-cases.


sigprocmask is a neat solution, but requires a syscall. Would it be theoretically possible to share the signal mask between the userspace and the kernel in no-cost way? I'm thinking vDSO style, mmaped var.

This could have big implications, for example most classic coroutine implementations using setjmp do some procmask dance.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: