从 linux 信号处理程序初始化 c++11 函数静态变量是否安全?

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

关于此参考代码中 [1] 处的 C++11 静态初始化的 2 个问题(如下)(这是一个完整的经过测试的 c++11 示例程序)。

#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());
    }
}

问题 1:这是否保证安全(无死锁等)? C++11 静态初始化涉及锁。如果在 main 中第一次调用 GetFoo() 时发送信号 before/after/during 怎么办?

问题 2:如果在安装信号处理程序之前在 [2] 插入对 GetFoo() 的调用,这是否保证安全? (编辑:)即在 [2] 处插入 GetFoo() 是否确保稍后在循环运行时信号到达时不会出现死锁?

我在最近 GNU/Linux 假设 C++11(g++ 或 clang),尽管各种 Unice 的答案也很有趣。 (剧透:我认为答案是 1:NO 和 2:YES 但我不知道如何证明。)

编辑:明确地说,我可以想象静态初始化可以像这样实现:

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

然后它就不是死锁安全的,因为信号处理程序可能会在主线程锁定 mx 时锁定它。

但是还有其他的实现,例如:

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);
}

如果代码路径在信号处理程序 运行 之前至少 运行 完全 运行,则不会出现死锁。

我的问题是 c++11(或任何更高版本)标准是否要求实现是异步信号安全的(无死锁,又名无锁)之后初传代码完成了吗?

static Foo foo = InitFoo(); 如何初始化必须在进入信号之前先说明。

它需要 dynamic initialization,它将在第一次调用 GetFoo() 时被初始化,因为你在 InitFoo() 中提到的“复杂初始化”无法在编译时完成-时间:

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.

有了这些,我们就可以开始做题了。

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?

不,这不能保证。考虑何时从 for 循环中第一次调用 GetFoo()

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.
                                                                             

(信号处理程序必须在这里等待,因为 'foo' 的初始化尚未完成——请参考上面的引述)。

因此在这种情况下(即使没有任何线程)也会发生死锁,因为线程被自身阻塞。

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

在这种情况下,根本没有为 SIGINT 建立信号处理程序。因此,如果 SIGINT 到达,程序就会退出。 SIGINT 的 default 处理是终止进程。 GetFoo() 的初始化是否有进展并不重要。所以这很好。

案例 (1) 的根本问题是信号处理程序 Handler 不是 async-signal-safe 因为它调用 GetFoo() 这不是异步信号安全的。


回复。使用静态初始化的可能实现更新了问题:

C++11 标准仅保证 foo 的初始化以线程安全的方式完成(参见上面的粗体引号)。但是处理信号不是“并发执行”。它更像是“递归重新进入”,因为它甚至可以在单线程程序中发生——因此它是未定义的。即使像在避免死锁的第二种方法中那样实现静态初始化也是如此。

换句话说,如果像你第一种方式那样实现静态初始化,是不是违反了标准?答案是不。因此,您不能依赖以异步信号安全方式实现的静态初始化。

假定您确保“...前提是代码路径 运行 在信号处理程序 运行 之前至少完成一次。”那么你可以引入另一个检查来确保 GetFoo() 是异步信号安全的,无论静态初始化是如何实现的:

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;
}