MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言高精度定时器timer_create()详解

2021-02-217.9k 阅读

一、timer_create() 函数简介

在 Linux 环境下的 C 语言编程中,timer_create() 函数是实现高精度定时器的关键接口。它属于 POSIX 定时器 API 的一部分,为开发者提供了一种灵活且精确控制时间的方式。与传统的基于系统调用(如 alarm())的定时器相比,timer_create() 具有更高的精度和更多的功能特性。

timer_create() 函数的主要作用是创建一个定时器。其函数原型如下:

#include <signal.h>
#include <time.h>

int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid);
  • clock_id:指定定时器所使用的时钟类型。常见的时钟类型有 CLOCK_REALTIMECLOCK_MONOTONICCLOCK_PROCESS_CPUTIME_IDCLOCK_THREAD_CPUTIME_ID 等。
  • evp:指向一个 sigevent 结构体的指针,用于指定定时器到期时采取的动作。如果为 NULL,定时器到期时将产生 SIGALRM 信号。
  • timerid:用于返回新创建的定时器的标识符。

二、时钟类型详解

  1. CLOCK_REALTIME 这是系统的实时时钟,它反映了实际的物理时间。系统时间的任何调整(例如通过 date 命令手动设置时间或通过 NTP 服务同步时间)都会影响这个时钟。当使用 CLOCK_REALTIME 创建定时器时,定时器的计时是基于实际的系统时间流逝。例如,如果系统时间被手动向前调整,定时器可能会提前到期;反之,如果系统时间被向后调整,定时器可能会延迟到期。
  2. CLOCK_MONOTONIC CLOCK_MONOTONIC 是单调递增的时钟,它不受系统时间调整的影响。这个时钟从系统启动开始计时,并且一直单调递增,即使系统时间发生改变(例如通过 date 命令设置时间或者 NTP 同步),CLOCK_MONOTONIC 也不会受到影响。它常用于需要精确测量时间间隔的场景,比如性能测试或者实时系统中对时间精度要求较高的任务调度。
  3. CLOCK_PROCESS_CPUTIME_ID 该时钟类型测量的是调用进程的 CPU 时间。也就是说,它统计的是进程在 CPU 上运行所花费的时间,包括用户态和内核态的时间。当使用这个时钟创建定时器时,定时器的计时是基于进程自身的 CPU 时间消耗。这在分析进程的性能,特别是需要知道进程在 CPU 上实际执行时间的场景中非常有用。
  4. CLOCK_THREAD_CPUTIME_IDCLOCK_PROCESS_CPUTIME_ID 类似,但 CLOCK_THREAD_CPUTIME_ID 测量的是调用线程的 CPU 时间。它仅统计特定线程在 CPU 上运行所花费的时间,为线程级别的性能分析和时间控制提供了支持。在多线程编程中,如果需要对每个线程的 CPU 时间进行精确测量和定时控制,就可以使用这个时钟类型。

三、sigevent 结构体

sigevent 结构体用于指定定时器到期时采取的动作。其定义如下:

union sigval {
    int sival_int;
    void *sival_ptr;
};

struct sigevent {
    int sigev_notify;
    int sigev_signo;
    union sigval sigev_value;
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t *sigev_notify_attributes;
};
  1. sigev_notify 该成员指定定时器到期时的通知方式,它可以取以下几个值:
  • SIGEV_NONE:定时器到期时不产生任何通知。
  • SIGEV_SIGNAL:定时器到期时产生一个信号。此时,sigev_signo 成员指定要产生的信号,sigev_value 成员可以传递给信号处理函数。
  • SIGEV_THREAD:定时器到期时启动一个新线程,执行 sigev_notify_function 指向的函数。sigev_value 作为参数传递给该函数,sigev_notify_attributes 可以指定线程的属性。
  1. sigev_signosigev_notifySIGEV_SIGNAL 时,此成员指定定时器到期时要产生的信号。通常使用自定义信号,以避免与系统默认的信号处理冲突。例如,可以使用 SIGUSR1SIGUSR2 等用户自定义信号。
  2. sigev_value 这是一个联合类型的成员,当 sigev_notifySIGEV_SIGNAL 时,它可以传递一个整数值或指针给信号处理函数。当 sigev_notifySIGEV_THREAD 时,它作为参数传递给 sigev_notify_function 函数。
  3. sigev_notify_functionsigev_notifySIGEV_THREAD 时,此成员指向一个函数,定时器到期时将启动一个新线程来执行该函数。
  4. sigev_notify_attributessigev_notifySIGEV_THREAD 时,此成员用于指定新线程的属性。如果为 NULL,则使用默认的线程属性。

四、定时器的启动与停止

  1. 启动定时器 使用 timer_settime() 函数来启动或重新启动一个定时器。其函数原型如下:
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
  • timerid:要操作的定时器的标识符,由 timer_create() 函数返回。
  • flags:可以为 0 或 TIMER_ABSTIME。如果为 0,表示 new_value 中的时间值是相对当前时间的偏移量;如果为 TIMER_ABSTIME,表示 new_value 中的时间值是绝对时间。
  • new_value:指向一个 itimerspec 结构体的指针,用于指定定时器的新设置。itimerspec 结构体定义如下:
struct itimerspec {
    struct timespec it_interval;
    struct timespec it_value;
};

it_interval 成员指定定时器的周期(如果是周期性定时器),it_value 成员指定定时器首次到期的时间。

  • old_value:如果不为 NULL,将返回定时器原来的设置。
  1. 停止定时器 要停止一个定时器,可以将 new_value 中的 it_valueit_interval 都设置为 0,并调用 timer_settime() 函数。例如:
struct itimerspec new_value;
new_value.it_value.tv_sec = 0;
new_value.it_value.tv_nsec = 0;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_nsec = 0;
timer_settime(timerid, 0, &new_value, NULL);

五、代码示例

  1. 基于信号的定时器示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>

void signal_handler(int signum) {
    printf("Timer expired. Signal received: %d\n", signum);
}

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

    // 设置信号处理函数
    signal(SIGUSR1, signal_handler);

    // 初始化 sigevent 结构体
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1;
    sev.sigev_value.sival_ptr = &timerid;

    // 创建定时器
    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }

    // 设置定时器的初始值和周期
    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("Timer started. Press Enter to cancel...\n");
    getchar();

    // 停止定时器
    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);
    }

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

    return 0;
}

在这个示例中,我们创建了一个基于 CLOCK_REALTIME 时钟的定时器,当定时器到期时,会产生 SIGUSR1 信号,信号处理函数 signal_handler 会被调用并打印一条消息。定时器首次到期时间为 2 秒,之后每隔 1 秒到期一次。用户可以通过按回车键停止并删除定时器。

  1. 基于线程的定时器示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>

void timer_callback(union sigval sv) {
    printf("Timer expired. Thread started.\n");
}

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

    // 初始化 sigevent 结构体
    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = timer_callback;
    sev.sigev_notify_attributes = NULL;
    sev.sigev_value.sival_ptr = &timerid;

    // 创建定时器
    if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) {
        perror("timer_create");
        exit(EXIT_FAILURE);
    }

    // 设置定时器的初始值和周期
    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("Timer started. Press Enter to cancel...\n");
    getchar();

    // 停止定时器
    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);
    }

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

    return 0;
}

在这个示例中,我们创建了一个基于线程的定时器。当定时器到期时,会启动一个新线程并执行 timer_callback 函数。定时器的设置和控制与基于信号的示例类似,但通知方式是通过启动线程而不是发送信号。

六、注意事项

  1. 信号处理与线程安全 当使用 SIGEV_SIGNAL 方式时,要注意信号处理函数中的操作应该是线程安全的。因为信号可能在任何时候中断程序的执行,如果信号处理函数中包含非线程安全的操作(例如对共享资源的不加锁访问),可能会导致程序出现未定义行为。
  2. 时钟选择 根据应用场景选择合适的时钟类型非常重要。如果需要与实际时间相关的定时(如定时备份任务),则应选择 CLOCK_REALTIME;如果需要精确测量时间间隔且不受系统时间调整影响(如性能测试),则应选择 CLOCK_MONOTONIC。对于进程或线程级别的 CPU 时间测量,应选择相应的 CPU 时间时钟类型。
  3. 资源管理 在使用完定时器后,务必调用 timer_delete() 函数删除定时器,以释放系统资源。否则,可能会导致资源泄漏,特别是在长时间运行的程序中。
  4. 精度问题 虽然 timer_create() 提供了高精度的定时器功能,但实际的精度可能会受到硬件和系统负载的影响。在高负载的系统中,定时器的实际到期时间可能会与设定时间有一定的偏差。

七、与其他定时器机制的比较

  1. 与 alarm() 函数的比较 alarm() 函数是 Unix 系统中一种简单的定时器机制,其函数原型为 unsigned int alarm(unsigned int seconds)。它只能设置一个相对时间的单次定时器,精度通常为秒级,并且只能产生 SIGALRM 信号。而 timer_create() 函数可以创建高精度、周期性的定时器,并且可以自定义到期时的通知方式和信号,功能更为强大。
  2. 与 setitimer() 函数的比较 setitimer() 函数可以设置相对时间的单次或周期性定时器,其精度可以达到微秒级。与 timer_create() 相比,setitimer() 的灵活性稍逊一筹。timer_create() 提供了更多的时钟类型选择,并且可以通过 sigevent 结构体实现更丰富的通知方式,如基于线程的通知。

八、应用场景

  1. 实时系统 在实时系统中,需要精确控制任务的执行时间。例如,工业控制系统中的数据采集任务可能需要每隔一定时间(如几毫秒)采集一次传感器数据。timer_create() 函数的高精度和灵活的定时设置可以满足这类需求。
  2. 网络编程 在网络编程中,经常需要设置超时机制。例如,在客户端向服务器发送请求后,需要在一定时间内收到响应,否则认为请求超时。使用 timer_create() 可以精确设置超时时间,提高网络应用的健壮性。
  3. 性能分析 在对程序进行性能分析时,需要精确测量函数或代码段的执行时间。通过使用基于 CLOCK_PROCESS_CPUTIME_IDCLOCK_THREAD_CPUTIME_ID 的定时器,可以统计进程或线程在 CPU 上的执行时间,帮助开发者优化代码性能。
  4. 定时任务调度 类似于 cron 服务的定时任务调度,应用程序内部可能需要定时执行某些任务,如定时清理缓存、定时备份数据等。timer_create() 函数可以方便地实现这些定时任务的调度,并且可以根据需要设置任务的执行周期和首次执行时间。

通过深入理解 timer_create() 函数的原理、使用方法以及注意事项,开发者可以在 Linux C 语言编程中充分利用高精度定时器,实现各种复杂的时间控制和定时任务需求。无论是开发实时系统、网络应用还是进行性能分析,timer_create() 都是一个非常强大且实用的工具。在实际应用中,根据具体场景合理选择时钟类型、通知方式,并注意资源管理和线程安全等问题,能够确保程序的稳定性和高效性。