内核态定时器 timer

需要链接 rt 库

Linux 定时器:精准的时间管理

在各种应用场景中,精确地在特定时间执行某些任务至关重要。无论是周期性地收集系统状态、在预定时间发送告警,还是实现复杂的任务调度,都需要可靠的定时器机制。Linux 内核提供了多种方式来处理时间,其中 POSIX 定时器提供了一种强大且灵活的解决方案。

POSIX 定时器简介

POSIX 定时器是符合 POSIX.1b 标准的实时扩展的一部分,它允许应用程序创建和管理多个高精度定时器。与传统的 sleep()alarm() 等函数相比,POSIX 定时器提供了更精细的控制和更多的功能。

核心概念包括:

  • 定时器 ID (timer_t): 每个成功创建的定时器都会返回一个唯一的标识符,类型为 timer_t。你可以使用这个 ID 来操作特定的定时器。
  • 定时器事件: 当定时器到期时,会产生一个信号或者触发一个线程函数。
  • 定时器属性: 可以设置定时器的触发方式(单次或周期性)、初始延迟以及时间间隔。

关键的系统调用

接下来,我们重点介绍用于创建、设置和管理 POSIX 定时器的关键系统调用。

1. timer_create(): 创建定时器

timer_create() 系统调用用于创建一个新的 POSIX 定时器。

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

int main() {
    timer_t timerid;
    struct sigevent sev;

    // 设置定时器到期时的通知方式为信号
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGRTMIN; // 使用一个实时信号
    sev.sigev_value.sival_ptr = &timerid; // 传递定时器 ID

    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }

    printf("定时器创建成功,ID: %ld\n", (long)timerid);

    // ... 后续设置和使用定时器的代码 ...

    return 0;
}

代码解释:

  • 我们包含了必要的头文件 <signal.h><time.h><stdio.h><stdlib.h>
  • 声明了一个 timer_t 类型的变量 timerid 用于存储定时器 ID,以及一个 struct sigevent 类型的变量 sev 用于设置定时器到期时的行为。
  • sev.sigev_notify = SIGEV_SIGNAL; 指定当定时器到期时,通过发送信号的方式通知进程。
  • sev.sigev_signo = SIGRTMIN; 设置发送的信号为实时信号 SIGRTMIN。实时信号比标准信号具有更高的优先级和可靠性。
  • sev.sigev_value.sival_ptr = &timerid; 允许在信号处理函数中获取定时器 ID。
  • timer_create(CLOCK_REALTIME, &sev, &timerid) 调用创建定时器。
    • CLOCK_REALTIME 指定了使用系统实时时钟。其他时钟类型包括 CLOCK_MONOTONIC(单调递增时钟,不受系统时间调整影响)等。
    • &sev 是指向 sigevent 结构的指针,定义了定时器到期时的行为。
    • &timerid 是一个指向 timer_t 变量的指针,用于接收新创建的定时器 ID。
  • 如果 timer_create() 返回 -1,则表示创建失败,我们使用 perror() 输出错误信息并退出。
  • 成功创建后,我们打印了定时器的 ID。

编译和运行:

gcc timer_create_example.c -o timer_create_example -lrt
./timer_create_example

可能的运行结果:

定时器创建成功,ID: 0

(实际的定时器 ID 可能不同)

2. timer_settime(): 设置定时器

timer_settime() 系统调用用于启动或停止定时器,并设置其初始延迟和间隔时间。

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

#define TIMER_SIG SIGRTMIN

void timer_handler(int sig, siginfo_t *si, void *uc) {
    timer_t *tidp;

    tidp = (timer_t *)si->si_value.sival_ptr;
    printf("[%ld] 定时器到期,信号:%d\n", (long)*tidp, sig);
}

