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

Linux C语言定时器的定时器嵌套使用

2022-04-233.6k 阅读

Linux C 语言定时器概述

在 Linux 环境下,C 语言提供了多种定时器机制来满足不同的应用需求。定时器在许多场景中都至关重要,比如网络通信中的超时控制、系统任务的定时调度等。常见的定时器类型有间隔定时器(Interval Timer)和单次定时器(One - shot Timer)。

常见定时器类型

  1. 间隔定时器:这种定时器会按照设定的时间间隔周期性地触发。例如,一个网络心跳检测机制,每隔一定时间(如 1 秒)向服务器发送一个心跳包,以确保连接的有效性。在 Linux 中,可以使用 setitimer 函数来创建间隔定时器。
  2. 单次定时器:单次定时器在设定的时间到达后只触发一次。例如,在一个文件下载任务中,如果超过一定时间(如 10 分钟)还未完成下载,就终止该任务。这种情况下就可以使用单次定时器。alarm 函数是实现单次定时器的一种简单方式。

Linux C 语言定时器的基本使用

使用 alarm 函数创建单次定时器

alarm 函数是一个简单的用于设置单次定时器的函数,它的原型如下:

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

该函数设置一个定时器,在 seconds 秒后向进程发送 SIGALRM 信号。返回值是之前设置的定时器剩余的秒数,如果之前没有设置定时器,则返回 0。

以下是一个简单的示例代码:

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

void sigalrm_handler(int signum) {
    printf("Received SIGALRM signal\n");
}

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

在上述代码中,我们首先定义了一个信号处理函数 sigalrm_handler 来处理 SIGALRM 信号。然后在 main 函数中,通过 signal 函数将 SIGALRM 信号与我们定义的处理函数关联起来。接着使用 alarm(5) 设置一个 5 秒的定时器。在 while 循环中,程序会每秒打印一次 “Waiting...”,直到 5 秒后触发 SIGALRM 信号,执行信号处理函数。

使用 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

new_value 是一个指向 itimerval 结构体的指针,用于设置定时器的新值。old_value 也是一个指向 itimerval 结构体的指针,用于返回定时器的旧值,如果不需要旧值,可以设为 NULL

itimerval 结构体定义如下:

struct itimerval {
    struct timeval it_interval;
    struct timeval it_value;
};
struct timeval {
    time_t      tv_sec;
    suseconds_t tv_usec;
};

it_interval 表示定时器的间隔时间,it_value 表示定时器首次触发的时间。

以下是创建间隔定时器的示例代码:

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

void sigalrm_handler(int signum) {
    printf("Received SIGALRM signal\n");
}

int main() {
    struct itimerval itv;
    signal(SIGALRM, sigalrm_handler);

    itv.it_interval.tv_sec = 2;
    itv.it_interval.tv_usec = 0;
    itv.it_value.tv_sec = 2;
    itv.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL, &itv, NULL);

    while (1) {
        sleep(1);
        printf("Waiting...\n");
    }
    return 0;
}

在这段代码中,我们设置了一个间隔为 2 秒的定时器。itv.it_interval 设置了定时器的间隔时间为 2 秒,itv.it_value 设置了首次触发时间也为 2 秒。通过 setitimer 函数将这个定时器设置为 ITIMER_REAL 类型,即使用系统真实时间。当定时器触发时,会执行 sigalrm_handler 函数。

定时器嵌套使用的概念

什么是定时器嵌套

定时器嵌套是指在一个定时器的处理函数中,再次设置另一个定时器。这种技术可以实现复杂的定时任务调度。例如,在一个网络爬虫程序中,可能需要先定时获取一批网页链接,然后在获取到链接后,针对每个链接再定时发起请求。这里就可以使用定时器嵌套,外层定时器控制获取链接的频率,内层定时器控制对每个链接发起请求的时间。

定时器嵌套的优势

  1. 灵活的任务调度:可以根据不同的任务需求,在不同的时间层次上进行任务调度。比如在一个游戏开发中,外层定时器可以控制游戏场景的切换频率,内层定时器可以控制场景中具体角色的动作时间间隔。
  2. 提高系统资源利用率:避免了一次性启动大量定时器造成的资源浪费。通过嵌套,可以根据前一个定时器的执行结果,动态地决定是否需要启动下一个定时器。

定时器嵌套使用的实现

基于 alarm 函数的定时器嵌套

我们可以在 SIGALRM 信号处理函数中再次调用 alarm 函数来实现定时器嵌套。以下是一个简单的示例:

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

