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

时间:2019-05-31 18:01:59

标签: linux c++11 signals

在此参考代码的[1]中有两个关于C ++ 11静态初始化的问题(下面)(这是一个经过完整测试的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()之前/之后/期间传递信号怎么办?

问题2:如果在安装信号处理程序之前在[2]处插入对GetFoo()的调用,这是否保证安全?

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

1 个答案:

答案 0 :(得分:1)

static Foo foo = InitFoo();的初始化方式必须首先说明,然后再进入信号。

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

具有静态存储的块范围变量的动态初始化 持续时间或线程存储持续时间是第一次执行 控制通过其声明;这样的变量被认为 初始化完成后初始化。如果 初始化通过抛出异常退出,初始化是 未完成,因此下次进入控件时将再次尝试 声明。 如果控件在同时输入声明 变量正在初始化,并发执行应等待 完成初始化。 85 如果在初始化变量时控件递归地重新输入声明,则 行为是不确定的。

85 该实现不得在初始化程序的执行过程中引入任何死锁。死锁可能仍然是由程序逻辑引起的。该实现只需避免由于自身的同步操作而导致的死锁。

在建立了这个之后,我们可以去提问。

问题1:这是否保证安全(无死锁等)? C ++ 11静态初始化涉及锁。如果在main中第一次调用GetFoo()之前/之后/期间传递信号怎么办?

不,不能保证。考虑一下何时从GetFoo()循环内部调用for的时间:

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'的初始化尚未完成,信号处理程序必须在这里等待-请参阅上面的引言)。

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

问题2:如果在安装信号处理程序之前在[2]处插入对GetFoo()的调用,这是否保证安全?

在这种情况下,根本没有为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;
    }
}