好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 alarm 函数,它是一个简单易用的定时器函数,用于在指定的秒数后向进程发送 SIGALRM 信号。


1. 函数介绍 链接到标题

alarm 是一个 Linux 系统调用,它提供了一种简单的方法来设置一个单次秒级定时器。当指定的秒数过去后,内核会向调用进程发送一个 SIGALRM 信号。

你可以把它想象成一个简单的“厨房定时器”:

  1. 你拨动定时器,设定 N 分钟(在这里是秒)。
  2. 时间一到,定时器就会“叮”一声(发送 SIGALRM 信号)提醒你。

alarm 非常适合用于需要在未来某个时间点执行某个操作,或者为可能长时间阻塞的系统调用设置一个超时时间


2. 函数原型 链接到标题

#include <unistd.h> // 必需

unsigned int alarm(unsigned int seconds);

3. 功能 链接到标题

  • 设置定时器: 安排内核在 seconds 秒之后向进程发送 SIGALRM 信号。
  • 覆盖旧定时器: 如果进程已经设置了一个未完成的 alarm 定时器,调用 alarm取消(取消调度)之前的定时器,并设置一个新的定时器。
  • 取消定时器: 如果 seconds 参数为 0,alarm 不会设置新的定时器,但如果存在未完成的定时器,则会取消它。

4. 参数 链接到标题

  • unsigned int seconds: 指定从现在开始,经过多少后发送 SIGALRM 信号。
    • 如果 seconds 为 0:不启动新定时器,并取消任何已存在的定时器。
    • 如果 seconds 大于 0:启动一个在 seconds 秒后到期的定时器。

5. 返回值 链接到标题

  • 返回旧的定时器剩余秒数: alarm 函数返回之前设置的 alarm 定时器的剩余秒数(如果有的话)。
    • 如果之前没有设置过 alarm 定时器,或者之前的定时器已经到期,则返回 0。
    • 如果之前设置的定时器还有 N 秒到期,则返回 N
  • 错误: alarm 函数总是成功,不会因为参数错误而返回 -1。因此,它没有失败的返回路径。

6. 相似函数,或关联函数 链接到标题

  • setitimer / getitimer: 更强大、更灵活的定时器函数。
    • alarm(seconds) 在功能上大致等价于 setitimer(ITIMER_REAL, ...),其中 itimerval.it_value.tv_sec = secondsitimerval.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. 示例代码 链接到标题

示例 1:基本的 alarm 使用和信号处理 链接到标题

这个例子演示了如何设置一个 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;
}

代码解释:

  1. 定义一个 volatile sig_atomic_t 类型的全局变量 alarm_flag,用于在信号处理函数和主程序之间通信。
  2. 定义 SIGALRM 信号的处理函数 alarm_handler。当定时器到期,内核发送 SIGALRM 信号时,该函数会被调用。它打印一条消息并设置 alarm_flag
  3. main 函数中,使用 signal() 注册 SIGALRM 处理函数。
  4. 调用 alarm(3) 设置一个 3 秒后到期的定时器。返回值 remaining 应该是 0,因为这是第一次设置。
  5. 使用一个 while 循环和 pause() 等待信号。pause() 会使进程挂起,直到收到任何信号。当 SIGALRM 到达时,alarm_handler 被调用,设置 alarm_flag,主循环退出。
  6. 再次调用 alarm(2) 设置一个新的 2 秒定时器。因为前一个定时器已经被处理,所以这次返回的 remaining 也应该是 0。
  7. 重置 alarm_flag 并再次等待信号。

示例 2:使用 alarm 为阻塞操作设置超时 链接到标题

这个例子演示了如何使用 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;
}