void inner_alarm_handler(int signum) {
    printf("Inner alarm triggered\n");
}

void outer_alarm_handler(int signum) {
    printf("Outer alarm triggered\n");
    signal(SIGALRM, inner_alarm_handler);
    alarm(3);
}

int main() {
    signal(SIGALRM, outer_alarm_handler);
    alarm(5);
    printf("Setting outer alarm for 5 seconds\n");
    while (1) {
        sleep(1);
        printf("Waiting...\n");
    }
    return 0;
}

在上述代码中,我们定义了两个信号处理函数 inner_alarm_handlerouter_alarm_handler。在 main 函数中,首先设置了一个 5 秒的外层定时器,当 5 秒后 outer_alarm_handler 被调用,在这个函数中,我们又设置了一个 3 秒的内层定时器,并将 SIGALRM 信号处理函数切换为 inner_alarm_handler。这样就实现了定时器的嵌套。

基于 setitimer 函数的定时器嵌套

同样,我们也可以使用 setitimer 函数来实现定时器嵌套。以下是一个示例:

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

void inner_setitimer_handler(int signum) {
    printf("Inner setitimer triggered\n");
}

void outer_setitimer_handler(int signum) {
    printf("Outer setitimer triggered\n");
    struct itimerval itv_inner;
    itv_inner.it_interval.tv_sec = 2;
    itv_inner.it_interval.tv_usec = 0;
    itv_inner.it_value.tv_sec = 2;
    itv_inner.it_value.tv_usec = 0;

    signal(SIGALRM, inner_setitimer_handler);
    setitimer(ITIMER_REAL, &itv_inner, NULL);
}

int main() {
    struct itimerval itv_outer;
    signal(SIGALRM, outer_setitimer_handler);

    itv_outer.it_interval.tv_sec = 5;
    itv_outer.it_interval.tv_usec = 0;
    itv_outer.it_value.tv_sec = 5;
    itv_outer.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL, &itv_outer, NULL);

    while (1) {
        sleep(1);
        printf("Waiting...\n");
    }
    return 0;
}

在这个示例中,外层定时器设置为 5 秒触发一次,当外层定时器触发后,在 outer_setitimer_handler 函数中,我们设置了一个 2 秒触发一次的内层定时器。这里通过 setitimer 函数分别设置了外层和内层定时器,并且在定时器触发时执行相应的信号处理函数。

定时器嵌套使用中的注意事项

信号处理与竞态条件

在定时器嵌套中,由于信号处理函数是异步执行的,可能会与主程序或其他信号处理函数产生竞态条件。例如,在信号处理函数中修改共享变量时,如果主程序也在同时访问该变量,就可能导致数据不一致。为了避免这种情况,可以使用互斥锁(Mutex)或信号量(Semaphore)来保护共享资源。

以下是一个使用互斥锁的简单示例:

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

pthread_mutex_t mutex;
int shared_variable = 0;

void inner_setitimer_handler(int signum) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("Inner setitimer triggered, shared_variable: %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
}

void outer_setitimer_handler(int signum) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("Outer setitimer triggered, shared_variable: %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);

    struct itimerval itv_inner;
    itv_inner.it_interval.tv_sec = 2;
    itv_inner.it_interval.tv_usec = 0;
    itv_inner.it_value.tv_sec = 2;
    itv_inner.it_value.tv_usec = 0;

    signal(SIGALRM, inner_setitimer_handler);
    setitimer(ITIMER_REAL, &itv_inner, NULL);
}

int main() {
    struct itimerval itv_outer;
    signal(SIGALRM, outer_setitimer_handler);

    itv_outer.it_interval.tv_sec = 5;
    itv_outer.it_interval.tv_usec = 0;
    itv_outer.it_value.tv_sec = 5;
    itv_outer.it_value.tv_usec = 0;

    pthread_mutex_init(&mutex, NULL);

    setitimer(ITIMER_REAL, &itv_outer, NULL);

    while (1) {
        sleep(1);
        printf("Waiting...\n");
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这个示例中,我们使用 pthread_mutex_t 类型的互斥锁 mutex 来保护共享变量 shared_variable。在信号处理函数中,首先通过 pthread_mutex_lock 函数获取锁,然后对共享变量进行操作,操作完成后通过 pthread_mutex_unlock 函数释放锁。

定时器资源管理

过多的定时器嵌套可能会消耗大量的系统资源,特别是在长时间运行的程序中。因此,需要合理管理定时器资源。例如,在不需要内层定时器时,及时取消它。可以通过 setitimer 函数将 it_valueit_interval 都设置为 0 来取消定时器。

struct itimerval itv_cancel;
itv_cancel.it_interval.tv_sec = 0;
itv_cancel.it_interval.tv_usec = 0;
itv_cancel.it_value.tv_sec = 0;
itv_cancel.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &itv_cancel, NULL);

这样就可以取消正在运行的定时器,释放相关资源。

定时器嵌套在实际项目中的应用

网络通信中的应用

在网络通信中,定时器嵌套可以用于实现复杂的连接管理和数据传输控制。例如,在一个 TCP 客户端 - 服务器模型中,客户端可以使用外层定时器定期检查与服务器的连接状态。如果发现连接断开,就使用内层定时器控制重连的时间间隔。以下是一个简单的示意代码:

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

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888

int sockfd;

void reconnect_handler(int signum) {
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == 0) {
        printf("Reconnected to server\n");
    } else {
        printf("Reconnection failed\n");
    }
}

void check_connection_handler(int signum) {
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
    char buffer[1024];
    int n = recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT);
    if (n < 0) {
        printf("Connection lost, attempting to reconnect\n");
        struct itimerval itv_reconnect;
        itv_reconnect.it_interval.tv_sec = 5;
        itv_reconnect.it_interval.tv_usec = 0;
        itv_reconnect.it_value.tv_sec = 5;
        itv_reconnect.it_value.tv_usec = 0;

        signal(SIGALRM, reconnect_handler);
        setitimer(ITIMER_REAL, &itv_reconnect, NULL);
    }
}

int main() {
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
        printf("Connection failed\n");
        return 1;
    }
    printf("Connected to server\n");

    struct itimerval itv_check;
    itv_check.it_interval.tv_sec = 10;
    itv_check.it_interval.tv_usec = 0;
    itv_check.it_value.tv_sec = 10;
    itv_check.it_value.tv_usec = 0;

    signal(SIGALRM, check_connection_handler);
    setitimer(ITIMER_REAL, &itv_check, NULL);

    while (1) {
        sleep(1);
        // 其他业务逻辑
    }
    close(sockfd);
    return 0;
}

在这段代码中,外层定时器 itv_check 每隔 10 秒触发一次 check_connection_handler 函数,该函数检查与服务器的连接状态。如果连接丢失,就启动内层定时器 itv_reconnect,每隔 5 秒尝试重连一次。

嵌入式系统中的应用

在嵌入式系统中,定时器嵌套可以用于任务调度和设备控制。例如,在一个智能家居系统中,可能有一个主定时器定期检查各个传感器的数据(如温度、湿度传感器)。当获取到传感器数据后,根据数据情况使用内层定时器控制相应的执行器(如空调、加湿器)的工作时间。以下是一个简化的示例:

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

// 模拟传感器数据获取函数
int get_temperature() {
    // 实际中这里会从传感器读取数据
    return 25;
}

void control_air_conditioner(int signum) {
    int temperature = get_temperature();
    if (temperature > 28) {
        printf("Temperature is high, turning on air conditioner\n");
    } else {
        printf("Temperature is normal, turning off air conditioner\n");
    }
}

void read_sensor_handler(int signum) {
    printf("Reading sensor data\n");
    struct itimerval itv_control;
    itv_control.it_interval.tv_sec = 10;
    itv_control.it_interval.tv_usec = 0;
    itv_control.it_value.tv_sec = 10;
    itv_control.it_value.tv_usec = 0;

    signal(SIGALRM, control_air_conditioner);
    setitimer(ITIMER_REAL, &itv_control, NULL);
}

int main() {
    struct itimerval itv_read;
    itv_read.it_interval.tv_sec = 60;
    itv_read.it_interval.tv_usec = 0;
    itv_read.it_value.tv_sec = 60;
    itv_read.it_value.tv_usec = 0;

    signal(SIGALRM, read_sensor_handler);
    setitimer(ITIMER_REAL, &itv_read, NULL);

    while (1) {
        sleep(1);
        // 其他嵌入式系统业务逻辑
    }
    return 0;
}

在这个示例中,外层定时器 itv_read 每隔 60 秒触发一次 read_sensor_handler 函数,该函数模拟读取传感器数据。然后启动内层定时器 itv_control,每隔 10 秒根据传感器数据控制空调的开关。

通过以上对 Linux C 语言定时器嵌套使用的详细介绍,包括基本概念、实现方法、注意事项以及实际应用案例,希望能帮助开发者更好地理解和运用这一技术,在实际项目中实现更高效、灵活的定时任务调度。