在 Linux 上创建定时器

本教程展示了如何创建一个符合 POSIX 标准的间隔定时器。
52 位读者喜欢这篇文章。
Team checklist

对于开发者来说,定时处理某些事件是一项常见的任务。定时器的常见应用场景包括看门狗、任务的循环执行或在特定时间调度事件。在本文中,我将展示如何使用 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)

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

Project configuration

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

线程定时器

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

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

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

这种方法的优点是代码量小,调试简单。缺点是由于每次到期时都创建新线程而产生的额外开销,以及由此导致的较差的确定性行为。

中断信号定时器

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

由于接收到信号时的默认操作是终止进程(请参阅 signal 手册页),我们必须提前准备好 Qt Creator,以便能够正确地进行调试。

当被调试程序接收到信号时,Qt Creator 的默认行为是

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

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

Qt Creator 在后台使用 GDB。为了防止 GDB 在进程接收到信号时停止执行,请转到 工具 -> 选项,选择 调试器,然后导航到 局部变量和表达式。将以下表达式添加到 调试助手自定义

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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#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 系统上编译示例代码。除了这个缺点之外,这种实现为您提供了通用定时应用程序的细粒度控制。

接下来阅读
标签
User profile image.
Stephan 是一位技术爱好者,他欣赏开源,因为它可以深入了解事物的运作方式。Stephan 在工业自动化软件这个主要为专有领域的行业中担任全职支持工程师。如果可能,他会从事基于 Python 的开源项目、撰写文章或骑摩托车。

评论已关闭。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.