代码解释:

  1. 定义 jmp_buf jmp_env 用于保存程序状态,volatile sig_atomic_t timed_out 用于标志。
  2. 定义 timeout_handler 信号处理函数。当 SIGALRM 到达时:
    • 打印超时消息。
    • 设置 timed_out 标志。
    • 调用 longjmp(jmp_env, 1)。这会使程序执行流立即跳转回到之前调用 setjmp(jmp_env) 的地方,并且 setjmp 返回 1。
  3. main 函数中:
    • 设置 SIGALRM 处理函数。
    • 调用 setjmp(jmp_env)。如果是第一次调用(直接调用),它返回 0。
    • if (setjmp(...) == 0) 为真的代码块中
      • 设置 5 秒 alarm
      • 调用 read(STDIN_FILENO, ...) 等待用户输入。这会阻塞。
      • 如果用户在 5 秒内输入并按回车,read 返回,程序继续执行。
      • 程序会调用 alarm(0) 来取消定时器(因为它已经不再需要)。
      • 处理 read 的返回值。
    • 如果 setjmp 返回非 0(在这里是 1,由 longjmp 导致):
      • 程序直接跳转到 else 分支,处理超时情况。
  4. 效果
    • 如果用户在 5 秒内输入,read 成功返回,定时器被取消。
    • 如果用户在 5 秒内没有输入,alarm 到期,SIGALRM 被发送,timeout_handler 被调用,longjmp 跳转,主程序在 else 分支处理超时。

注意: 使用 setjmp/longjmp 从信号处理函数中跳转是一种高级且需要小心使用的技巧。它会跳过正常的函数返回链,可能导致资源(如已分配的内存、打开的文件)未被正确清理。在现代 C 编程中,更推荐使用返回值检查和循环重试的方式,或者使用更现代的 sigsetjmp/siglongjmp

示例 3:alarm 的覆盖和返回值 链接到标题

这个例子演示了 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;
}

代码解释:

  1. 调用 alarm(10) 设置一个 10 秒定时器。因为之前没有定时器,所以返回 0。
  2. 等待 3 秒。此时原定时器还剩 7 秒。
  3. 调用 alarm(5) 设置一个 5 秒定时器。这会取消之前的 10 秒定时器。alarm 返回被取消的定时器的剩余时间,大约是 7 秒。
  4. 再等待 2 秒。此时新定时器还剩 3 秒。
  5. 调用 alarm(0) 来取消当前的定时器。alarm 返回被取消的定时器的剩余时间,大约是 3 秒。
  6. 程序继续休眠 10 秒。因为定时器已被取消,所以不会收到 SIGALRM 信号。

示例 4:忽略 SIGALRM 信号 链接到标题

这个例子演示了如果 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;
}

代码解释:

  1. 使用 fork() 创建子进程。
  2. 子进程:
    • 使用 signal(SIGALRM, SIG_IGN) 忽略 SIGALRM 信号。
    • 调用 alarm(3) 设置一个 3 秒定时器。
    • 调用 sleep(5) 睡眠 5 秒。
  3. 父进程: 等待子进程结束。
  4. 结果: 子进程会完整地睡眠 5 秒。虽然 3 秒后 alarm 定时器到期并向子进程发送了 SIGALRM 信号,但由于该信号被设置为 SIG_IGN(忽略),信号被内核丢弃,sleep 不会因此被中断。sleep 返回 0,表示它完整地睡了 5 秒。

重要提示与注意事项:

  1. 精度: alarm 的精度是秒级。如果需要更高精度(微秒或纳秒),应使用 setitimertimer_create 等函数。
  2. 单次性: alarm 只能设置一次性定时器。如果需要周期性定时,必须在 SIGALRM 处理函数中再次调用 alarm
  3. 信号安全: 在 SIGALRM 信号处理函数中,应只调用异步信号安全的函数。
  4. 竞态条件: 在设置信号处理函数和调用 alarm 之间,或者在检查标志和调用 pause/sleep 之间,可能存在竞态条件。使用 sigsuspend 可以更安全地处理。
  5. sleepalarm 的交互: sleep 函数的实现可能会使用 SIGALRM 信号。如果程序同时使用 alarmsleep,它们可能会相互干扰。在使用 alarm 设置超时的程序中,应避免使用 sleep,或者确保理解它们的交互方式。
  6. 现代替代: alarm 功能简单,易于使用,适用于许多基本场景。但对于更复杂的需求(高精度、周期性、多种时钟源),setitimertimer_ 系列的 POSIX 定时器是更强大和现代的选择。

总结:

alarm 是一个简单而有效的函数,用于设置秒级的单次定时器。它通过发送 SIGALRM 信号来通知定时器到期。理解其返回值(旧定时器的剩余时间)和与信号处理的结合使用是掌握它的关键。虽然它功能有限,但在实现简单的超时机制或定时提醒时非常有用。