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

Linux C语言定时器的定时任务取消

2021-10-033.4k 阅读

1. 定时器在 Linux C 语言开发中的重要性

在 Linux 环境下的 C 语言编程中,定时器是一种非常有用的工具。它允许程序在特定的时间间隔内执行某些任务,或者在未来的某个特定时刻执行任务。定时器在许多场景中都有应用,比如网络通信中的心跳检测、游戏开发中的定时更新、系统监控中的定期数据采集等。

例如,在一个网络服务器程序中,我们可能需要每隔一段时间向客户端发送心跳包,以确保连接仍然有效。如果没有定时器,我们就需要手动实现轮询机制,这不仅会增加代码的复杂度,还可能导致资源的浪费。

2. Linux 下常用的定时器机制

2.1 alarm 函数

alarm 函数是 Linux 提供的一个简单的定时器函数,它以秒为单位设置一个定时器。当定时器到期时,会向进程发送 SIGALRM 信号。

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

void sigalrm_handler(int signum) {
    printf("Alarm signal received.\n");
}

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

在上述代码中,alarm(5) 设置了一个 5 秒的定时器,当 5 秒过去后,会调用 sigalrm_handler 函数。然而,alarm 函数的精度相对较低,并且只能设置一个定时器。如果我们需要更精确的定时或者多个定时器,就需要使用其他机制。

2.2 setitimer 函数

setitimer 函数提供了更灵活和精确的定时器设置。它可以以微秒为单位设置定时器,并且支持一次性定时器和周期性定时器。

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

void sigalrm_handler(int signum) {
    printf("Alarm signal received.\n");
}

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

    signal(SIGALRM, sigalrm_handler);
    setitimer(ITIMER_REAL, &new_value, NULL);

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

在这段代码中,new_value.it_value 设置了定时器的初始值,new_value.it_interval 设置了定时器的周期。setitimer(ITIMER_REAL, &new_value, NULL) 启动了定时器,ITIMER_REAL 表示使用系统实时时钟。当定时器到期时,同样会发送 SIGALRM 信号。

2.3 POSIX 定时器(timer_create 等函数)

POSIX 定时器提供了更高级的定时器功能,它允许创建多个定时器,并且可以指定定时器到期时执行的处理函数。

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

void timer_handler(int signum, siginfo_t *info, void *context) {
    printf("Timer expired.\n");
}

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

    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 = 3;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 3;
    its.it_interval.tv_nsec = 0;

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

    struct sigaction sa;
    sa.sa_sigaction = timer_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for timer to expire...\n");
    while (1);

    return 0;
}

在这段代码中,timer_create 创建了一个定时器,timer_settime 设置了定时器的初始值和周期。SIGEV_SIGNAL 表示定时器到期时发送信号,SIGUSR1 是自定义的信号。sigaction 函数用于注册信号处理函数 timer_handler

3. 定时任务取消的需求场景

在实际应用中,我们经常会遇到需要取消定时任务的情况。例如:

  • 资源释放:在一个定时进行数据采集的程序中,如果程序需要提前结束,我们需要取消定时器,以避免不必要的资源消耗,比如文件描述符的占用、内存的持续分配等。
  • 任务变更:在一个游戏开发场景中,原本定时更新游戏场景的任务可能因为游戏状态的改变(如进入暂停状态)而需要取消,等待游戏恢复时再重新启动定时任务。
  • 错误处理:当程序在定时任务执行过程中检测到严重错误时,为了保证系统的稳定性,可能需要立即取消定时任务,防止错误进一步扩散。

4. 基于 alarm 函数的定时任务取消

alarm 函数本身提供了一种取消定时任务的方式。如果在定时器到期之前再次调用 alarm 函数,并且传入参数为 0,就可以取消当前正在运行的定时器。

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

void sigalrm_handler(int signum) {
    printf("Alarm signal received.\n");
}

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

在上述代码中,alarm(5) 设置了一个 5 秒的定时器,sleep(2) 模拟了程序的其他操作。2 秒后,调用 alarm(0) 取消了定时器。这样,定时器就不会在剩余的 3 秒后触发 SIGALRM 信号。

5. 基于 setitimer 函数的定时任务取消

对于 setitimer 函数,我们可以通过将 it_valueit_interval 都设置为 0 来取消定时器。

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

void sigalrm_handler(int signum) {
    printf("Alarm signal received.\n");
}

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

    signal(SIGALRM, sigalrm_handler);
    setitimer(ITIMER_REAL, &new_value, NULL);

    printf("Waiting for alarm...\n");
    sleep(2);

    struct itimerval zero_value;
    zero_value.it_value.tv_sec = 0;
    zero_value.it_value.tv_usec = 0;
    zero_value.it_interval.tv_sec = 0;
    zero_value.it_interval.tv_usec = 0;

    setitimer(ITIMER_REAL, &zero_value, NULL);
    printf("Alarm cancelled.\n");
    while (1);
    return 0;
}

