POSIX 计时器可以安全地修改 C++ STL 对象吗?

Can POSIX timers safely modify C++ STL objects?

我正在尝试为 Linux 上的 POSIX 计时器系统编写一个 C++ "wrapper",这样我的 C++ 程序就可以设置超时(例如等待消息通过网络到达)使用系统时钟,无需处理 POSIX 丑陋的 C 接口。它似乎大部分时间都有效,但偶尔我的程序会在 运行ning 成功几分钟后出现段错误。问题似乎是我的 LinuxTimerManager 对象(或其成员对象之一)的内存已损坏,但不幸的是,如果我 运行 Valgrind 下的程序,问题就不会出现,所以我是一直盯着我的代码,试图找出它出了什么问题。

这是我的计时器包装器实现的核心:

LinuxTimerManager.h:

namespace util {

using timer_id_t = int;

class LinuxTimerManager {
private:
    timer_id_t next_id;
    std::map<timer_id_t, timer_t> timer_handles;
    std::map<timer_id_t, std::function<void(void)>> timer_callbacks;
    std::set<timer_id_t> cancelled_timers;
    friend void timer_signal_handler(int signum, siginfo_t* info, void* ucontext);
public:
    LinuxTimerManager();
    timer_id_t register_timer(const int delay_ms, std::function<void(void)> callback);
    void cancel_timer(const timer_id_t timer_id);
};

void timer_signal_handler(int signum, siginfo_t* info, void* ucontext);
}

LinuxTimerManager.cpp:

namespace util {

LinuxTimerManager* tm_instance;

LinuxTimerManager::LinuxTimerManager() : next_id(0) {
    tm_instance = this;
    struct sigaction sa = {0};
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = timer_signal_handler;
    sigemptyset(&sa.sa_mask);
    int success_flag = sigaction(SIGRTMIN, &sa, NULL);
    assert(success_flag == 0);
}

void timer_signal_handler(int signum, siginfo_t* info, void* ucontext) {
    timer_id_t timer_id = info->si_value.sival_int;
    auto cancelled_location = tm_instance->cancelled_timers.find(timer_id);
     //Only fire the callback if the timer is not in the cancelled set
    if(cancelled_location == tm_instance->cancelled_timers.end()) {
        tm_instance->timer_callbacks.at(timer_id)();
    } else {
        tm_instance->cancelled_timers.erase(cancelled_location);
    }
    tm_instance->timer_callbacks.erase(timer_id);
    timer_delete(tm_instance->timer_handles.at(timer_id));
    tm_instance->timer_handles.erase(timer_id);
}

timer_id_t LinuxTimerManager::register_timer(const int delay_ms, std::function<void(void)> callback) {
    struct sigevent timer_event = {0};
    timer_event.sigev_notify = SIGEV_SIGNAL;
    timer_event.sigev_signo = SIGRTMIN;
    timer_event.sigev_value.sival_int = next_id;

    timer_t timer_handle;
    int success_flag = timer_create(CLOCK_REALTIME, &timer_event, &timer_handle);
    assert(success_flag == 0);
    timer_handles[next_id] = timer_handle;
    timer_callbacks[next_id] = callback;

    struct itimerspec timer_spec = {0};
    timer_spec.it_interval.tv_sec = 0;
    timer_spec.it_interval.tv_nsec = 0;
    timer_spec.it_value.tv_sec = 0;
    timer_spec.it_value.tv_nsec = delay_ms * 1000000;
    timer_settime(timer_handle, 0, &timer_spec, NULL);

    return next_id++; 
}


void LinuxTimerManager::cancel_timer(const timer_id_t timer_id) {
    if(timer_handles.find(timer_id) != timer_handles.end()) {
        cancelled_timers.emplace(timer_id);
    }
}

}

当我的程序崩溃时,段错误总是来自 timer_signal_handler(),通常是行 tm_instance->timer_callbacks.erase(timer_id)tm_instance->timer_handles.erase(timer_id)。实际的段错误是从 std::map 实现的某个深处抛出的(即 stl_tree.h)。

我的内存损坏可能是由于修改同一个 LinuxTimerManager 的不同计时器信号之间的竞争条件引起的吗?我以为一次只传递一个计时器信号,但也许我误解了手册页。使 Linux 信号处理程序修改像 std::map 这样的复杂 C++ 对象通常是不安全的吗?

信号可能发生在例如中间。 mallocfree 因此大多数使用容器做有趣事情的调用可能会导致重新进入内存分配支持,而其数据结构处于任意状态。 (正如评论中所指出的,大多数函数在异步信号处理程序中调用是不安全的。mallocfree 只是示例。)以这种方式重新进入组件会导致几乎任意失败。

如果不在库内的任何操作期​​间阻止整个进程的信号,就无法使库免受此行为的影响。这样做的成本非常高,无论是管理信号掩码的开销还是信号被阻塞的时间量。 (它必须适用于整个进程,因为信号处理程序不应阻塞在锁上。如果处理信号的线程调用受互斥锁保护的库,而另一个线程持有信号处理程序所需的互斥锁,则处理程序将阻塞。它是发生这种情况时很难避免死锁。)

解决此问题的设计通常有一个线程来侦听特定事件,然后进行处理。您必须使用信号量在线程和信号处理程序之间进行同步。