Search code examples
c++multithreadingsignals

C++14: Usage of condition variable in signal handler if only one signal is handled at any given time (exclusion using atomic bool)


I am aware that by default condition variable usage in a signal handler is not safe (c++11 use condition variable in signal handler). However, as per https://man7.org/linux/man-pages/man7/signal-safety.7.html, it seems to imply that if we ensure that no other signals are getting processed during this time, it maybe okay to do so.

  1. Block signal delivery in the main program when calling functions that are unsafe or operating on global data that is also accessed by the signal handler.

So, if I use a std::atomic_bool to ensure that only one handling is running at any time, is usage of condition variable safe? Something like in the code below (simplified a lot):

std::atomic_bool g_signalBlock; // global 

void handleSignal(int signalNumber, siginfo_t* signalInfo, void* ucontext) {
    if (g_signalBlock.exchange(true)) {
        return; // ignore this signal. another signal being processed.
    } else {
       // process the signal
       if (signalNumber == SIGUSR2) {
         // notify another thread using std::condition_variable
       } else {
         // re-raise non-SIGUSR2 signal (to exit the program) 
         signal(signalNumber, SIG_DFL);
         raise(signal);
       }

       g_signalBlock.store(false);  // now allow other signals
    }
}

Use case:

As part of signal handling logic, there are certain operations that are currently async-signal-safe but takes too long (logging to files which is implemented in a async-signal-safe manner). This is acceptable for signals like SIGABRT etc. where the process is going to exit, but the same handler is also used for handling SIGUSR2 signals to log thread traces (automatically triggered under certain conditions). I want to avoid it impacting other threads, thus a need to delegate logging to a background thread.


Solution

  • It is not enough to merely ensure that only one signal handler can be running at a time. A signal handler can race against any thread in your program.

    So if you have a condition variable cv, and you access it from within a signal handler, you must ensure that that signal handler cannot be invoked while any other code that accesses cv is running. The only way to do this is to either change the action that will be taken when a signal is received (e.g., ignore the signal or terminate the process) or mask out the signal (e.g. using sigprocmask) prior to executing the code that accesses cv.

    You might find this strange. If two threads can concurrently access cv then why can't one thread and one signal handler safely concurrently access cv? Well, all thread-safe types need to guard against a second thread trying to do something with them while the first thread is in the middle of something. But if a thread is in the middle of doing something with the variable, and then that thread gets interrupted in order to execute the signal handler (which executes on the very same thread), which tries to do something with the same variable, the implementation won't necessarily be able to handle that kind of same-thread re-entrancy properly.

    Only lock-free atomics can offer the guarantee that a single operation truly is indivisible from the hardware's point of view, and therefore, it is impossible for a thread to get interrupted to run a signal handler while it's in the middle of accessing the variable.

    If you were hoping to have the signal handler add something to a queue and then notify the condition variable to let some thread know that the queue is nonempty, unfortunately, the standard just doesn't allow that. The signal handler should communicate with the rest of the program solely using lock-free atomic variables. Since C++20, atomic variables themselves support wait, notify_one and notify_all operations, so you can use those. Prior to C++20, the options aren't that great; you could (1) use platform-specific functionality like futexes, (2) communicate using signal-safe library functions; for example, the signal handler can write to a file descriptor and this can wake up another thread that's blocked on a read; or (3) have a thread that just spins until the value of an atomic variable changes.