Hacker News new | ask | show | jobs
by xyzzy_plugh 1414 days ago
This makes no sense. Ctrl-C is just a way to tell your terminal to send a SIGINT signal to the current process. How that process handles the signal is up to it! It's by definition ignorable, as the author points out, but it's not rocket science to handle it in a sane fashion even in a multi-threaded application. Modern languages make this trivial. The author makes it sound like some dark art but in reality you just have to read the manpages.

SIGINT is really designed for interactive applications. Most processes should simply treat it like a SIGTERM unless they have some sort of REPL. Unless you need graceful shutdown, most processes shouldn't mask either signal. If they do, the polite thing is to unmask after receiving the first signal so subsequent signals immediately terminate.

7 comments

The SIGINT (if the terminal is configured to generate one) goes to the foreground process group, not the current process. See the termios man page, look for INTR. This gets complicated in shell pipelines (oh look, a process group) where one or more of the tools involved are fiddling with the global terminal state, in which case there may be a process group signal, or there might instead be a key for some random program of the pipeline to to read, which it may ignore.

With an important productivity app like rogue(6) there is (probably) only one process in the foreground process group, and curses has (probably) set the terminal to have control+c either ignored or delivered to the rogue process as a key event. The player probably does not want to have their rogue process vanish because they hit control+c by habit, like trek(6) likes to do, but someone wrote blocksig(1) as an exec wrapper so that SIGINT can be ignored. With a complicated shell pipeline, the player probably does want the whole thing to go away, but that may be difficult depending on exactly how complicated the shell pipeline is and whether any processes in that pipeline are fiddling with the global terminal state. (Global to the process groups and their PIDs under that particular terminal, not the whole system or universe or whatever. But global enough to cause problems.)

Opinions also vary here, some want that LISP image to never go away (those emacs users, probably), others may want control+c to murder the LISP image so they can go back to hacking in vi. POSIX probably says something about shell pipelines and control+c and such, which various shells may or may not follow, exactly. Etc...

I know this! I was hoping to hand wave away the complexity of process groups.

And you probably know that the author of curses is the author of rogue!

> someone wrote blocksig(1)

Link?

There is an entire world here beyond registering for a signal that comment seems unaware of. Even the simplest of preliminaries: registering for a signal is arguably non-trivial and incorrectly specified in many places since sigaction() supersedes signal().

> it's not rocket science to handle it in a sane fashion even in a multi-threaded application. Modern languages make this trivial. The author makes it sound like some dark art

Which language? I'll specify one so we can begin the process of picking each apart. Python? There is a sibling thread indicating Python issues. I don't know what the actual internal status is with Python signal handling but I am guessing the interpreter actually doesn't handle it correctly if I spent any time digging. Do you mean apps implemented in Python? They will almost certainly not be internally data-consistent. Exposing a signal handling wrapper means very little particularly when they frequently do this by ignoring all of the bad implications. I just checked Python's docs, and not surprisingly, Python guarantees you'll be stuck in tight loops: https://docs.python.org/3/library/signal.html That's just one gotcha of many that they probably aren't treating. This dialogue is going to play out the same way regardless of which language you choose.

Do you mean Postgres? I haven't used it recently but the last comment I read on HN seemed to indicate you needed to kill it in order to stop ongoing queries in at least some situations. If by a stroke of luck it does support SIGINT recovery (which would be great), what about the hundreds of other db applications that have appeared recently? You can't just call the signal handler wrapper and declare victory.

How about C? Bash? Perl? Java? Go? Rust?

I've done plenty of signal handling in Python and it's extremely straight forward. Like other languages, the runtime takes care of safely propagating information from the signal handler to other execution contexts, which requires being careful in a language like C (it's not hard, but you can't be naive). I wouldn't be surprised if there were bugs in Python, it's a mess generally and I'm not a fan.

Postgres queries run as subprocesses. You can send them any signals you want. Postgres tries very hard to be durable, and it handles signals carefully but often to the dismay of the operator who can't force it to stop without SIGKILL.

> registering for a signal is arguably non-trivial and incorrectly specified in many places since sigaction() supersedes signal().

This isn't a good argument, no one uses signal(2), I'm not aware of that ever being recommended in recent history and even the docs on my system scream "never use this" quite clearly.

Look, if you're not going to read the docs, signal handling will be the least of your concern. Signal behavior is extremely well documented.

> it's not rocket science to handle it in a sane fashion even in a multi-threaded application

It's not, though you need to be careful if you want to exit cleanly -- you can't just exit() or _exit(). You have to get all the threads to exit, and that requires a global volatile sig_atomic_t flag and some way to signal all threads that might be sleeping in event loops or what have you.

Sure you can just exit(), you just have to be sure that all your on-disk state changes are atomic. Which you should make sure of anyways.
A separate thread with sigwait() may be easier. Though I must admit, I've rarely had to do that manually, as I'm usually using a language or framework that gives an easier way to get notified about signals (e.g. Python KeyboardInterrupt or listeners in Boost ASIO or libuv). Aside from saving some boilerplate, those also emulate equivalent signals in Windows.

Threads waiting on event loops is exactly what you want on shutdown: that's what you use to notify them to exit.

I never use sigwait() or sigsuspend() -- I just have signal handlers that write a byte (e.g., the signal number) into a pipe, and maybe they set a global volatile sig_atomic_t variable. DJB calls this a "self-pipe". A self-pipe turns signals into I/O events that select/poll/epoll/event ports/kqueue/io_ring/whatever can handle like any other events.

At exit time I just make sure every thread can get an event. The main thread can pthread_cond_wait() for all the other threads to exit, waiting for the count of them to fall to zero.

Ah self pipe is a good idea, I suspect that's what those async frameworks are doing under the hood.
> SIGINT is really designed for interactive applications.

Which are the applications the article is talking about anyway.

It mentions ACID compliant databases for one.
Agreed -- if signal handlers are too messy, `sem_post(3)`, `sigwait(2)`, or `signalfd(2)` will get that control flow where you want it. Then the problem is reduced to "my application needs to handle a graceful shutdown event", which, though possibly complex, isn't really that novel.
Not really sure what the authorial intent is here tbh.
Tried searching for your username name, but, nothing happens.