If you've ever read the Lions' Book commentary on 6th Edition Unix, you'll notice that many parts of the API as implemented back there are pretty solid -- quality, well designed interfaces that have stood the test of time.
Signals are not one of those parts. The 6th Ed signal handling code reads to me as somewhat of an afterthought whose use cases were mostly "kill the process for a fatal signal or terminal ^C", "ptrace for a debugger" and maybe SIGALRM. The data structures don't allow a process to have more than one pending signal -- if a new one comes along the fact an old one was pending is simply dropped. Running a signal handler automatically deregistered it, leaving a race condition if two signals arrived in close succession (this is a well known bug fixed by BSD later). And EINTR is an irrelevance if signals are generally fatal but its effects spread like poison through every other kernel API if you need your program to be reliable even with signals being delivered.
The worst bugs and races were fixed up by the BSD folks and others, but the underlying concept is an unfortunate combination of "basically irredeemable", "indispensable" (you have to have some kind of "kernel tells you something has happened" API, and signals are what we got) and "insidious" (thanks to EINTR). I think they're a strong candidate for "worst design decision in unix".
(PS: one of the reasons they stand out in 6th Ed is that so much of the rest of that code is so good!)
>The worst bugs and races were fixed up by the BSD folks and others, but the underlying concept is an unfortunate combination of "basically irredeemable", "indispensable" (you have to have some kind of "kernel tells you something has happened" API, and signals are what we got) and "insidious" (thanks to EINTR). I think they're a strong candidate for "worst design decision in unix".
Suppose you were to throw the whole thing out and write a good replacement (and backwards-compatibility be damned), what would it be like?
Suppose you were to throw the whole thing out and write a good replacement (and backwards-compatibility be damned), what would it be like?
Steal the best bits from Windows NT, and improve the existing mechanisms.
Kill signals in their current form. Build a general-purpose notification mechanism consisting of a mutex and a message. Possibly allow a process to have more than one message queue (Windows makes this really, really easy).
All IO, networking and informational signals (SIGWINCH, SIGCHLD etc) then come as messages (these may have to be fixed size, but anything from a few words to a 4k page would do). select, poll etc are replaced by waiting on a mutex. You can put all your worker threads waiting on that mutex. A message arrives. The kernel wakes one waiting thread and gives it the message (via an atomic deque-or-block syscall). You don't have to do any O(n) processing to work out which socket it relates to as the kernel has helpfully put it in the message.
In the deluxe 4k page version, a 1500-byte ethernet frame arrives, is DMA'd into the top half of a page, the kernel inspects it and sets message headers to say where the data is, and hands it directly into the userspace of a waiting process.
The one downside of this is that UNIX pipe programs become slightly more complicated. Rather than just doing "while(read()) write()" you'd have to switch on the type of message recieved and implement your own abnormal exit functionality. This could probably be tidied away for you inside the standard library.
External process control mechanisms would have to be built for killing processes and suspend/resume.
Unix already has a general notification mechanism in the form of poll and select, no need to add a new one. The problem is not all interesting events are not portably delivered via a file descriptor, but that can be more easily extended (as done by lot of unices, including linux) than coming up with a completely new primitive.
But some messages really must be delivered synchronously and can't normally be queued: SIGSEGV, SIGFPE, SIGBUS, etc. There is really no way around interrupts.
BTW mutexes are not for signaling. What you want for signaling in a queue are semaphores, events or condition variables (or even file descriptors, like eventfd).
Well, the brief was to "throw the whole thing out", including select (which is bad) and poll (which is merely adequate).
The machine traps are interesting in that they should only be generated locally - there's no sensible case for injecting SIGSEGV into other processes. Arguably we should learn from Windows "structured exception handling" here. There are two sensible things to do with traps (other than sudden death): hand over to a callback of some kind (which should be told about the state of the stack), or turn into a language-native exception and throw that.
Poll is perfectly fine for the very large majority of unix applications which do not need to scale to tens of thousands of sockets.
The handling over to callback is exactly what is done by unix signals. Converting to exceptions can be implemented on top of signal handlers, but note that even MS stopped mapping by default structured exceptions to language exception a while ago, at least in C++, as unwinding the stack, destroying state and potentially calling destructors is the last thing you want on a segmentation fault or other unexpected events.
The handling over to callback is exactly what is done by unix signals
Not quite, there are quite a lot of restrictions on what you can do in a signal handler. It ought to be possible to design a callback mechanism without those restrictions. And a signal tells you nothing about its origin or what file descriptor / child process etc. it might relate to.
I assume that by restrictions you are talking about the async signal safety; this is inherent on the 'interrupt' nature of signals as they can happen at any point in a program execution, there is really no way around that. It would be of course nice if more functions where async signal safe (especially malloc).
Regarding the lack metadata, I agreed else thread that messages that carry such data ought to be transported via an explicit message queue, not via signals.
The equivalent of SIGSEGV/SIGBUS on Mach is handled in basically the way he describes. On an access violation, it suspends the thread and delivers a message to the registered port. The thing listening on the port (in a different thread or even different process) receives the message, does whatever it needs to do, and then sends a response, after which the original thread is woken back up. From the perspective of the violating thread it was handled synchronously, but the actual implementation was an async message queue.
It is not much better though. If it is another thread it has to work under the same async signal safety rules of a signal handler (the blocked thread might be holding an arbitrary mutex). If it is another process, there isn't a lot it can do.
Now go and read https://news.ycombinator.com/item?id=11864211 . There are those who argue (at some length, q.v.) that "the best bits from Windows NT" are not to have readiness-oriented application designs in the first place.
I was thinking more along the lines of Windows "registered I/O" and Linux "netmap". 'trentnelson' argues for a distinction between readiness and asynchronous. I don't think the distinction needs to exist, and that in an ideal world the disk subsystem would be more like a kind of networking subsystem. If the request can't be satisfied from a RAM buffer, add it to the "outbound requests" list and return to the calling thread. When the corresponding reply eventually turns up from the disk, add it to the input queue of the process.
> Suppose you were to throw the whole thing out and write a good replacement (and backwards-compatibility be damned), what would it be like?
Keep the "you can always kill or stop a process" provided by SIGKILL and SIGSTOP/SIGCONT, using dedicated system calls. Handle every other kind of message (from the kernel or otherwise) via file descriptors, similar to signalfd.
That's two people so far who have mentioned signalfd here, without reference to what the headlined article has to say on the matter (which, ironically, amongst other things points to https://news.ycombinator.com/item?id=9564975 on Hacker News and what it, in its turn, points to).
edit: Actually there are some fairly decent systems for IPC now, with the Mach kernel from CMU probably being the most popular example. Most systems with micro-kernels probably have something similar, and of course dbus is there, and Android intents.
I think Applescript deserves a mention here, because although it's a rather weird language, the IPC stuff is really really easy.
Considering that you can basically establish no invariants in the signals model, to me it sounds like a cooperative model is the way to go.
Have a way to poll for "outstanding" signals and deal with them accordingly, in some sort of queue (and the OS drops older messages or something, if they're important you'd deal with them).
The "freeze-the-world-and-do-this-thing-instead" model seems like the best way to introduce "my entire program is now broken"-style bugs. The OS deals with interrupts but the OS is also running by itself.
While the "notification" system seems is cleaner than the signals system (and presumably avoids those races fixed by BSD) -- it does not seem fundamentally different.
If I am understanding that man page correctly, it does nothing at all to fix EINTR: "If the note interrupted an incomplete system call, that call returns an error (with error string interrupted) after the process resumes."
EINTR is only "needed" because of the situation where a call is made to a blocking system call like read, and a signal is received before it completes. It needs to cancel the blocking call so that it can run the signal handler.
If you get rid of signals, you wouldn't need EINTR. Even with signals, what people do 99% of the time is just retry the system call immediately. It would probably have been much better to have it do so automatically by default, avoiding exposing EINTR to most users.
EINTR is retried automatically if you use sigaction() and set the SA_RESTART flag.
If you use signal() instead of sigaction() it depends on your OS whether EINTR is automatically retried - never use signal().
(There's a few exceptions still - partial completed syscalls, e.g. a signal can cause read() to return partial data or sleep() to return before it should - neither case gives you EINTR though)
Yes, but how does the Plan9 "notify" mechanism help with any of that? It seems to just be a tidied up version of signals, and therefore "needs" to do EINTR in much the same way as Unix.
Maybe, the signal coming from another process should be handled in an independent thread (maybe created the first time a signal is raised). Something would still be needed to interrupt pending system calls.
I think signals are a "poor mans" implementation of threads and queues. They didn't have threads and queues back in the 70'ies, so they resorted to an ugly hack. Ditto with interruptable system calls.
It's somewhat like the interrupt-level callbacks classic MacOS (version 1-9) had for network and file i/o. They too didn't have threads back then, so interrupt-callbacks was the only way to avoid blocking calls which would make the UI hang while waiting for network requests.
This is an interesting perspective. I think of signals the other way around -- that they came out of people realizing how useful hardware interrupts could be in allowing devices to talk to the kernel, so they engineered a similar affordance (signals) to allow the kernel to talk to processes.
In the case of hardware interrupts (like for old keyboards), if you didn't grab that character off the hardware right away, it would vanish, or another one would take its place. As long as you were fast, it worked. Same with signals. As long as you don't try to do too much in the handler, it works. In other words, it was designed to be "good enough" if used properly (just like all hardware).
...so interrupt-callbacks was the only way to avoid blocking calls
I don't know how early MacOS works, but on Unix its not the _only_ way: that's what poll(), select() and the like are for.
And while threads their uses, they don't really change this aspect. Eventually the thread must report a result, and the best way to do that is to report into to some queue that gets polled somewhere.
Options were more limited in early unix. Use of signals as an adhoc IPC mechanism predates BSD sockets api (1983). Poll has something to do with SysV, so would have been later. There may have been other IPC mechanisms that were dropped before modern unix (but presumably dropped for good reasons). Art of Unix Programming refers to one called mx.
No time to read it back again, but if I remember correctly not even POSIX defines portable semantics for signals across implementations, it always leaves some room for implementation specific behavior.
Signals are not one of those parts. The 6th Ed signal handling code reads to me as somewhat of an afterthought whose use cases were mostly "kill the process for a fatal signal or terminal ^C", "ptrace for a debugger" and maybe SIGALRM. The data structures don't allow a process to have more than one pending signal -- if a new one comes along the fact an old one was pending is simply dropped. Running a signal handler automatically deregistered it, leaving a race condition if two signals arrived in close succession (this is a well known bug fixed by BSD later). And EINTR is an irrelevance if signals are generally fatal but its effects spread like poison through every other kernel API if you need your program to be reliable even with signals being delivered.
The worst bugs and races were fixed up by the BSD folks and others, but the underlying concept is an unfortunate combination of "basically irredeemable", "indispensable" (you have to have some kind of "kernel tells you something has happened" API, and signals are what we got) and "insidious" (thanks to EINTR). I think they're a strong candidate for "worst design decision in unix".
(PS: one of the reasons they stand out in 6th Ed is that so much of the rest of that code is so good!)