4 ms·
So signals make sense once you understand where they came from. The writers of Unix needed a kernel->user downcall mechanism. Back then, user mode code was th
by monocasa 3y ago
So signals make sense once you understand where they came from.
The writers of Unix needed a kernel->user downcall mechanism. Back then, user mode code was thought of as being like what we know call unikernels with the kernel thought of a bit more like a hypervisor. The individual user sessions were, early on in Unix's history, only a couple more primitives on top of what we'd later think of as DOS style environments, simply multiplexed on the hardware. From that perspective, mirroring the microcode->kernel downcall mechanism (interrupts) at the kernel->user boundary makes plenty of sense.
Once you see them for what they are (interrupts for user mode instead of kernel mode), all of their goofiness makes sense.
* SIGSEGV, SIGILL, etc. map to processor exceptions.
* SIGIO, SIGWINCH, etc. map to I/O device interrupts, including SIGALRM as a timer interrupt.
* SIGSTOP, SIGKILL, etc. map to platform interrupts like NMIs.
* SIGUSR, etc. map to IPIs.
It's a one shot downcall mechanism that can happen at any time, so all of the same way you think about concurrency during an interrupt applies here as well. You can't take a regular mutex because it might already be held by the context you interrupted. Less of a deal now, but any non-reentrant code shared with the regular user context is off the table. So just like how in interrupt context, large swaths of the kernel are off limits, large swaths of user mode are off limits except very carefully written, very context dependent code.
I even ported an RTOS to run in *nix user mode for CI/development purposes, where the interrupt controller abstraction was simply implemented as signals. The code didn't look any different than you'd expect from running under a hypervisor with a paravirtualized interrupt controller.
- scottlamb 3y ago> * SIGSEGV, SIGILL, etc. map to processor exceptions. That's the idea anyway. But you can also `kill -SEGV $SOMEPID`. There are (now, since threading became a thing on Unix) three categories of signals: * async, process-directed, as with the command above, SIGALRM, or the like. * async, thread-directed, as with `pthread_kill`. * sync, thread-directed, as with the processor exceptions you mentioned. The signal numbers generally are good enough, but the `si_code` member of `siginfo_t` will more authoritatively distinguish the source of a signal. Anyway, I agree it's useful to think about the type of signal you're interested in handling. I think most people are primarily interested in async, process-directed signals for graceful shutdown on `SIGTERM`/`SIGINT` and/or config reloading on `SIGHUP`. Fortunately, that's the easiest to handle safely. One of my favorite ways is to not use a signal handler at all. Mask out the signal in main() before creating any threads, and create a thread which loops over sigwaitinfo.
- monocasa 3y ago> That's the idea anyway. But you can also `kill -SEGV $SOMEPID`. I can also `int 14` with the right hardware permissions and from software to manually invoke a page fault exception. That's how `int 3` works as a breakpoint.
- adonovan 3y agoThis is a useful classification. With the benefit of hindsight--and threading--it would be nice if async signals could just go away, since the kernel can send a notification message to the application over a file descriptor (see man signalfd(2)) and the application can have a dedicated thread to deal with it at its leisure. That leaves the synchronous signals, which are the kernel's way of telling you that you executed a bad instruction and giving you a chance to do something awfully clever instead of crashing.
- mike_hock 3y ago> From that perspective, mirroring the microcode->kernel downcall mechanism (interrupts) at the kernel->user boundary makes plenty of sense. Also called the "inner platform effect."
- kazinator 3y ago> kernel->user downcall mechanism Your remarks reminded me of scheduler activations. I believe a signal handler is a kind of upcall. A system call is a downcall.
- leoc 3y ago> Back then, user mode code was thought of as being like what we know call unikernels with the kernel thought of a bit more like a hypervisor. The individual user sessions were, early on in Unix's history, only a couple more primitives on top of what we'd later think of as DOS style environments, simply multiplexed on the hardware. Hang on, at which point was this?
- omginternets 3y agoHow are signals actually implemented? I'm asking because I'm currently working with WASM/WASI, in which a process is strictly single-threaded. The WASM equivalent of a syscall (a "host call") exists, but as you'd expect, it suspends the execution of the WASM code until it returns. As a result, if you want to set up a bidirectional bytestream between the WASM code and the host-language's runtime, you can't block on a call to `read()`, since this would effectively block until data arrives. Polling is obviously not a great solution. So if I were to implement a signaling mechanism in a WASM runtime, what would that look like?
- fsckboy 3y agoNobody else answered you so I'm going to make up an answer which, even if it's not a specifically correct answer, it's a good answer :) look into a.out and ELF formats for executable files; your executable files, said files hereby called "your code", will be in this format on linux. There will be an entry point for your code, so if your program gets run, the kernel (or whatever userspace launcher it's given this task to... now that I think of it, from the command line, the shell would fork itself, and one fork would exec your program, so we're talking about exec here) will load your code into memory, and transfer control to--transfer control to means set the program counter to--your code at a particular address (the address of C main(), so to speak). Before this happens, the launcher will have allocated memory for you, set up the stack pointer, opened stdin/out/err, put the command line arguments into place (argc, argv), etc. Then your code can run till it's done, making system calls if it needs to, and potentially being preemptively multitasked, but that's all invisible to your code, black box in terms of how it's implemented. now for signals... Signals from the operating system need their own entry point(s) into your code. So, while your code is running, the OS can interrupt it (stuffing everything of yours onto your stack so it can be popped off later when control flow is returned to the same point later). In this meantime, the OS will now transfer control (set the program counter just like it did when it first launched you) and you're off and running with code to handle the signal, the interrupt. If you have not prepared your code for this event, the default (set up by the launcher for programs) program flow at this point will be sent to a generic handler that says "unhandled signal", and your program will be killed. They're actually lying, the signal is being handled, but it's being handled by a handler that cleans up and exits. But, you can write code to handle the signal yourself, and when your program launches you (generally in main() or nearby) register the address of the handler you want to use so that when the interrupt comes the OS will know where to send control. At that point it's as if you just called a function/subroutine at wherever your code running at that moment, and when that function returns, things continue as they were before, modulo whatever you did in your handler. There's a bunch of specifics I don't know, is there one entry point for all signals, and a signal argument token decides where to transfer control, or are there 10 separate handlers so you don't waste time? etc. if you're doing no-wait I/O with a single thread, one way is to make a blocking call, call into something that doesn't return till there's new data, and that new data may then arrive, handled by your interrupt handler; in that case returning from your input-getting handler could return from the blocking call and your code is off and running as if it just returned in the first place. There are a myriad variations on this, but that's the general idea. happy to clarify anything, others can probably answer better, etc.