好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 alarm
函数,它是一个简单易用的定时器函数,用于在指定的秒数后向进程发送 SIGALRM
信号。
1. 函数介绍 見出しへのリンク
alarm
是一个 Linux 系统调用,它提供了一种简单的方法来设置一个单次的秒级定时器。当指定的秒数过去后,内核会向调用进程发送一个 SIGALRM
信号。
你可以把它想象成一个简单的“厨房定时器”:
- 你拨动定时器,设定
N
分钟(在这里是秒)。 - 时间一到,定时器就会“叮”一声(发送
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 = 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. 示例代码 見出しへのリンク
示例 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;
}
代码解释:
- 定义一个
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
为阻塞操作设置超时
見出しへのリンク
这个例子演示了如何使用 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
的覆盖和返回值
見出しへのリンク
这个例子演示了 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
信号
見出しへのリンク
这个例子演示了如果 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
信号来通知定时器到期。理解其返回值(旧定时器的剩余时间)和与信号处理的结合使用是掌握它的关键。虽然它功能有限,但在实现简单的超时机制或定时提醒时非常有用。