Linux定时器待处理信号

5
我正在使用 timer_create 函数和 SIGEV_THREAD 参数创建 Linux 定时器。
有时,在我取消并删除定时器后,回调仍会被调用。这会导致段错误,因为它尝试访问已删除的资源。
Linux 手册上说:
“timer_delete() 函数删除 ID 为 timerid 的定时器。如果在此调用时定时器已启动,则在删除前先将其停止。任何由已删除的定时器生成的待处理信号的处理未指定。”
基本上这意味着我不知道回调是否会被调用,也没有方法来取消它或在清理资源之前强制挂起信号传递。请参见链接:http://man7.org/linux/man-pages/man2/timer_delete.2.html
class timer_wrapper
{
private:
    std::function<void()> callback_;
    timer_t timer_;

    static void timer_callback(sigval_t val)
    {
        static_cast<timer_wrapper*>(val.sival_ptr)->callback_();
    }
public:
    timer_wrapper(std::function<void()> callback, uint32_t interval_sec)
        : callback_(std::move(callback))
    {
        struct sigevent ev;
        ev.sigev_notify = SIGEV_THREAD;
        ev.sigev_signo = 0;
        ev.sigev_value.sival_ptr = this;
        ev.sigev_notify_function = &timer_wrapper::timer_callback;
        ev.sigev_notify_attributes = 0;
        timer_create(CLOCK_REALTIME, &ev, &timer_);

        struct itimerspec spec = {{0, 0}, {interval_sec, 0}};
        timer_settime(timer_, 0, &spec, nullptr);
    }

    ~timer_wrapper()
    {
        timer_delete(timer_);
    }
};

如果timer_wrapper超出范围,我希望回调不再被调用,然而有时候它仍会被调用,根据手册,这是期望的行为。如何解决这个问题?

你是否在sigev处理程序中检查你的资源状态? - aram
也许你可以设置一个标志来检查你正在删除的资源的状态? - babu646
不,我怎么检查呢?我有一个指针,是从sigval_t获取的,我不知道它是否已被删除。 - incognito
为什么不把你的代码以 MCVE 的形式发布出来? - aram
他们似乎尝试通过在glibc中使用全局计时器列表来缓解问题,并在全局互斥锁下检查此列表中的计时器存在性以及使用timer_delete从该列表中删除计时器。不幸的是,这确实无法帮助非平凡的用户可处理资源,除非重新实现该方法,因为最终用户回调会在单独的线程中异步调用。 - dewaffled
显示剩余4条评论
1个回答

2
避免使用 timer_create() 创建的定时器在删除时生成事件的最简单方法是根本不避免它。相反,使用 volatile sig_atomic_t disarmed = 0; 标志,并使事件函数在执行任何其他操作之前测试该标志,并在 disarmed 不为零时立即返回。
这样,您首先设置 disarmed,然后删除定时器。
(更好的方法是使用原子内置函数,即旧式的 __sync_fetch_and_add(&disarmed, 0)__sync_fetch_and_and(&disarmed, 0),或者 __atomic_load_n(&disarmed, __ATOMIC_SEQ_CST)__atomic_exchange_n(&disarmed, 0, __ATOMIC_SEQ_CST),来访问标志,以确保正确的排序。)
对于SIGEV_SIGNAL,您可以首先阻塞信号(使用pthread_sigmask()),然后删除定时器,在定时器删除期间使用带有零超时的sigtimedwait()检查是否已引发信号,最后恢复旧信号掩码。
(我个人使用单个POSIX实时信号(SIGRTMIN+0SIGRTMAX-0在编译时定义),以及一个按事件时间排序的小根堆(每个堆槽包含时间和自定义超时/事件结构的引用)来处理大量事件,配合一个专用线程。)

这很简单,但只适用于单个实例计时器。如果我有多个实例,我应该将这个原子标志存储在哪里?它应该是成员标志(那么我们遇到相同的问题,标志作为实例的一部分被删除),或者一种带有原子标志的容器,需要互斥保护,并且使这种方法不那么简单。 - incognito
对于SIGEV_SIGNAL,我也可以检查未决信号,但我正在尝试找到SIGEV_THREAD的解决方案。如果这不起作用,我会考虑更改计时器。 - incognito
@incognito:在C++中,删除计时器(使用timer_delete())必须是一个单独的操作,与表示计时器的对象的删除分开。当使用SIGEV_THREAD时,需要一个宽限期(包括CPU和墙钟时间),以确保在删除计时器之前,C库没有创建新线程,但新线程没有获得运行时间。由于没有确定性的方法来关闭这个竞争窗口,正确的解决方案是使用专用线程和SIGEV_SIGNAL(在所有其他线程中阻塞信号)。 - Nominal Animal
请注意,您可以使用自己专用的线程处理SIGEV_SIGNAL来轻松“模拟”SIGEV_THREAD行为,并完全避免竞争窗口。专用计时器线程唯一的问题/困难是将“新计时器”和“取消计时器”处理与超时结合起来。在实践中,与其使用信号,不如在请求互斥锁和条件变量上使用pthread_cond_timedwait()(以便睡眠到下一个超时),这样更加健壮。 - Nominal Animal
仿真可能有效,但似乎比标准解决方案更加繁琐。不管怎样,谢谢。我本来期望有一个标准解决方案,但看起来只有一些hack方法。 - incognito
@incognito:我不同意。例如,计时器是一种有限的资源。一个进程有几个计时器是合理的,但需要几十个或更多就可能成为问题。 "模拟"方法实际上有很多积极的方面:您可以指定线程属性,特别是堆栈大小(默认情况下堆栈大小非常大,结果成为可能并发线程数量的瓶颈)。在我认识的任何意义上,这些都不是“黑客”,而只是解决一组问题的不同方法。 - Nominal Animal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接