Acorn
Acorn

Reputation: 857

Is it safe to initialize a c++11 function-static variable from a linux signal handler?

2 questions (below) about the C++11 static initialization at [1] in this reference code (this is a complete tested c++11 example program).

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

struct Foo {
    /* complex member variables. */
};

void DoSomething(Foo *foo) {
    // Complex, but signal safe, use of foo. 
}

Foo InitFoo() {
    Foo foo;
    /* complex, but signal safe, initialization of foo */
    return foo;
}

Foo* GetFoo() {
    static Foo foo = InitFoo();   // [1]
    return &foo;
}

void Handler(int sig) {
    DoSomething(GetFoo());
}

int main() {
    // [2]

    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = Handler;
    sigaction(SIGINT, &act, nullptr);

    for (;;) {
        sleep(1);
        DoSomething(GetFoo());
    }
}

Question1: Is this guaranteed safe (no deadlocks etc)? C++11 static initialization involves locks. What if the signal is delivered before/after/during the first call to GetFoo() in main?

Question2: Is this guaranteed safe if a call to GetFoo() is inserted at [2] before the signal handler is installed? (Edit:) I.e. does inserting GetFoo() at [2] ensure that, later, when a signal arrives while the loop is operating, that there will be no deadlock?

I'm assuming C++11 (g++ or clang) on recent GNU/Linux, although answers for various Unices would also be interesting. (Spoiler: I think the answer is 1:NO and 2:YES but I don't know how to prove it.)

Edit: To be clear, I can imagine static initialization could be implemeted like this:

Mutex mx;           // global variable
bool done = false;  // global variable
...
lock(mx);
if (!done) {
  foo = InitFoo();
  done = true;
}
unlock(mx);

and then it would not be deadlock safe because the signal handler might lock mx while the main thread has it locked.

But there are other implementations, for example:

Mutex mx;                        // global variable
std::atomic<bool> done = false;  // global variable
...
if  (!done.load()) {
  lock(mx);
  if (!done.load()) {
    foo = InitFoo();
    done.store(true); 
  }
  unlock(mx);
}

which would not have potential for deadlock provided the codepath was run completely at least once before a signal handler runs it.

My question is whether the c++11 (or any later) standard requires the implementation to be async-signal-safe (deadlock free, aka lock free) after the initial pass through the code has completed?

Upvotes: 3

Views: 113

Answers (1)

P.P
P.P

Reputation: 121397

How static Foo foo = InitFoo(); gets initialized must be stated first before getting into signals.

It requires dynamic initialization, where it'll be initialized the first time GetFoo() gets called since the "complex initialization" you mention in InitFoo() can't be done at compile-time:

Dynamic initialization of a block-scope variable with static storage duration or thread storage duration is performed the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. 85 If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

85 The implementation must not introduce any deadlock around execution of the initializer. Deadlocks might still be caused by the program logic; the implementation need only avoid deadlocks due to its own synchronization operations.

With that established, we can go to the questions.

Question1: Is this guaranteed safe (no deadlocks etc)? C++11 static initialization involves locks. What if the signal is delivered before/after/during the first call to GetFoo() in main?

No, this isn't guaranteed. Consider when GetFoo() is called the first time from inside the for loop:

GetFoo() -> a lock is taken to initialize 'foo'-> a signal arrives [control goes to signal handling function] -> blocked here for signal handling to complete
                                                                                                                                                                                                                                                                                             
--> Handler() -> DoSomething(GetFoo()) -> GetFoo() -> waits here because the lock is unavailable.
                                                                             

(The signal handler has to wait here since the initialization of 'foo' isn't complete yet -- refer the quote above).

So the deadlock occurs in this scenario (even without any threads) as the thread is blocked on itself.

Question2: Is this guaranteed safe if a call to GetFoo() is inserted at [2] before the signal handler is installed?

In this case, there's no signal handler established at all for SIGINT. So if SIGINT arrives, the program simply exits. The default disposition for SIGINT is to terminate the process. It doesn't matter whether the initialization of GetFoo() is progress or not. So this is fine.

The fundamental problem with case (1) is that the signal handler Handler isn't async-signal-safe because it calls GetFoo() which isn't async-signal-safe.


Re. updated question with possible implementations of static initialization:

The C++11 standard only guarantees that the initialization of foo is done in a thread-safe manner (see the bold quote above). But handling signals is not "concurrent execution". It's more like "recursively re-entering" as it can happen even in a single-threaded program - and thus it'd be undefined. This is true even if static initialization is implemented like in your second method that'd avoid deadlocks.

Put it the other way: if static initialization is implemented like your first method, does it violate the standard? The answer is no. So you can't rely on static initialization being implemented in an async-signal-safe way.

Given you ensure "...provided the codepath was run completely at least once before a signal handler runs it." then you could introduce another check that'd ensure GetFoo() is async-signal-safe regardless of how static initialization is implemented:

std::atomic<bool> foo_done = false;
static_assert( std::atomic<bool>::is_lock_free );

Foo* GetFoo() {
    if (!foo_done) {
        static Foo foo = InitFoo();   // [1]
        foo_done = true;
    }
    return &foo;
}  

Upvotes: 1

Related Questions