5 ms·
While Julia starts scared and ends up feeling better, I've only gotten more scared of Unix signals over time. Context: I've written a robust command-line utili
by gavinhoward 3y ago
While Julia starts scared and ends up feeling better, I've only gotten more scared of Unix signals over time.
Context: I've written a robust command-line utility [0] that must handle signals, and unlike most utilities that are I/O-bound, mine is CPU-bound.
An I/O-bound utility can easily use signalfd() (subject to the gotchas in the "signalfd() is useless" post that Julia links to, which you should also read). signalfd() will work for I/O-bound utilities because it turns those signals into I/O. Perfect.
However, in a CPU-bound program, signals are used specifically to interrupt execution. This is, to put it mildly, as difficult as writing code for interrupts in the embedded space. Why? Because that's really what you're doing: handling an interrupt that can happen at any time.
My solution was something I wish on no one: I used setjmp() and longjmp().
Horrors!
Yep. And it gets worse: I had to longjmp() out of the signal handler.
AH!
And it gets worse: to ensure that there were no memory leaks, I had to keep a stack of jmp_bufs and manually jump to each one, which would be in a function where memory had to be cleaned up.
Cue screams of bloody murder
You may insist that longjmp()'ing out of signal handler is not allowed; it actually is [1], but unlike most other "async-signal-safe" functions, you need to ensure you don't interrupt a syscall or other code that is not async-signal-safe.
So it gets worse: I have a signal lock that the signal handler checks. If it's not locked, the signal handler will longjmp() out of the signal handler. If it is locked, the signal handler sets a flag and returns. Then the code that unlocks signals checks for the flag and does a longjmp() if it's set.
He's dead, Jim!
I have another project that is a framework in C. This framework needs to handle signals for clients. It has to be general, so it has to handle CPU-bound clients. So I had to implement the same thing. I was able to make it easier, but it is also harder because I have that one thing that messes up every Unix API: threads.
Nuclear mushroom cloud
So should you be scared? It depends; if you can get away with signalfd() and know its gotchas, maybe not.
But if you need anything more complex, yes, be very afraid.
[0]: https://git.gavinhoward.com/gavin/bc https://git.gavinhoward.com/gavin/bc
[1]: https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/ https://pubs.opengroup.org/onlinepubs/9699919799.2008edition...
- fzeindl 3y agoHave you ever read up on communicating sequential processes? In that model you have one or multiple threads executing work while new messages arrive on a queue. In such an architecture handling signals is trivial.
- boffinAudio 3y agoYeah, I was going to mention that if the long-running/CPU-bound work was put in a worker thread with a message queue in which state messages are received/handled, it'd be relatively trivial to handle the signal across these constraints, since signals are only delivered upon context switches. But if you've just got one very tight loop and are running like a bat out of hell, well you deserve the hell you're in. ;) longjmp() is definitely not the right way to do this. A thread dedicated to handling signals (or doing the CPU work) which properly tells the main() thread to die/quit/clean up is how I'd do it .. even if you only spawn a thread for signal handling, its still way cleaner than that longjmp() business .. which is very difficult to understand, even with the context you've given in this thread (I read your code - its nasty) ...
- gavinhoward 3y agoWhy do I deserve that for having a CPU-bound program? And why is longjmp() the wrong thing to do?
- boffinAudio 3y agoBecause signals are delivered on context switch, and you're not allowing any of that to happen. Its not a well-behaved process in that case .. not that you shouldn't be able to just burn as much CPU as you want, just that you would need to open up some space to communicate with the OS in the meantime, and having a signal-handler-thread or a cpu-working-thread is how to do it properly. (Do you also handle SIGHUP? SIGSTOP?) longjmp() is just asking for hell 6 months later when you come back and have to work out why you did such a hokey thing in the first place. EDIT: it should be noted that the "side-effect" of using longjmp is .. a context switch .. so you gave yourself some space to behave properly with the OS. Its just that the more future-proof way to do it is to have a thread for work (you have this already with main()) and a thread for signals (you'd just add this to service signals only and give main() a way to cleanly exit) ... Not too complex, and a tad bit easier to read 6 months later ..
- 3y ago
- pjc50 3y ago> I had to keep a stack of jmp_bufs and manually jump to each one, which would be in a function where memory had to be cleaned up. This is similar to (but not identical to) what Windows calls "structured exception handling" (SEH), which allows you to intercept things like "divide by zero" and do unwinding. https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170 https://learn.microsoft.com/en-us/cpp/cpp/structured-excepti...
- gavinhoward 3y agoYes. I considered using SEH for my bc on Windows (I ported it to Windows), but decided against it because I wasn't sure I understood SEH enough.
- ridiculous_fish 3y agoI searched for longjmp and was pleased to see someone else brought it up first. Yes, you can longjmp from a signal handler back to standard execution flow. It is even considered a "best practice" using readline! https://stackoverflow.com/questions/16828378/readline-get-a-new-prompt-on-sigint https://stackoverflow.com/questions/16828378/readline-get-a-...