Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Dark Side of Posix APIs (vorner.github.io)
95 points by ingve on May 17, 2021 | hide | past | favorite | 23 comments


I clicked expecting to read about signals and was not disappointed. Dark corners of POSIX is putting it lightly. There just seems to be no correct way to deal with them.

The best solution I know is signalfd, a Linux feature. Block all the traditional signal handlers then epoll a signal file descriptor in a perfectly normal event loop. When a signal is received, it's possible to read instances of signalfd_siginfo from it. Even this has limitations such as being unable to handle SIGSEGV or SIGFPE.

https://man7.org/linux/man-pages/man2/signalfd.2.html


> There just seems to be no correct way to deal with them.

No, you can definitely do it correctly.

Signals are more than one thing. They can be divided into three buckets:

* asynchronous, process-directed. When you press ctrl-C, the terminal driver generates a SIGINT to the process to handle at its leisure. It can be delivered to any thread of the process and isn't necessarily delivered immediately.

* asynchronous, thread-directed. pthread_sigkill sends this.

* synchronous, thread-directed. A null-pointer dereference (or other access to unmapped memory) causes a SIGSEGV to that specific thread. It's delivered immediately; letting the thread continue for a while first doesn't make sense.

* (synchronous, process-directed isn't a thing IIRC and wouldn't make sense)

Let's just talk about asynchronous, process-directed because that's what most people mean when they say signals.

There are low-level techniques to handle them correctly. If you have an event loop, my favorite is the "self-pipe trick". At process start time, create a pipe and set the O_NONBLOCK flag. Hold onto both ends. From the signal handler, write into the pipe (ignoring EAGAIN). In your event loop, read from the pipe. When there's something to read, a signal is pending. This gives you roughly the same thing as signalfd but is cross-platform.

The best high-level technique IMHO is to write your code in a language that has a common library for this stuff. The article is about signal-hook, which is commonly used in Rust.


> If you have an event loop, my favorite is the "self-pipe trick".

That's nice. Seems good to me, similar to signalfd.

What if you don't have an event loop though? When I tried to learn this I couldn't find any sane way to use signal handler functions. There's just too many caveats. With event loops I can dedicate a thread to the task, the code executes in a normal context and everything works as expected.

> This gives you roughly the same thing as signalfd but is cross-platform.

Roughly? What are the differences? The most obvious to me is the fact I can read signalfd_siginfo structures from the file descriptor.


> What if you don't have an event loop though?

You can use my second-favorite technique: dedicate a thread to signals. Block the signals of interest, keeping them queued rather than running signal handlers. Do this early in main(), before spawning any threads, so that they'll inherit the block. Spawn a thread which dequeues in a loop via sigwaitinfo or sigtimedwait. Signal handling without a signal handler!

> Roughly? What are the differences [between the self-pipe trick and signalfd]? The most obvious to me is the fact I can read siginfo structures from the file descriptor.

That's the difference I mean. If you want to get those from the self-pipe trick, you need to do it yourself, dealing with the platform difference crap this article describes. Additionally, if you write more than one byte per signal, you'll have to be careful of half-written messages, since your signal handler can't block for capacity. (Why? It may be blocking the thread that does the reading, so it'd deadlock.) Maybe you'd have a separate buffer for the leftover part that doesn't quite fit, or ensure your your message size divides evenly into the pipe buffer capacity (see F_GETPIPE_SZ), or drop messages if there's insufficient capacity (use FIONREAD; the capacity shouldn't go down unless you're using the same pipe for multiple signals and don't have them masking each other out). Stuff like this can get complex but the basic technique of just ensuring ctrl-C reaches your event loop isn't that bad. You have to be very careful in the signal handler, but it's like five lines long, so life goes on.

Another difference is that installing a self-pipe handler lets you see the previous handler (if any). I've heard of chaining handlers to address complaints like kstenerud's about libraries. I've never actually wanted to share signals like that, but it's something one could try. Caveat: if you also want to uninstall signals, doing that out of order wouldn't go well.


Except that once you do this, no one else can listen for that signal anymore. Pending signals go to the first to register for them, and then are no longer pending, so no one else gets notified. Aaaand you don't get notified of this problem - your registration call succeeds but you get nothing when a signal comes.

The Android runtime library uses similar tricks for ANRs, with similar problems.

Signals are probably the most broken of all POSIX APIs.


> Except that once you do this, no one else can listen for that signal anymore.

What do you mean by that? Who else is there other than the process being signaled?

> Signals are probably the most broken of all POSIX APIs.

I agree completely.


>> Except that once you do this, no one else can listen for that signal anymore.

> What do you mean by that? Who else is there other than the process being signaled?

A library that needs to be notified of certain signals and perhaps also their contents.


Yeah, that's going to be a huge problem. Libraries shouldn't be doing that. They should leave signal handling to the caller and provide the functions that should be called in those cases. Just like memory allocation, initialization...


Which is great in theory, except that signal handlers are such an esoteric body of knowledge that very few people can do it at all, let alone do it properly. So, many libraries do it for them (which works so long as nobody uses the blocking or blocking-dependent APIs - which is generally true). Even the JVM taps SIGQUIT for dumping process info.


