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

Linux C语言定时器精确计时技巧

2024-10-043.5k 阅读

1. Linux 下时间相关概念

在深入探讨定时器精确计时技巧之前,我们先来了解一下 Linux 系统下与时间相关的一些基本概念。

1.1 系统时钟

Linux 系统维护着多个时钟,其中最主要的是系统时钟(System Clock)。系统时钟记录了系统启动以来经过的时间,它以硬件时钟为基础进行初始化,并在系统运行过程中不断更新。系统时钟的精度通常取决于硬件定时器的精度,一般来说,现代计算机的系统时钟精度可以达到微秒级别。

1.2 硬件定时器

硬件定时器是计算机硬件中的一个重要组件,它能够按照设定的时间间隔产生中断信号。Linux 内核利用硬件定时器来实现各种时间相关的功能,如进程调度、系统计时等。常见的硬件定时器包括可编程间隔定时器(Programmable Interval Timer,PIT)、高精度事件定时器(High - Precision Event Timer,HPET)等。不同的硬件定时器在精度、功能等方面可能会有所差异。

1.3 时间单位

在 Linux 中,常用的时间单位有秒(second)、毫秒(millisecond,1/1000 秒)、微秒(microsecond,1/1000000 秒)和纳秒(nanosecond,1/1000000000 秒)。在进行精确计时时,我们需要根据具体需求选择合适的时间单位。例如,对于一些实时性要求较高的应用场景,可能需要使用微秒甚至纳秒级别的计时精度。

2. C 语言中常用的计时函数

在 Linux C 语言编程中,有几个常用的函数可以用于计时。下面我们分别介绍这些函数及其特点。

2.1 time 函数

time函数是 C 标准库中的函数,用于获取当前的日历时间,即从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间所经过的秒数。其函数原型如下:

#include <time.h>
time_t time(time_t *t);

其中,time_t是一个整数类型,用于表示时间值。如果参数t不为NULL,则当前时间会存储在t指向的变量中。该函数返回值为当前时间的秒数。例如:

#include <stdio.h>
#include <time.h>

int main() {
    time_t current_time;
    current_time = time(NULL);
    printf("Current time in seconds since 1970-01-01 00:00:00 UTC: %ld\n", (long)current_time);
    return 0;
}

time函数的优点是简单易用,适用于获取相对较长时间间隔的场景,例如计算程序运行了多少天等。但其精度较低,只能精确到秒级别,对于需要高精度计时的场景并不适用。

2.2 gettimeofday 函数

gettimeofday函数可以获取更精确的时间,它能够精确到微秒级别。函数原型如下:

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);

struct timeval结构体定义如下:

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

tv_sec表示秒数,tv_usec表示微秒数。tz参数一般可以设置为NULL。例如,计算一段代码执行时间的示例:

#include <stdio.h>
#include <sys/time.h>

int main() {
    struct timeval start, end;
    gettimeofday(&start, NULL);

    // 模拟一段需要计时的代码
    for (int i = 0; i < 1000000; i++);

    gettimeofday(&end, NULL);
    long seconds = end.tv_sec - start.tv_sec;
    long microseconds = end.tv_usec - start.tv_usec;
    double elapsed_time = seconds + microseconds / 1000000.0;
    printf("Elapsed time: %f seconds\n", elapsed_time);
    return 0;
}

gettimeofday函数在大多数情况下能够满足一般精度要求的计时需求,其精度可以达到微秒级别。然而,在一些对精度要求极高的场景下,如高性能计算、实时信号处理等,微秒级别的精度可能仍然不够。

2.3 clock_gettime 函数

clock_gettime函数是 POSIX 标准定义的函数,它提供了更高精度的计时功能,能够精确到纳秒级别。函数原型如下:

#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp);

clockid_t是一个枚举类型,用于指定要获取的时钟类型,常见的时钟类型有:

  • CLOCK_REALTIME:系统实时时钟,随系统时间的改变而改变。
  • CLOCK_MONOTONIC:单调时钟,从系统启动开始计时,不受系统时间调整的影响。
  • CLOCK_PROCESS_CPUTIME_ID:本进程运行时间的时钟。
  • CLOCK_THREAD_CPUTIME_ID:本线程运行时间的时钟。

struct timespec结构体定义如下:

struct timespec {
    time_t  tv_sec;         /* seconds */
    long    tv_nsec;        /* nanoseconds */
};

下面是一个使用clock_gettime函数计算代码执行时间的示例,以CLOCK_MONOTONIC时钟为例:

#include <stdio.h>
#include <time.h>

int main() {
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);

    // 模拟一段需要计时的代码
    for (int i = 0; i < 1000000; i++);

    clock_gettime(CLOCK_MONOTONIC, &end);
    long seconds = end.tv_sec - start.tv_sec;
    long nanoseconds = end.tv_nsec - start.tv_nsec;
    double elapsed_time = seconds + nanoseconds / 1000000000.0;
    printf("Elapsed time: %f seconds\n", elapsed_time);
    return 0;
}

clock_gettime函数在需要高精度计时的场景中非常有用,尤其是在对时间精度要求苛刻的应用开发中。

3. Linux C 语言定时器实现方式

在 Linux 环境下,我们可以通过多种方式实现定时器功能,每种方式在精度、应用场景等方面都有所不同。

3.1 alarm 函数实现定时器

alarm函数是一个简单的定时器函数,它可以设置一个定时器,当定时器到期时,会向进程发送SIGALRM信号。函数原型如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds参数表示定时器的时间间隔,单位为秒。例如,下面的代码设置了一个 5 秒的定时器:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void signal_handler(int signum) {
    printf("Alarm signal received!\n");
}

int main() {
    signal(SIGALRM, signal_handler);
    alarm(5);
    printf("Waiting for alarm...\n");
    while (1);
    return 0;
}

在上述代码中,通过signal函数注册了SIGALRM信号的处理函数signal_handler,然后使用alarm函数设置了一个 5 秒的定时器。当 5 秒后,SIGALRM信号被发送,signal_handler函数被调用。

alarm函数的优点是简单易用,适用于对精度要求不高且时间间隔以秒为单位的场景。然而,它的精度较低,只能精确到秒级别,并且在定时器到期时只能通过信号机制通知进程,这在一些复杂的应用场景中可能不太灵活。

3.2 setitimer 函数实现定时器

setitimer函数提供了更灵活的定时器设置功能,它可以设置一次性定时器或周期性定时器,并且精度可以达到微秒级别。函数原型如下:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

which参数指定定时器类型,常见的有:

  • ITIMER_REAL:实时定时器,按实际时间计时,到期时发送SIGALRM信号。
  • ITIMER_VIRTUAL:按进程用户态运行时间计时,到期时发送SIGVTALRM信号。
  • ITIMER_PROF:按进程用户态和内核态运行时间总和计时,到期时发送SIGPROF信号。

struct itimerval结构体定义如下:

struct itimerval {
    struct timeval it_interval;  /* 定时器周期 */
    struct timeval it_value;     /* 定时器初始值 */
};

it_interval表示定时器的周期,如果设置为 0,则表示一次性定时器。it_value表示定时器的初始值。

下面是一个使用setitimer函数设置周期性定时器的示例,以ITIMER_REAL类型为例:

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void signal_handler(int signum) {
    printf("Timer expired!\n");
}

int main() {
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_sec = 2;
    timer.it_interval.tv_usec = 0;

    signal(SIGALRM, signal_handler);
    setitimer(ITIMER_REAL, &timer, NULL);

    printf("Waiting for timer...\n");
    while (1);
    return 0;
}

在上述代码中,设置了一个每 2 秒触发一次的周期性定时器,当定时器到期时,会发送SIGALRM信号,从而调用signal_handler函数。

setitimer函数相比alarm函数更加灵活,精度也更高,可以满足一些对时间精度有一定要求且需要周期性定时的场景。但它仍然存在一些局限性,比如在高精度计时场景下,微秒级别的精度可能不够。

3.3 POSIX 定时器实现

POSIX 定时器提供了更高级、更灵活且精度更高的定时器功能,它基于timer_createtimer_settime等函数实现。

首先是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_MONOTONIC等。evp是一个指向struct sigevent结构体的指针,用于指定定时器到期时的通知方式,可以是发送信号或者启动一个线程。timerid用于返回创建的定时器 ID。

struct 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;
};

sigev_notify可以取值为SIGEV_NONE(不通知)、SIGEV_SIGNAL(发送信号)、SIGEV_THREAD(启动线程)等。

timer_settime函数用于设置定时器的时间,原型如下:

#include <time.h>
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

timerid是定时器 ID,flags一般设置为 0。new_value是一个指向struct itimerspec结构体的指针,用于设置定时器的初始值和周期。old_value用于返回之前的定时器设置。

struct itimerspec结构体定义如下:

struct itimerspec {
    struct timespec it_interval;  /* 定时器周期 */
    struct timespec it_value;     /* 定时器初始值 */
};

下面是一个使用 POSIX 定时器的示例,以CLOCK_MONOTONIC时钟为例,通过发送信号通知:

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

void signal_handler(int signum, siginfo_t *info, void *context) {
    printf("Timer expired! Value: %d\n", info->si_value.sival_int);
}

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

    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1;
    sev.sigev_value.sival_int = 42;

    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    its.it_value.tv_sec = 3;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 3;
    its.it_interval.tv_nsec = 0;

    struct sigaction sa;
    sa.sa_sigaction = signal_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    timer_settime(timerid, 0, &its, NULL);

    printf("Waiting for timer...\n");
    while (1);
    return 0;
}

在上述代码中,首先创建了一个基于CLOCK_MONOTONIC时钟的定时器,设置定时器到期时发送SIGUSR1信号,并传递一个整数值 42。然后设置定时器每 3 秒触发一次。通过sigaction函数注册SIGUSR1信号的处理函数signal_handler,在处理函数中打印接收到的整数值。

POSIX 定时器功能强大,精度可以达到纳秒级别,并且在通知方式上更加灵活,可以满足各种高精度、复杂的定时需求场景。

4. 精确计时技巧与注意事项

在实际应用中,要实现精确计时,除了选择合适的计时函数和定时器实现方式外,还需要注意一些技巧和可能遇到的问题。

4.1 选择合适的时钟类型

如前文所述,不同的时钟类型有不同的特点。在选择时钟类型时,需要根据具体需求来决定。

  • 如果需要获取系统的实时时间,并且不受系统时间调整的影响较小,可以选择CLOCK_REALTIME。例如,记录事件发生的绝对时间戳。
  • 对于需要测量一段代码执行时间,并且不希望受到系统时间调整干扰的场景,CLOCK_MONOTONIC是一个很好的选择。比如,性能测试等场景。
  • 如果只关心进程或线程自身的运行时间,那么CLOCK_PROCESS_CPUTIME_IDCLOCK_THREAD_CPUTIME_ID则更为合适,例如统计某个进程或线程在 CPU 上的运行时长。

4.2 减少系统调用开销

系统调用是用户态程序与内核态交互的方式,但系统调用会带来一定的开销。在进行精确计时时,如果在计时区间内包含过多的系统调用,可能会影响计时的精度。例如,频繁的文件 I/O 操作、网络操作等系统调用都可能导致计时误差。因此,在设计代码时,应尽量将非必要的系统调用移出计时区间,或者优化系统调用的频率。

4.3 考虑硬件因素

硬件定时器的精度和性能会对计时结果产生影响。不同的计算机硬件可能采用不同的硬件定时器,其精度和稳定性也有所差异。在一些对精度要求极高的应用中,可能需要对硬件进行针对性的优化或选择性能更好的硬件设备。此外,硬件的负载情况也会影响计时精度,例如在 CPU 负载过高的情况下,定时器的中断可能会被延迟处理,从而导致计时误差。

4.4 处理定时器中断与信号

当使用基于信号的定时器实现方式时,需要妥善处理定时器到期时产生的信号。信号处理函数应尽量简单,避免在信号处理函数中执行复杂的操作,因为信号处理函数的执行可能会打断正常的程序流程,并且信号处理函数的执行环境相对特殊。如果在信号处理函数中执行复杂操作,可能会导致程序出现不可预测的行为,进而影响计时的准确性。

4.5 多次测量取平均值

为了提高计时的准确性,可以对需要计时的操作进行多次测量,然后取平均值。由于系统环境、硬件状态等因素的影响,单次测量的结果可能存在一定的误差。通过多次测量并取平均值,可以在一定程度上减小这种误差,得到更接近真实值的计时结果。例如:

#include <stdio.h>
#include <time.h>

#define NUM_RUNS 10

int main() {
    struct timespec start, end;
    double total_time = 0.0;

    for (int i = 0; i < NUM_RUNS; i++) {
        clock_gettime(CLOCK_MONOTONIC, &start);

        // 模拟一段需要计时的代码
        for (int j = 0; j < 1000000; j++);

        clock_gettime(CLOCK_MONOTONIC, &end);
        long seconds = end.tv_sec - start.tv_sec;
        long nanoseconds = end.tv_nsec - start.tv_nsec;
        double elapsed_time = seconds + nanoseconds / 1000000000.0;
        total_time += elapsed_time;
    }

    double average_time = total_time / NUM_RUNS;
    printf("Average elapsed time: %f seconds\n", average_time);
    return 0;
}