在这段代码中,首先设置了一个 5 秒的周期性定时器。sleep(2) 后,通过设置 zero_value 并调用 setitimer 函数,将定时器的剩余时间和周期都设置为 0,从而取消了定时器。

6. 基于 POSIX 定时器的定时任务取消

POSIX 定时器的取消通过 timer_delete 函数来实现。

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

void timer_handler(int signum, siginfo_t *info, void *context) {
    printf("Timer expired.\n");
}

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

    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 = 3;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 3;
    its.it_interval.tv_nsec = 0;

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

    struct sigaction sa;
    sa.sa_sigaction = timer_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for timer to expire...\n");
    sleep(1);

    if (timer_delete(timerid) == -1) {
        perror("timer_delete");
        exit(EXIT_FAILURE);
    }
    printf("Timer cancelled.\n");
    while (1);

    return 0;
}

在上述代码中,timer_create 创建了一个定时器,timer_settime 设置了定时器的属性。sleep(1) 模拟了程序的运行。1 秒后,调用 timer_delete 函数取消了定时器。

7. 定时任务取消时的注意事项

  • 信号处理:在取消定时器时,要注意信号的处理。如果定时器是通过发送信号来通知任务到期,取消定时器后,可能需要处理已经发送但还未处理的信号。例如,在基于 alarmsetitimer 函数的定时器中,取消定时器后,如果 SIGALRM 信号已经发送到进程队列中,需要确保在适当的时候处理该信号,避免程序出现异常行为。
  • 资源清理:定时任务可能会占用一些资源,如文件描述符、网络连接等。在取消定时任务时,必须确保这些资源被正确释放。比如,在一个定时从文件中读取数据的任务中,取消任务后要关闭文件描述符,防止资源泄漏。
  • 线程安全:在多线程环境下,取消定时任务需要特别注意线程安全。如果多个线程都可能操作定时器,需要使用互斥锁等同步机制来确保定时器的创建、设置和取消操作的原子性,避免出现竞态条件。例如,在一个多线程的服务器程序中,不同线程可能根据不同的条件取消同一个定时器,这时就需要使用互斥锁来保护对定时器的操作。

8. 实际应用案例分析

假设我们正在开发一个智能家居监控系统,其中有一个功能是定时采集传感器数据并上传到服务器。

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

#define SENSOR_DATA_SIZE 100

void upload_sensor_data() {
    char data[SENSOR_DATA_SIZE];
    // 模拟采集传感器数据
    snprintf(data, SENSOR_DATA_SIZE, "Sensor data at %ld", (long)time(NULL));
    printf("Uploading data: %s\n", data);
    // 实际应用中这里会有网络上传代码
}

void sigalrm_handler(int signum) {
    upload_sensor_data();
}

int main() {
    struct itimerval new_value;
    new_value.it_value.tv_sec = 10;
    new_value.it_value.tv_usec = 0;
    new_value.it_interval.tv_sec = 10;
    new_value.it_interval.tv_usec = 0;

    signal(SIGALRM, sigalrm_handler);
    setitimer(ITIMER_REAL, &new_value, NULL);

    printf("Data collection started. Press any key to stop...\n");
    char ch;
    scanf("%c", &ch);

    struct itimerval zero_value;
    zero_value.it_value.tv_sec = 0;
    zero_value.it_value.tv_usec = 0;
    zero_value.it_interval.tv_sec = 0;
    zero_value.it_interval.tv_usec = 0;

    setitimer(ITIMER_REAL, &zero_value, NULL);
    printf("Data collection stopped.\n");
    return 0;
}

在这个例子中,setitimer 设置了一个每 10 秒执行一次的定时任务,upload_sensor_data 函数模拟了传感器数据的采集和上传。当用户按下任意键时,通过将 it_valueit_interval 设置为 0 取消了定时器,从而停止了数据采集任务。在实际应用中,我们还需要处理网络上传失败等异常情况,并且可能需要使用更复杂的同步机制来确保数据的准确性和一致性。

9. 总结定时任务取消的不同方法及适用场景

  • alarm 函数:取消定时任务简单直接,通过调用 alarm(0) 即可。适用于对精度要求不高,且只需要一个简单定时器的场景,例如一些简单的脚本程序或者对资源占用要求较低的小型应用。
  • setitimer 函数:通过设置 it_valueit_interval 为 0 来取消定时器。它适用于对定时精度有一定要求,并且可能需要周期性定时任务的场景,如一些系统监控工具,它们需要定期采集系统信息。
  • POSIX 定时器:使用 timer_delete 函数取消定时器。适用于对定时器功能要求较为复杂,如需要多个定时器、更精确的定时控制以及自定义信号处理的场景,例如在大型服务器程序中,需要管理多个不同定时需求的任务。

在实际的 Linux C 语言开发中,根据具体的应用场景和需求,合理选择定时器机制以及定时任务取消的方法,能够提高程序的效率和稳定性,避免资源浪费和程序异常行为。同时,在处理定时任务取消时,要充分考虑信号处理、资源清理和线程安全等问题,确保程序的健壮性。