The workaround for this you can find in SDL2 and some other libraries with event loops is having a default, library-provided setup routine and event loop.

Know what you're doing? Set up signals yourself. Don't? Library sets it up for you.


> Long story short, while Rust’s libc bindings give one access to the si_code field, it doesn’t export the actual constants to know what that value means. While the maintainers are generally open to adding these constants, I don’t have access to all the large number of platforms to figure out what values the constants have on each (yes, they are not the same) and what constants are even available on each of them

https://github.com/jart/cosmopolitan/blob/7cbc2bc0838a88ff46...


Is there a full and complete example of how to properly handle signals in a multi-threaded program that does the usual stuff (talk to disk, talk on the network)? Written in plain C?

I think I know everything that I'm supposed to do (on Linux, for example). But I wouldn't mind looking through an example that handles everything correctly. Something that is designed as an example, as opposed to reading through some much larger and older project.


It is very hard to do it properly in correct way, given how OS specific and underspecified their behaviour is.

The easiest way is just to set a variable to mark the occurrence of a signal and nothing else. Then check for changes from other parts of the program.

Also be sure to check every C library error result and the value that it was interrupted by signal occurrence, and eventually retry if needed.


On the subject of signals and other problems with UNIX I can recommend Neil Brown's Ghosts of Unix past, part 3: Unfixable designs[1]

[1]: https://lwn.net/Articles/414618/


Just try to do the same across more UNIX implementations for more POSIX joy.

Like most design by committee standards there is plenty of write once debug everywhere.


> Like most design by committee standards there is plenty of write once debug everywhere.

Are there any non-committee counter examples?


Proprietary APIs.


write once debug everywhere is how the web works too, and i think it is just a fun fact of life that when you speak, each listener may understand you differently, and you have to think about it and account for it?


Anyone that doesn't think Web === ChromeOS needs to think and account for it.

However in about 10 years that might not matter anymore.


I think retro-Web will only continue growing, as will sites which care about accessibility by the long tail of older devices.

If anything, I think Chrome will eventually split off into its own AOL, while the Web will continue on.


Signals are one of the things that I think UNIX (and later POSIX) got horribly, horribly wrong. POSIX makes it hard to do things that people would love to do with signals. And their safety issues are hard to encapsulate even in a language like Rust (because you can only call async-signal-safe functions from a signal handler, and those are pretty far and few between).

And sadly, Linux manages to do some of its own custom things here that makes it even worse. One thing I find helpful sometimes is being able to inspect the actual hardware trap number, which can help distinguish between a SIGSEGV caused by a pointer to memory that doesn't exist (which is the normal cause) or an unaligned memory access in an instruction that requires aligned access [1]. There's a field for it in siginfo_t, si_trapno... but Linux doesn't fill in si_trapno for x86 nor even provide it as a field to be provided, unlike the BSDs in both regards. However, Linux does helpfully provide the trap number in the mccontext_t struct... which is only accessible via the third argument of a signal handler, and not via any ptrace access for a debugger.

If I were to redesign signals, I'd start by dividing them up into two categories: synchronous signals and asynchronous signals. This division actually already exists, but only in terms of how a signal is dispatched; I'd extend it so that the details of what the signals look like and how they are handled are completely different.

Synchronous signals are those that are caused by processor interrupts (think a page fault), or some set of user-custom dispatchable signals (e.g., being able to request thread cancellation). These would be handled like a regular try/catch scope, although with additional opportunities for first-ditch and last-ditch handling--Windows SEH is the kind of programming model I'd go for. What this allows is being able to do something like "I'm going to call some custom code, and if it causes a segmentation fault, I can report an error and unwind the stack to code that's unaffected" (and possibly report a full stacktrace in the meantime, maybe even as the default handler if uninstalled). This kind of handling allows it to be scoped on a per-thread basis, unlike the current per-process signal handlers.

Asynchronous signals would instead be handled by inserting them into a per-process queue of unhandled signals. There would be some syscall for getting the next unhandled signal, and naturally something that could feed into whatever syscall you use for "wait until something interesting happens" (which on Linux would mean something akin to signalfd). At this point, dequeuing signals is now a relatively benign step, and you can handle this with a simple thread that does nothing but dequeues signals and sticks them into a thread-safe queue for other threads to check at their leisure. No more need to worry about async-signal-safety!

[1] Side side rant: On x86, Linux maps the #AC (alignment check) exception to SIGBUS. But #AC isn't used for SSE aligned vector move instructions, instead #GP is used. #GP is instead mapped to SIGSEGV, but Linux doesn't fill in many of the fields. (Not that I think x86 fills in the faulted address for a #GP exception, but still, no si_trapno field to easily distinguish a #PF SIGSEGV versus #GP SIGSEGV). It took me several hours of pulling my hair out before I realized I was getting #GP and not #PF...


I'm pretty sure unaligned stores on ARM can give you a SIGBUS too, so that's another thing you need to keep track of…


Hmm. This seems like a good use of lock-free programming. Its not a technique you want to use very often (lock-free is known by a small minority of programmers, and is difficult to do), but... if mutexes can't work and you only can rely upon memory-level assumptions... that's the sort of stuff that load_acquire() and store_release() was made for. (As well as atomics, with associated memory orderings)




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

Search: