好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 alarm 函数,它是一个简单易用的定时器函数,用于在指定的秒数后向进程发送 SIGALRM 信号。
1. 函数介绍 Link to heading
alarm 是一个 Linux 系统调用,它提供了一种简单的方法来设置一个单次的秒级定时器。当指定的秒数过去后,内核会向调用进程发送一个 SIGALRM 信号。
你可以把它想象成一个简单的“厨房定时器”:
- 你拨动定时器,设定 
N分钟(在这里是秒)。 - 时间一到,定时器就会“叮”一声(发送 
SIGALRM信号)提醒你。 
alarm 非常适合用于需要在未来某个时间点执行某个操作,或者为可能长时间阻塞的系统调用设置一个超时时间。
2. 函数原型 Link to heading
#include <unistd.h> // 必需
unsigned int alarm(unsigned int seconds);
3. 功能 Link to heading
- 设置定时器: 安排内核在 
seconds秒之后向进程发送SIGALRM信号。 - 覆盖旧定时器: 如果进程已经设置了一个未完成的 
alarm定时器,调用alarm会取消(取消调度)之前的定时器,并设置一个新的定时器。 - 取消定时器: 如果 
seconds参数为 0,alarm不会设置新的定时器,但如果存在未完成的定时器,则会取消它。 
4. 参数 Link to heading
unsigned int seconds: 指定从现在开始,经过多少秒后发送SIGALRM信号。- 如果 
seconds为 0:不启动新定时器,并取消任何已存在的定时器。 - 如果 
seconds大于 0:启动一个在seconds秒后到期的定时器。 
- 如果 
 
5. 返回值 Link to heading
- 返回旧的定时器剩余秒数: 
alarm函数返回之前设置的alarm定时器的剩余秒数(如果有的话)。- 如果之前没有设置过 
alarm定时器,或者之前的定时器已经到期,则返回 0。 - 如果之前设置的定时器还有 
N秒到期,则返回N。 
 - 如果之前没有设置过 
 - 错误: 
alarm函数总是成功,不会因为参数错误而返回 -1。因此,它没有失败的返回路径。 
6. 相似函数,或关联函数 Link to heading
setitimer/getitimer: 更强大、更灵活的定时器函数。alarm(seconds)在功能上大致等价于setitimer(ITIMER_REAL, ...),其中itimerval.it_value.tv_sec = seconds且itimerval.it_interval.tv_sec = 0。setitimer支持微秒精度、一次性或周期性定时器,并且可以选择不同的时钟类型(ITIMER_REAL,ITIMER_VIRTUAL,ITIMER_PROF)。
signal/sigaction: 用于设置当收到SIGALRM信号时应执行的操作。pause: 使进程进入睡眠状态,直到收到信号(如SIGALRM)。sleep: 使进程主动睡眠指定的秒数。与alarm不同,sleep是主动挂起,而alarm是设置一个未来事件。nanosleep: 提供更高精度(纳秒级)的主动睡眠。timer_create/timer_settime: POSIX 定时器 API,功能最强大,支持多种时钟源、多种通知方式(信号、线程),是现代推荐的定时器方案。
7. 示例代码 Link to heading
  示例 1:基本的 alarm 使用和信号处理
  
    
    Link to heading
  
这个例子演示了如何设置一个 alarm 定时器,并在定时器到期时通过信号处理函数执行操作。
#include <unistd.h>   // alarm
#include <signal.h>   // signal, SIGALRM
#include <stdio.h>    // printf, perror
#include <stdlib.h>   // exit
#include <errno.h>    // errno (虽然 alarm 不会失败,但信号处理可能涉及)
#include <string.h>   // strerror
volatile sig_atomic_t alarm_flag = 0;
// SIGALRM 信号处理函数
void alarm_handler(int sig) {
    // 在信号处理函数中,应只调用异步信号安全的函数
    // printf 通常被认为是安全的,但更安全的做法是只修改标志
    write(STDERR_FILENO, "Alarm! SIGALRM signal received.\n", 32);
    alarm_flag = 1; // 设置全局标志
}
int main() {
    unsigned int remaining;
    printf("Process PID: %d\n", getpid());
    // 1. 设置 SIGALRM 信号处理函数
    if (signal(SIGALRM, alarm_handler) == SIG_ERR) {
        perror("signal SIGALRM");
        exit(EXIT_FAILURE);
    }
    // 2. 设置一个 3 秒的 alarm 定时器
    printf("Setting alarm for 3 seconds.\n");
    remaining = alarm(3);
    // 3. 检查是否有旧的定时器 (应该没有)
    if (remaining > 0) {
        printf("There was an old alarm set to expire in %u seconds. It's now canceled.\n", remaining);
    } else {
        printf("No previous alarm was set.\n");
    }
    // 4. 等待信号
    printf("Waiting for the alarm to go off...\n");
    // 这里使用 pause() 等待信号
    // 在实际应用中,这里可能是 read(), write(), connect() 等阻塞调用
    while (!alarm_flag) {
        pause(); // 挂起进程,直到收到信号
    }
    printf("Main function continuing after alarm.\n");
    // 5. 再设置一个 2 秒的 alarm
    printf("Setting another alarm for 2 seconds.\n");
    remaining = alarm(2);
    if (remaining > 0) {
        printf("Canceled a previous alarm that had %u seconds left.\n", remaining);
    }
    // 6. 等待第二个 alarm
    alarm_flag = 0; // 重置标志
    printf("Waiting for the second alarm...\n");
    while (!alarm_flag) {
        pause();
    }
    printf("Second alarm received. Program exiting.\n");
    return 0;
}
代码解释:
- 定义一个 
volatile sig_atomic_t类型的全局变量alarm_flag,用于在信号处理函数和主程序之间通信。 - 定义 
SIGALRM信号的处理函数alarm_handler。当定时器到期,内核发送SIGALRM信号时,该函数会被调用。它打印一条消息并设置alarm_flag。 - 在 
main函数中,使用signal()注册SIGALRM处理函数。 - 调用 
alarm(3)设置一个 3 秒后到期的定时器。返回值remaining应该是 0,因为这是第一次设置。 - 使用一个 
while循环和pause()等待信号。pause()会使进程挂起,直到收到任何信号。当SIGALRM到达时,alarm_handler被调用,设置alarm_flag,主循环退出。 - 再次调用 
alarm(2)设置一个新的 2 秒定时器。因为前一个定时器已经被处理,所以这次返回的remaining也应该是 0。 - 重置 
alarm_flag并再次等待信号。 
  示例 2:使用 alarm 为阻塞操作设置超时
  
    
    Link to heading
  
这个例子演示了如何使用 alarm 来防止 read 系统调用无限期地阻塞。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h> // 用于非局部跳转 setjmp/longjmp
// jmp_buf 是一个特殊的数据结构,用于保存程序执行状态
static jmp_buf jmp_env;
volatile sig_atomic_t timed_out = 0;
void timeout_handler(int sig) {
    // 当 read 超时,SIGALRM 会触发此处理函数
    write(STDERR_FILENO, "Read operation timed out!\n", 26);
    timed_out = 1;
    // 使用 longjmp 跳转回调用 setjmp 的地方
    longjmp(jmp_env, 1);
}
int main() {
    char buffer[100];
    ssize_t bytes_read;
    printf("Setting timeout handler for SIGALRM.\n");
    if (signal(SIGALRM, timeout_handler) == SIG_ERR) {
        perror("signal SIGALRM");
        exit(EXIT_FAILURE);
    }
    printf("Setting alarm for 5 seconds. Please type something and press Enter:\n");
    // setjmp 保存当前程序执行状态到 jmp_env
    // 首次调用 setjmp 返回 0
    // 当 longjmp 被调用并跳转回来时,setjmp 返回 longjmp 传入的值 (这里是 1)
    if (setjmp(jmp_env) == 0) {
        // 1. 设置 5 秒超时
        alarm(5);
        // 2. 执行可能阻塞的操作
        bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
        // 3. 如果 read 返回,说明没有超时 (或者在超时前完成了)
        // 取消 alarm (传入 0)
        alarm(0);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("You typed: %s", buffer); // buffer 可能已包含 \n
        } else if (bytes_read == 0) {
            printf("EOF reached on input.\n");
        } else {
            perror("read");
        }
    } else {
        // 4. 从 longjmp 跳转到这里 (超时发生)
        printf("Jumped back from timeout handler.\n");
        // 注意:read 可能已被中断,errno 为 EINTR
        // 在这个简单的例子中,我们不重试 read
    }
    printf("Program finished.\n");
    return 0;
}
代码解释:
- 定义 
jmp_buf jmp_env用于保存程序状态,volatile sig_atomic_t timed_out用于标志。 - 定义 
timeout_handler信号处理函数。当SIGALRM到达时:- 打印超时消息。
 - 设置 
timed_out标志。 - 调用 
longjmp(jmp_env, 1)。这会使程序执行流立即跳转回到之前调用setjmp(jmp_env)的地方,并且setjmp返回 1。 
 - 在 
main函数中:- 设置 
SIGALRM处理函数。 - 调用 
setjmp(jmp_env)。如果是第一次调用(直接调用),它返回 0。 - 在 
if (setjmp(...) == 0)为真的代码块中:- 设置 5 秒 
alarm。 - 调用 
read(STDIN_FILENO, ...)等待用户输入。这会阻塞。 - 如果用户在 5 秒内输入并按回车,
read返回,程序继续执行。 - 程序会调用 
alarm(0)来取消定时器(因为它已经不再需要)。 - 处理 
read的返回值。 
 - 设置 5 秒 
 - 如果 
setjmp返回非 0(在这里是 1,由longjmp导致):- 程序直接跳转到 
else分支,处理超时情况。 
 - 程序直接跳转到 
 
 - 设置 
 - 效果:
- 如果用户在 5 秒内输入,
read成功返回,定时器被取消。 - 如果用户在 5 秒内没有输入,
alarm到期,SIGALRM被发送,timeout_handler被调用,longjmp跳转,主程序在else分支处理超时。 
 - 如果用户在 5 秒内输入,
 
注意: 使用 setjmp/longjmp 从信号处理函数中跳转是一种高级且需要小心使用的技巧。它会跳过正常的函数返回链,可能导致资源(如已分配的内存、打开的文件)未被正确清理。在现代 C 编程中,更推荐使用返回值检查和循环重试的方式,或者使用更现代的 sigsetjmp/siglongjmp。
  示例 3:alarm 的覆盖和返回值
  
    
    Link to heading
  
这个例子演示了 alarm 如何覆盖旧的定时器,以及其返回值的含义。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
    unsigned int ret1, ret2, ret3;
    printf("Process PID: %d\n", getpid());
    printf("No alarm set initially.\n");
    // 1. 设置一个 10 秒的 alarm
    printf("Setting alarm for 10 seconds...\n");
    ret1 = alarm(10);
    printf("alarm(10) returned: %u (should be 0, no previous alarm)\n", ret1);
    // 等待一小会儿,让时间流逝
    printf("Sleeping for 3 seconds...\n");
    sleep(3);
    // 2. 设置一个 5 秒的新 alarm,覆盖旧的
    printf("Setting alarm for 5 seconds (should override the previous one)...\n");
    ret2 = alarm(5);
    // 旧 alarm 已运行了 3 秒,还剩 7 秒,所以返回 7
    printf("alarm(5) returned: %u (should be ~7, remaining time of previous alarm)\n", ret2);
    // 再等一小会儿
    printf("Sleeping for 2 more seconds...\n");
    sleep(2);
    // 3. 取消 alarm (设置为 0)
    printf("Canceling alarm (setting it to 0)...\n");
    ret3 = alarm(0);
    // 旧 alarm 已运行了 5 秒,还剩 3 秒,所以返回 3
    printf("alarm(0) returned: %u (should be ~3, remaining time of previous alarm)\n", ret3);
    printf("Alarm canceled. Waiting for 10 seconds to prove no signal is sent.\n");
    sleep(10);
    printf("No signal received. Program finished normally.\n");
    return 0;
}
代码解释:
- 调用 
alarm(10)设置一个 10 秒定时器。因为之前没有定时器,所以返回 0。 - 等待 3 秒。此时原定时器还剩 7 秒。
 - 调用 
alarm(5)设置一个 5 秒定时器。这会取消之前的 10 秒定时器。alarm返回被取消的定时器的剩余时间,大约是 7 秒。 - 再等待 2 秒。此时新定时器还剩 3 秒。
 - 调用 
alarm(0)来取消当前的定时器。alarm返回被取消的定时器的剩余时间,大约是 3 秒。 - 程序继续休眠 10 秒。因为定时器已被取消,所以不会收到 
SIGALRM信号。 
  示例 4:忽略 SIGALRM 信号
  
    
    Link to heading
  
这个例子演示了如果 SIGALRM 信号被忽略会发生什么。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
int main() {
    pid_t pid;
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid == 0) {
        // --- 子进程 ---
        printf("Child process (PID %d): Ignoring SIGALRM and setting alarm(3).\n", getpid());
        // 忽略 SIGALRM 信号
        if (signal(SIGALRM, SIG_IGN) == SIG_ERR) {
            perror("signal SIG_IGN");
            _exit(EXIT_FAILURE);
        }
        // 设置 3 秒 alarm
        alarm(3);
        printf("Child: Alarm set. Now sleeping for 5 seconds. Alarm should have no effect.\n");
        // 睡眠 5 秒,比 alarm 时间长
        unsigned int slept = sleep(5);
        printf("Child: sleep(5) returned. It claims %u seconds were left unslept.\n", slept);
        if (slept == 0) {
            printf("Child: sleep completed fully. No signal interrupted it.\n");
        } else {
            printf("Child: sleep was interrupted (unexpected in this setup).\n");
        }
        printf("Child process exiting.\n");
        _exit(EXIT_SUCCESS);
    } else {
        // --- 父进程 ---
        printf("Parent process (PID %d): Waiting for child (PID %d) to finish.\n", getpid(), pid);
        int status;
        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid");
            exit(EXIT_FAILURE);
        }
        if (WIFEXITED(status)) {
            printf("Parent: Child exited normally with status %d.\n", WEXITSTATUS(status));
        } else {
            printf("Parent: Child did not exit normally.\n");
        }
    }
    return 0;
}
代码解释:
- 使用 
fork()创建子进程。 - 子进程:
- 使用 
signal(SIGALRM, SIG_IGN)忽略SIGALRM信号。 - 调用 
alarm(3)设置一个 3 秒定时器。 - 调用 
sleep(5)睡眠 5 秒。 
 - 使用 
 - 父进程: 等待子进程结束。
 - 结果: 子进程会完整地睡眠 5 秒。虽然 3 秒后 
alarm定时器到期并向子进程发送了SIGALRM信号,但由于该信号被设置为SIG_IGN(忽略),信号被内核丢弃,sleep不会因此被中断。sleep返回 0,表示它完整地睡了 5 秒。 
重要提示与注意事项:
- 精度: 
alarm的精度是秒级。如果需要更高精度(微秒或纳秒),应使用setitimer或timer_create等函数。 - 单次性: 
alarm只能设置一次性定时器。如果需要周期性定时,必须在SIGALRM处理函数中再次调用alarm。 - 信号安全: 在 
SIGALRM信号处理函数中,应只调用异步信号安全的函数。 - 竞态条件: 在设置信号处理函数和调用 
alarm之间,或者在检查标志和调用pause/sleep之间,可能存在竞态条件。使用sigsuspend可以更安全地处理。 sleep与alarm的交互:sleep函数的实现可能会使用SIGALRM信号。如果程序同时使用alarm和sleep,它们可能会相互干扰。在使用alarm设置超时的程序中,应避免使用sleep,或者确保理解它们的交互方式。- 现代替代: 
alarm功能简单,易于使用,适用于许多基本场景。但对于更复杂的需求(高精度、周期性、多种时钟源),setitimer和timer_系列的 POSIX 定时器是更强大和现代的选择。 
总结:
alarm 是一个简单而有效的函数,用于设置秒级的单次定时器。它通过发送 SIGALRM 信号来通知定时器到期。理解其返回值(旧定时器的剩余时间)和与信号处理的结合使用是掌握它的关键。虽然它功能有限,但在实现简单的超时机制或定时提醒时非常有用。