int main() {
    timer_t timerid;
    struct sigevent sev;
    struct itimerspec its;
    struct sigaction sa;

    // 设置信号处理函数
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = timer_handler;
    sigemptyset(&sa.sa_mask);
    if (sigaction(TIMER_SIG, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // 创建定时器
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = TIMER_SIG;
    sev.sigev_value.sival_ptr = &timerid;
    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }
    printf("定时器创建成功,ID: %ld\n", (long)timerid);

    // 设置定时器的初始延迟和间隔
    its.it_value.tv_sec = 2;      // 初始延迟 2 秒
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 1;   // 间隔时间 1 秒(周期性定时器)
    its.it_interval.tv_nsec = 0;

    if (timer_settime(timerid, 0, &its, NULL) == -1) {
        perror("timer_settime");
        exit(EXIT_FAILURE);
    }

    printf("定时器已启动,每隔 1 秒触发一次,初始延迟 2 秒...\n");

    // 让程序运行一段时间
    sleep(10);

    // 停止定时器
    its.it_value.tv_sec = 0;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 0;
    its.it_interval.tv_nsec = 0;
    if (timer_settime(timerid, 0, &its, NULL) == -1) {
        perror("timer_settime (停止)");
        exit(EXIT_FAILURE);
    }
    printf("定时器已停止。\n");

    // 删除定时器
    if (timer_delete(timerid) == -1) {
        perror("timer_delete");
        exit(EXIT_FAILURE);
    }
    printf("定时器已删除。\n");

    return 0;
}

代码解释:

  • 我们定义了一个信号处理函数 timer_handler,当定时器到期并发送 TIMER_SIG 信号时,该函数会被调用。
  • main 函数中,我们首先使用 sigaction() 设置了信号 TIMER_SIG 的处理方式,将其与 timer_handler 函数关联起来。SA_SIGINFO 标志表示信号处理函数接收附加信息(通过 siginfo_t 结构)。
  • 创建定时器的步骤与之前的示例相同。
  • 我们声明了一个 struct itimerspec 类型的变量 its,用于设置定时器的超时值。
    • its.it_value 指定了定时器的初始延迟。
    • its.it_interval 指定了定时器的间隔时间。如果 it_interval 的两个字段都设置为 0,则定时器只触发一次。如果都大于 0,则定时器会周期性地触发。
  • timer_settime(timerid, 0, &its, NULL) 调用启动或修改定时器。
    • timerid 是要操作的定时器 ID。
    • 第二个参数 flags 可以是 0 或 TIMER_ABSTIME。如果为 0,则 it_value 是相对时间;如果为 TIMER_ABSTIME,则 it_value 是绝对时间(基于定时器的时钟)。
    • &its 是指向 itimerspec 结构的指针,包含了新的定时器设置。
    • NULL 表示我们不关心旧的定时器设置。如果需要获取之前的设置,可以传递一个指向 itimerspec 结构的指针。
  • 程序使用 sleep(10) 暂停 10 秒,以便观察定时器的触发。
  • 之后,我们将 its.it_valueits.it_interval 都设置为 0,从而停止了定时器。
  • 最后,我们使用 timer_delete() 系统调用删除了不再需要的定时器。

编译和运行:

gcc timer_settime_example.c -o timer_settime_example -lrt
./timer_settime_example

可能的运行结果:

定时器创建成功,ID: 0
定时器已启动,每隔 1 秒触发一次,初始延迟 2 秒...
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
[0] 定时器到期,信号:34
定时器已停止。
定时器已删除。

(实际的定时器 ID 和信号编号可能不同,34 对应 SIGRTMIN

3. timer_gettime(): 获取定时器状态

timer_gettime() 系统调用用于获取当前定时器的剩余时间和间隔。

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

int main() {
    timer_t timerid;
    struct sigevent sev;
    struct itimerspec its;
    struct itimerspec current_its;

    // 创建一个单次触发的定时器
    sev.sigev_notify = SIGEV_NONE; // 不发送信号
    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }
    printf("定时器创建成功,ID: %ld\n", (long)timerid);

    // 设置定时器在 3 秒后触发一次
    its.it_value.tv_sec = 3;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 0;
    its.it_interval.tv_nsec = 0;

    if (timer_settime(timerid, 0, &its, NULL) == -1) {
        perror("timer_settime");
        exit(EXIT_FAILURE);
    }
    printf("定时器已设置为 3 秒后触发。\n");

    sleep(1);

    // 获取定时器的当前状态
    if (timer_gettime(timerid, &current_its) == -1) {
        perror("timer_gettime");
        exit(EXIT_FAILURE);
    }

    printf("当前剩余时间: %ld 秒 %ld 纳秒\n",
           (long)current_its.it_value.tv_sec, (long)current_its.it_value.tv_nsec);
    printf("间隔时间: %ld 秒 %ld 纳秒\n",
           (long)current_its.it_interval.tv_sec, (long)current_its.it_interval.tv_nsec);

    // 等待定时器触发
    sleep(3);

    // 再次获取定时器状态(应该已经触发)
    if (timer_gettime(timerid, &current_its) == -1) {
        perror("timer_gettime");
        exit(EXIT_FAILURE);
    }

    printf("触发后剩余时间: %ld 秒 %ld 纳秒\n",
           (long)current_its.it_value.tv_sec, (long)current_its.it_value.tv_nsec);
    printf("触发后间隔时间: %ld 秒 %ld 纳秒\n",
           (long)current_its.it_interval.tv_sec, (long)current_its.it_interval.tv_nsec);

    if (timer_delete(timerid) == -1) {
        perror("timer_delete");
        exit(EXIT_FAILURE);
    }
    printf("定时器已删除。\n");

    return 0;
}

代码解释:

  • 我们创建了一个单次触发的定时器,设置 sev.sigev_notify = SIGEV_NONE; 表示不发送信号,仅仅使用定时器本身的功能。
  • 使用 timer_settime() 设置定时器在 3 秒后触发一次。
  • 在等待 1 秒后,我们调用 timer_gettime(timerid, &current_its) 来获取当前定时器的状态,并将结果存储在 current_its 结构中。
  • 我们打印了当前的剩余时间和间隔时间。对于单次触发的定时器,触发后剩余时间通常会接近 0。
  • 程序等待 3 秒,让定时器触发。
  • 再次调用 timer_gettime(),可以看到触发后剩余时间通常为 0。

编译和运行:

gcc timer_gettime_example.c -o timer_gettime_example -lrt
./timer_gettime_example

可能的运行结果:

定时器创建成功,ID: 0
定时器已设置为 3 秒后触发。
当前剩余时间: 2 秒 999999999 纳秒
间隔时间: 0 秒 0 纳秒
触发后剩余时间: 0 秒 0 纳秒
触发后间隔时间: 0 秒 0 纳秒
定时器已删除。

(实际的纳秒值可能略有不同)

其他重要的系统调用

除了上述三个核心系统调用外,还有一个用于删除定时器的函数:

  • timer_delete(timer_t timerid): 删除由 timer_create() 创建的定时器。一旦删除,与该定时器 ID 关联的资源将被释放。