在上述代码中,对一段代码进行了 10 次计时,并计算了平均执行时间,这样可以得到更准确的计时结果。

5. 应用案例分析

为了更好地理解 Linux C 语言定时器精确计时技巧的应用,我们来看几个实际的应用案例。

5.1 实时数据采集系统

在实时数据采集系统中,需要按照一定的时间间隔精确采集数据。例如,采集传感器的数据,要求每 100 毫秒采集一次。我们可以使用 POSIX 定时器来实现这个功能。

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

// 模拟传感器数据采集函数
void collect_data() {
    printf("Collecting data...\n");
}

void signal_handler(int signum, siginfo_t *info, void *context) {
    collect_data();
}

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

    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1;

    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    its.it_value.tv_sec = 0;
    its.it_value.tv_nsec = 100000000;  // 100 毫秒
    its.it_interval.tv_sec = 0;
    its.it_interval.tv_nsec = 100000000;

    struct sigaction sa;
    sa.sa_sigaction = signal_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    timer_settime(timerid, 0, &its, NULL);

    printf("Data collection started...\n");
    while (1);
    return 0;
}

在上述代码中,通过 POSIX 定时器设置了每 100 毫秒触发一次的定时器,当定时器到期时,会调用collect_data函数来模拟传感器数据采集。

5.2 网络心跳检测

在网络通信中,经常需要使用心跳机制来检测连接是否正常。例如,客户端每隔 5 秒向服务器发送一次心跳包。我们可以使用setitimer函数来实现客户端的心跳定时器。

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888

// 模拟发送心跳包函数
void send_heartbeat() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    memset(&servaddr.sin_zero, 0, sizeof(servaddr.sin_zero));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    char buffer[] = "HEARTBEAT";
    sendto(sockfd, buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
    close(sockfd);
    printf("Heartbeat sent...\n");
}

void signal_handler(int signum) {
    send_heartbeat();
}

int main() {
    struct itimerval timer;
    timer.it_value.tv_sec = 5;
    timer.it_value.tv_usec = 0;
    timer.it_interval.tv_sec = 5;
    timer.it_interval.tv_usec = 0;

    signal(SIGALRM, signal_handler);
    setitimer(ITIMER_REAL, &timer, NULL);

    printf("Heartbeat detection started...\n");
    while (1);
    return 0;
}

在上述代码中,使用setitimer函数设置了一个每 5 秒触发一次的定时器,当定时器到期时,会调用send_heartbeat函数向服务器发送心跳包。

5.3 任务调度系统

在任务调度系统中,需要按照设定的时间精确执行任务。例如,有一个任务需要每隔 1 分钟执行一次。我们可以使用 POSIX 定时器结合任务队列来实现这个功能。

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

// 任务结构体
typedef struct Task {
    void (*func)();
    struct Task *next;
} Task;

// 任务队列
Task *task_queue = NULL;

// 模拟任务函数
void task_function() {
    printf("Task executed...\n");
}

// 添加任务到队列
void add_task(void (*func)()) {
    Task *new_task = (Task *)malloc(sizeof(Task));
    new_task->func = func;
    new_task->next = task_queue;
    task_queue = new_task;
}

void signal_handler(int signum, siginfo_t *info, void *context) {
    Task *current = task_queue;
    while (current) {
        current->func();
        current = current->next;
    }
}

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

    add_task(task_function);

    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1;

    timer_create(CLOCK_MONOTONIC, &sev, &timerid);

    its.it_value.tv_sec = 60;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 60;
    its.it_interval.tv_nsec = 0;

    struct sigaction sa;
    sa.sa_sigaction = signal_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    timer_settime(timerid, 0, &its, NULL);

    printf("Task scheduling started...\n");
    while (1);
    return 0;
}

在上述代码中,首先定义了任务结构体和任务队列,然后将任务函数task_function添加到任务队列中。通过 POSIX 定时器设置了每 1 分钟触发一次的定时器,当定时器到期时,会遍历任务队列并执行每个任务。

通过以上应用案例,我们可以看到 Linux C 语言定时器精确计时技巧在不同领域的实际应用,能够满足各种实时性和定时需求。在实际开发中,我们需要根据具体的应用场景,选择合适的计时函数和定时器实现方式,并结合精确计时技巧,以达到最佳的效果。