Curious Curious - 4 months ago 38
Linux Question

Signal handlers and async safety

I am still a little confused as to why exactly it is unsafe to receive a signal and call a non async safe function from within that signal handler. Could someone explain the reasoning behind this and possibly try and give me some references that I can follow to read up more on this myself?

In other words I am asking why it is unsafe to say call printf from within a signal handler. Is it because of intra-process issues and possible race conditions resulting from two possible calls to printf without protection or is it because of inter process races to the same resource (in this example stdout). Say a thread within process A is calling printf and another thread receives the signal and then calls printf. Is it possibly because the kernel here will not know what to do because it will not be able to distinguish between the two calls.

Answer

Say a thread within process A is calling printf and another thread receives the signal and then calls printf. Is it possibly because the kernel here will not know what to do because it will not be able to distinguish between the two calls.

It's not the kernel that will have issues. It's your application itself. printf is not a kernel function. It's a function in the C library, that your application uses. printf is actually a fairly complicated function. It supports a wide variety of output formatting.

The end result of this formatting is a formatted output string that's written to standard output. That process in of itself also involves some work. The formatted output string gets written into the internal stdout file handle's output buffer. The output buffer gets flushed (and only at this point the kernel takes over and writes a defined chunk of data to a file) whenever certain defined conditions occur, namely when the output buffer is full, and/or whenever a newline character gets written to the output stream.

All of that is supported by the output buffer's internal data structures, which you don't have to worry about it because it's the C library's job. Now, a signal can arrive at any point while printf does it work. And I mean, at any time. It might very well arrive while printf in the middle of updating the output buffer's internal data structure, and they're in a temporary inconsistent state because printf hasn't yet finished updating it.

Example: on modern C/C++ implementations, printf may not be signal-safe, but it is thread safe. Multiple threads can use printf to write to standard output. It's the threads' responsibility to coordinate this process amongst themselves, to make sure that the eventual output actually makes sense, and it's not jumbled up, at random, from multiple threads' output, but that's beside the point.

The point is that printf is thread safe, and that typically means that somewhere there's a mutex involved in the process. So, the sequence of events that might occur is:

  • printf acquires the internal mutex.

  • printf proceeds with its work with formatting the string and writing it to stdout's output buffer.

  • before printf is done, and can release the acquired mutex, a signal arrives.

Now, the internal mutex is locked. The thing about signal handlers is that it's generally not specified which thread, in a process, gets to handle the signal. A given implementation might pick a thread at random, or it might always pick the thread that's currently running. In any case, it can certainly pick the thread that has locked the printf, here, in order to handle the signal.

So now, your signal handler runs, and it also decides to call printf. Because printf's internal mutex is locked, the thread has to wait for the mutex to get unlocked.

And wait.

And wait.

Because, if you were keeping track of things: the mutex is locked by the thread that was interrupted to service the signal. The mutex won't get unlocked until the thread resumes running. But that won't happen until the signal handler terminates, and the thread resumes running, but the signal handler is now waiting for the mutex to get unlocked.

You're boned.

Now, of course, printf might use the C++ equivalent of std::recursive_mutex, to avoid this problem, but even this won't solve all possible deadlocks that could get introduced by a signal.

To summarize, the reason why it's "unsafe to receive a signal and call a non async safe function from within that signal handler" is because it's not, by definition. It's not safe to call a non-async safe function from within the signal handler" because the signal is an asynchronous event, and since it's not an async-safe function, you can't, by definition. Water is wet because it's water, and an async-unsafe function cannot be called from an asynchronous signal handler.