在Linux上创建一个定时器的方法

316 阅读4分钟

对开发者来说,某些事件的定时是一项常见的任务。计时器的常见场景是看门狗、任务的循环执行或为特定时间调度事件。在这篇文章中,我展示了如何使用timer_create(...)创建一个符合POSIX标准的时间间隔计时器。

你可以从GitHub下载以下例子的源代码。

准备好Qt Creator

我使用Qt Creator作为这个例子的IDE。要在Qt Creator中运行和调试示例代码,请克隆GitHub仓库,打开Qt Creator,然后进入文件->打开文件或项目...,选择CMakeLists.txt

Qt Creator open project

在Qt Creator中打开一个项目(CC-BY-SA 4.0)

选择工具链后,点击配置项目。该项目包含三个独立的例子(在本文中我们将只介绍其中的两个)。通过绿色标记的菜单,在每个例子的配置之间切换,并激活每个例子的终端运行 (见下面的黄色标记)。可以通过左下角的Debug按钮选择当前活动的例子进行构建和调试(见下面的橙色标记)。

Project configuration

项目配置 (CC-BY-SA 4.0)

线程定时器

让我们来看看simple_threading_timer.c的例子。这是最简单的一个。它显示了如何创建一个间隔定时器,在到期时调用函数expired。在每次过期时,都会创建一个新的线程,在其中调用函数expired

#include 
#include 
#include 
#include 
#include 
#include 
#include 
void expired(union sigval timer_data);
pid_t gettid(void);
struct t_eventData{
    int myData;
};
int main()
{
    int res = 0;
    timer_t timerId = 0;
    struct t_eventData eventData = { .myData = 0 };
    /*  sigevent specifies behaviour on expiration  */
    struct sigevent sev = { 0 };
    /* specify start delay and interval
     * it_value and it_interval must not be zero */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };
    printf("Simple Threading Timer - thread-id: %d\n", gettid());
    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = &expired;
    sev.sigev_value.sival_ptr = &eventData;
    /* create timer */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);
    if (res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }
    /* start timer */
    res = timer_settime(timerId, 0, &its, NULL);
    if (res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }
    printf("Press ETNER Key to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}
void expired(union sigval timer_data){
    struct t_eventData *data = timer_data.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

这种方法的优点是在代码和简单调试方面占用的空间小。缺点是由于在过期时创建一个新的线程而产生的额外开销,因此,行为的确定性较差。

中断信号定时器

另一种被过期定时器通知的可能性是基于一个内核信号的。内核不是在每次定时器过期时创建一个新的线程,而是向进程发送一个信号,进程被中断,并调用相应的信号处理器。

由于收到信号时的默认行为是终止进程(见信号手册页),我们必须事先准备好Qt Creator,这样才能正确地进行调试。

当调试器收到一个信号时,Qt Creator的默认行为是。

  • 中断执行并切换到调试器上下文。
  • 显示一个弹出式窗口,通知用户收到信号。

这两个动作都是不需要的,因为接收信号是我们应用程序的一部分。

Qt Creator在后台使用GDB。为了防止GDB在进程收到信号时停止执行,进入工具->选项,选择调试器,并导航到本地和表达式。在Debugging Helper Customization中添加以下表达式。

handle SIG34 nostop pass

Signal no stop with error

Sig 34 no stop with error (CC-BY-SA 4.0)

你可以在GDB文档中找到更多关于GDB信号处理的信息。

接下来,我们要抑制在信号处理程序中停止时每次收到信号时通知我们的弹出窗口。

Signal 34 pop up box

信号34弹出框(CC-BY-SA 4.0)

要做到这一点,请浏览选项卡GDB并取消勾选标记的复选框。

Timer signal windows

计时器信号窗口 (CC-BY-SA 4.0)

现在你可以正确调试signal_interrupt_timer了。信号定时器的实际实现要复杂一些。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define UNUSED(x) (void)(x)
static void handler(int sig, siginfo_t *si, void *uc);
pid_t gettid(void);
struct t_eventData{
    int myData;
};
int main()
{
    int res = 0;
    timer_t timerId = 0;
    struct sigevent sev = { 0 };
    struct t_eventData eventData = { .myData = 0 };
    /* specifies the action when receiving a signal */
    struct sigaction sa = { 0 };
    /* specify start delay and interval */
    struct itimerspec its = {   .it_value.tv_sec  = 1,
                                .it_value.tv_nsec = 0,
                                .it_interval.tv_sec  = 1,
                                .it_interval.tv_nsec = 0
                            };
    printf("Signal Interrupt Timer - thread-id: %d\n", gettid());
    sev.sigev_notify = SIGEV_SIGNAL; // Linux-specific
    sev.sigev_signo = SIGRTMIN;
    sev.sigev_value.sival_ptr = &eventData;
    /* create timer */
    res = timer_create(CLOCK_REALTIME, &sev, &timerId);
    if ( res != 0){
        fprintf(stderr, "Error timer_create: %s\n", strerror(errno));
        exit(-1);
    }
    /* specifz signal and handler */
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;
    /* Initialize signal */
    sigemptyset(&sa.sa_mask);
    printf("Establishing handler for signal %d\n", SIGRTMIN);
    /* Register signal handler */
    if (sigaction(SIGRTMIN, &sa, NULL) == -1){
        fprintf(stderr, "Error sigaction: %s\n", strerror(errno));
        exit(-1);
    }
    /* start timer */
    res = timer_settime(timerId, 0, &its, NULL);
    if ( res != 0){
        fprintf(stderr, "Error timer_settime: %s\n", strerror(errno));
        exit(-1);
    }
    printf("Press ENTER to Exit\n");
    while(getchar()!='\n'){}
    return 0;
}
static void
handler(int sig, siginfo_t *si, void *uc)
{
    UNUSED(sig);
    UNUSED(uc);
    struct t_eventData *data = (struct t_eventData *) si->_sifields._rt.si_sigval.sival_ptr;
    printf("Timer fired %d - thread-id: %d\n", ++data->myData, gettid());
}

与线程定时器相比,我们必须初始化信号并注册一个信号处理器。这种方法更具有性能,因为它不会导致创建额外的线程。由于这个原因,信号处理程序的执行也更具有确定性。缺点显然是需要额外的配置工作来正确调试。

总结

本文所描述的两种方法都是对定时器的接近内核的实现。即使timer_create(...)函数是 POSIX 规范的一部分,由于数据结构的细微差别,也不可能在 FreeBSD 系统上编译示例代码。除了这个缺点,这样的实现为你提供了对通用定时应用的细粒度控制。