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

Linux C语言定时器的信号处理关联

2022-01-162.2k 阅读

Linux C 语言定时器概述

在 Linux 环境下,定时器是一种重要的机制,它允许程序在特定的时间间隔执行某些操作。C 语言作为 Linux 系统下广泛使用的编程语言,提供了丰富的函数和接口来实现定时器功能。定时器在很多场景中都有应用,比如周期性的数据采集、系统监控以及网络协议中的超时处理等。

定时器相关的系统调用

在 Linux 中,主要通过 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 结构体的指针,用于设置定时器的初始值和周期值。itimerval 结构体定义如下:
struct itimerval {
    struct timeval it_interval;  /* 下一次定时器到期的时间间隔 */
    struct timeval it_value;     /* 定时器的初始值 */
};
struct timeval {
    time_t      tv_sec;         /* 秒数 */
    suseconds_t tv_usec;        /* 微秒数 */
};
  • old_value 也是一个指向 itimerval 结构体的指针,如果不为 NULL,则会将定时器原来的设置值保存到该指针指向的结构体中。

信号处理基础

在 Linux 系统中,信号是一种异步通知机制,用于在进程之间或者内核与进程之间传递事件信息。当一个信号产生时,内核会暂停当前进程的执行,并根据进程对该信号的处理方式来决定下一步操作。

信号的种类

Linux 系统定义了多种信号,常见的与定时器相关的信号如前面提到的 SIGALRMSIGVTALRMSIGPROF。每个信号都有一个唯一的编号,并且有对应的默认处理动作,比如 SIGTERM 信号的默认动作是终止进程,而 SIGALRM 的默认动作也是终止进程。

信号处理函数

进程可以通过 signal 函数或者 sigaction 函数来设置对某个信号的处理方式。signal 函数的原型为:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum 是要处理的信号编号。
  • handler 可以是一个函数指针,指向自定义的信号处理函数,也可以是 SIG_IGN(忽略该信号) 或者 SIG_DFL(使用默认处理方式)。

sigaction 函数提供了更灵活和强大的信号处理设置功能,其原型为:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • signum 同样是信号编号。
  • act 是一个指向 sigaction 结构体的指针,用于设置新的信号处理方式。
  • oldact 如果不为 NULL,则会保存原来的信号处理设置。

sa_handler 用于指定信号处理函数,sa_mask 用于设置在信号处理函数执行期间需要屏蔽的其他信号,sa_flags 可以设置一些附加的标志,比如 SA_SIGINFO 标志可以使信号处理函数接收更多的信息,此时需要使用 sa_sigaction 来指定信号处理函数。

Linux C 语言定时器与信号处理的关联

基于 SIGALRM 信号的定时器示例

下面通过一个简单的示例来展示如何使用 setitimer 设置定时器,并通过 signal 函数处理 SIGALRM 信号。

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

// 信号处理函数
void alarm_handler(int signum) {
    printf("SIGALRM signal received. Timer expired.\n");
}

int main() {
    struct itimerval new_value;
    // 设置定时器的初始值为 2 秒,周期值为 1 秒
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;
    new_value.it_interval.tv_sec = 1;
    new_value.it_interval.tv_usec = 0;

    // 设置信号处理函数
    signal(SIGALRM, alarm_handler);

    // 设置定时器
    if (setitimer(ITIMER_REAL, &new_value, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    printf("Timer set. Waiting for signals...\n");

    // 程序进入循环,等待信号
    while (1) {
        // 这里可以执行其他任务,定时器到期会中断这个循环执行信号处理函数
    }

    return 0;
}

在上述代码中:

  1. 首先定义了一个 alarm_handler 函数作为 SIGALRM 信号的处理函数,在该函数中简单地打印一条消息。
  2. main 函数中,初始化了一个 itimerval 结构体 new_value,设置定时器的初始值为 2 秒,周期值为 1 秒。
  3. 使用 signal 函数将 SIGALRM 信号与 alarm_handler 函数关联起来。
  4. 调用 setitimer 函数设置定时器,类型为 ITIMER_REAL
  5. 程序进入一个无限循环,在循环中可以执行其他任务,当定时器到期时,会触发 SIGALRM 信号,从而执行 alarm_handler 函数。

使用 sigaction 进行更复杂的定时器信号处理

sigaction 函数相比 signal 函数提供了更多的功能,比如可以在信号处理函数执行期间屏蔽其他信号,以及获取更多的信号相关信息。以下是一个使用 sigaction 处理 SIGALRM 信号的示例:

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

// 信号处理函数
void alarm_handler(int signum, siginfo_t *info, void *context) {
    printf("SIGALRM signal received. Timer expired. \n");
    printf("Signal value: %d\n", info->si_value.sival_int);
}

int main() {
    struct itimerval new_value;
    struct sigaction sa;

    // 设置定时器的初始值为 2 秒,周期值为 1 秒
    new_value.it_value.tv_sec = 2;
    new_value.it_value.tv_usec = 0;
    new_value.it_interval.tv_sec = 1;
    new_value.it_interval.tv_usec = 0;

    // 初始化 sigaction 结构体
    sa.sa_sigaction = alarm_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    // 添加要屏蔽的信号,这里以 SIGINT 为例
    sigaddset(&sa.sa_mask, SIGINT);

    // 设置信号处理函数
    if (sigaction(SIGALRM, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // 设置定时器
    if (setitimer(ITIMER_REAL, &new_value, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    printf("Timer set. Waiting for signals...\n");

    // 程序进入循环,等待信号
    while (1) {
        // 这里可以执行其他任务,定时器到期会中断这个循环执行信号处理函数
    }

    return 0;
}

在这个示例中:

  1. 定义了一个更复杂的 alarm_handler 函数,该函数接收三个参数,其中 siginfo_t *info 可以获取更多关于信号的信息,比如在这个例子中打印出 si_value.sival_int 的值。
  2. 初始化 sigaction 结构体 sa,设置 sa_sigaction 为自定义的信号处理函数 alarm_handler,并设置 sa_flagsSA_SIGINFO 以启用 siginfo_t 结构体传递信息。
  3. 使用 sigemptyset 清空 sa_mask,然后使用 sigaddset 添加要在信号处理函数执行期间屏蔽的信号,这里添加了 SIGINT 信号,意味着在处理 SIGALRM 信号时,如果 SIGINT 信号产生,会被暂时屏蔽,直到 SIGALRM 信号处理函数执行完毕。
  4. 通过 sigaction 函数设置 SIGALRM 信号的处理方式。
  5. 同样设置定时器并进入循环等待信号。

定时器与信号处理在实际项目中的应用场景

  1. 网络通信中的心跳检测:在网络编程中,为了保持客户端和服务器之间的连接状态,通常会使用心跳机制。服务器可以设置一个定时器,每隔一段时间(比如 10 秒)向客户端发送一个心跳包,并等待客户端的响应。如果在一定时间内没有收到客户端的响应(即定时器超时),则认为客户端可能已经断开连接,从而采取相应的处理措施,比如关闭连接或者重新发起连接请求。在这个过程中,定时器超时会触发相应的信号,信号处理函数可以处理连接关闭或重连等操作。
// 简化的网络心跳检测示例(仅展示定时器和信号处理部分)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>

// 假设这里有一个全局变量表示连接状态
int connection_status = 1;

// 信号处理函数
void alarm_handler(int signum) {
    if (connection_status) {
        printf("Heartbeat timeout. Checking connection...\n");
        // 这里可以添加实际的连接检测和处理逻辑
        connection_status = 0; // 假设连接异常,设置连接状态为断开
    }
}

int main() {
    struct itimerval new_value;

    // 设置定时器的初始值为 10 秒,周期值为 10 秒
    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, alarm_handler);

    // 设置定时器
    if (setitimer(ITIMER_REAL, &new_value, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    printf("Heartbeat timer set. Running...\n");

    // 模拟网络通信主循环
    while (1) {
        if (connection_status) {
            // 模拟正常通信操作
            printf("Normal communication...\n");
        } else {
            // 处理连接断开后的重连等操作
            printf("Connection lost. Trying to reconnect...\n");
            // 这里添加重连逻辑
            connection_status = 1; // 假设重连成功
        }
        sleep(1);
    }

    return 0;
}
  1. 系统资源监控:可以使用定时器周期性地采集系统资源信息,如 CPU 使用率、内存使用率等。定时器超时触发信号,信号处理函数负责采集并记录这些资源信息,或者在资源使用率超过一定阈值时进行报警。
// 简单的系统资源监控示例(仅展示定时器和信号处理部分)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/resource.h>

// 信号处理函数
void alarm_handler(int signum) {
    struct rusage usage;
    if (getrusage(RUSAGE_SELF, &usage) == 0) {
        printf("CPU time used: %ld.%06ld seconds\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);
        // 这里还可以添加获取内存使用率等其他资源信息的代码
    } else {
        perror("getrusage");
    }
}

int main() {
    struct itimerval new_value;

    // 设置定时器的初始值为 5 秒,周期值为 5 秒
    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, alarm_handler);

    // 设置定时器
    if (setitimer(ITIMER_REAL, &new_value, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    printf("Resource monitoring timer set. Running...\n");

    // 主循环
    while (1) {
        // 这里可以执行其他任务
        sleep(1);
    }

    return 0;
}
  1. 任务调度:在一些复杂的应用程序中,可能需要按照一定的时间间隔执行不同的任务。可以通过设置多个定时器,每个定时器对应一个特定的任务,并通过信号处理函数来执行相应的任务。例如,一个监控系统可能需要每隔一段时间进行数据采集,每隔更长时间进行数据分析和报告生成。
// 简单的任务调度示例(仅展示定时器和信号处理部分)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>

// 数据采集任务
void data_collection(int signum) {
    printf("Data collection task is running.\n");
    // 这里添加实际的数据采集代码
}

// 数据分析和报告生成任务
void data_analysis(int signum) {
    printf("Data analysis and report generation task is running.\n");
    // 这里添加实际的数据分析和报告生成代码
}

int main() {
    struct itimerval collection_timer, analysis_timer;

    // 设置数据采集定时器的初始值为 10 秒,周期值为 10 秒
    collection_timer.it_value.tv_sec = 10;
    collection_timer.it_value.tv_usec = 0;
    collection_timer.it_interval.tv_sec = 10;
    collection_timer.it_interval.tv_usec = 0;

    // 设置数据分析定时器的初始值为 60 秒,周期值为 60 秒
    analysis_timer.it_value.tv_sec = 60;
    analysis_timer.it_value.tv_usec = 0;
    analysis_timer.it_interval.tv_sec = 60;
    analysis_timer.it_interval.tv_usec = 0;

    // 设置数据采集任务的信号处理函数
    signal(SIGALRM, data_collection);
    // 设置数据分析任务的信号处理函数
    signal(SIGUSR1, data_analysis);

    // 设置数据采集定时器
    if (setitimer(ITIMER_REAL, &collection_timer, NULL) == -1) {
        perror("setitimer for data collection");
        exit(EXIT_FAILURE);
    }

    // 设置数据分析定时器
    struct itimerval *old_value;
    if (setitimer(ITIMER_REAL, &analysis_timer, old_value) == -1) {
        perror("setitimer for data analysis");
        exit(EXIT_FAILURE);
    }

    printf("Task scheduling timers set. Running...\n");

    // 主循环
    while (1) {
        // 这里可以执行其他任务
        sleep(1);
    }

    return 0;
}

在这个示例中,使用了两个定时器,一个用于数据采集(周期为 10 秒),另一个用于数据分析和报告生成(周期为 60 秒)。分别设置了不同的信号(SIGALRMSIGUSR1)及其对应的信号处理函数来执行相应的任务。

定时器与信号处理的注意事项

  1. 信号处理函数的可重入性:信号处理函数可能在程序执行的任何时刻被调用,因此必须保证其可重入性。可重入函数是指可以被中断,并且在中断后重新进入时不会出现数据错误的函数。例如,在信号处理函数中避免使用静态变量,除非对其访问进行了适当的同步处理。像 printf 函数在某些情况下可能不是可重入的,虽然在简单示例中可以使用,但在实际复杂的多线程或信号频繁触发的场景下可能会出现问题,应尽量使用可重入的函数,如 write 函数来进行输出。
  2. 信号屏蔽与恢复:在使用 sigaction 设置信号处理函数时,合理设置 sa_mask 来屏蔽在信号处理期间不希望被处理的信号是很重要的。但要注意在信号处理函数结束后,信号屏蔽状态会恢复到信号处理前的状态。如果需要在信号处理函数中临时改变信号屏蔽状态,要确保正确地恢复。
  3. 定时器精度:虽然 setitimer 提供了微秒级别的计时精度,但实际的定时器精度可能会受到系统负载、硬件等因素的影响。在对时间精度要求极高的场景下,可能需要考虑其他更精确的计时方式,比如利用高精度定时器(如 clock_gettime 函数结合 CLOCK_MONOTONIC 时钟)来实现更精确的定时需求。
  4. 信号竞争问题:当多个信号同时产生或者在信号处理函数执行期间又有新的信号产生时,可能会出现信号竞争问题。可以通过合理设置信号处理方式,如使用 SA_RESTART 标志(在 sigactionsa_flags 中设置)来使某些系统调用在信号中断后自动重新启动,避免因信号中断导致系统调用失败。

通过深入理解 Linux C 语言定时器与信号处理的关联,并注意上述的各种事项,开发者可以在 Linux 环境下编写出高效、稳定且功能丰富的程序,满足各种定时任务和异步事件处理的需求。无论是网络编程、系统监控还是任务调度等领域,定时器与信号处理的结合都发挥着重要的作用。在实际项目开发中,需要根据具体的需求和场景,灵活运用这些知识,优化程序的性能和稳定性。