alarm 函数是 C 标准库(在 <unistd.h> 头文件中定义)提供的一个非常有用的工具,用于设置一个定时器,当定时器到期时,内核会向进程发送一个 SIGALRM 信号,这个机制是实现“闹钟”功能的基础。

(图片来源网络,侵删)
函数原型
#include <unistd.h> unsigned int alarm(unsigned int seconds);
参数:
seconds: 你希望闹钟在多少秒后触发,这是一个无符号整型。
返回值:
- 成功时:返回之前设置的闹钟剩余的秒数,如果之前没有设置闹钟,则返回
0。 - 失败时:返回
0(errno会被设置为EINVAL,但这在 Linux 系统上很少见,因为0是一个有效的参数)。
关键点:
- 一个进程只能有一个
alarm闹钟在运行,如果你在旧的闹钟到期之前调用alarm,旧的闹钟会被取消,alarm函数会返回旧闹钟剩余的时间。 seconds参数为0,则取消任何之前设置的闹钟,并返回之前闹钟剩余的时间。
工作原理:信号
alarm 函数本身并不会让你的程序“暂停”或“休眠”,它的工作流程是:

(图片来源网络,侵删)
- 调用
alarm(5),告诉内核:“请在 5 秒后给我发送一个SIGALRM信号”。 - 你的程序会继续执行后续的代码,不会被阻塞。
- 5 秒后,内核向你的进程发送
SIGALRM信号。 - 默认情况下,收到
SIGALRM信号的进程会终止,这就是为什么如果你只调用alarm而不处理信号,程序会在指定时间后自动退出。
要实现一个有用的“闹钟”,我们必须学会如何“捕获”并“处理”这个 SIGALRM 信号,而不是让它默认终止程序。
如何处理信号:signal 函数
为了处理信号,我们需要使用 signal 函数(在 <signal.h> 中定义)。
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
signal 函数允许你为某个信号(如 SIGALRM)指定一个“处理函数”(也叫“信号处理函数”或“信号处理器”)。
参数:

(图片来源网络,侵删)
signum: 你想处理的信号编号或宏,对于alarm,我们使用SIGALRM。handler: 一个指向函数的指针,当信号发生时,这个函数会被调用,它可以是:SIG_IGN: 忽略该信号。SIG_DFL: 恢复该信号的默认行为(对于SIGALRM,默认行为是终止进程)。- 一个自定义函数的地址:这个函数就是你的信号处理函数。
信号处理函数的要求:
- 它的参数类型必须是
int,代表接收到的信号编号。 - 它的返回类型必须是
void。 - 重要:信号处理函数应该尽可能简单、快速,避免在处理函数中调用标准 I/O 函数(如
printf)、malloc等非异步安全(async-signal-safe)的函数,因为在信号处理期间调用它们可能会导致不可预测的行为。
完整示例:一个简单的 5 秒闹钟
这个例子会设置一个 5 秒的闹钟,并打印一条消息,然后等待闹钟触发,触发后,它会打印另一条消息,而不是直接退出。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h> // 用于 exit
// 全局变量,用于表示闹钟是否触发
volatile sig_atomic_t alarm_triggered = 0;
// SIGALRM 信号的处理函数
void alarm_handler(int signum) {
// 注意:这个函数应该尽量简单
printf("闹钟响了!\n");
alarm_triggered = 1; // 设置标志位
}
int main() {
// 注册 SIGALRM 信号的处理函数
if (signal(SIGALRM, alarm_handler) == SIG_ERR) {
perror("无法注册信号处理函数");
exit(EXIT_FAILURE);
}
printf("程序开始,5秒后将触发闹钟...\n");
// 设置一个 5 秒的闹钟
alarm(5);
// 等待闹钟触发
// 使用一个简单的循环来等待,直到我们的标志位被设置
// 在实际应用中,这里可能是主程序的业务逻辑
while (!alarm_triggered) {
// 空循环,让 CPU 稍作休息
// 在更复杂的程序中,这里可以执行其他任务
sleep(1); // 休眠1秒,避免CPU空转
}
printf("闹钟处理完毕,程序继续执行...\n");
return 0;
}
编译和运行:
gcc -o my_alarm my_alarm.c ./my_alarm
预期输出:
程序开始,5秒后将触发闹钟...
(等待5秒)...
闹钟响了!
闹钟处理完毕,程序继续执行...
更实用的示例:带超时的 read
alarm 的一个经典应用是为可能阻塞的系统调用(如 read, write, accept)设置超时。
假设你想从标准输入读取一行数据,但如果用户在 10 秒内没有输入,你就想放弃并提示超时。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 1024
volatile sig_atomic_t timeout = 0;
void timeout_handler(int signum) {
printf("\n输入超时!\n");
timeout = 1;
}
int main() {
char buffer[BUFFER_SIZE];
int bytes_read;
// 注册超时处理函数
if (signal(SIGALRM, timeout_handler) == SIG_ERR) {
perror("无法注册信号处理函数");
exit(EXIT_FAILURE);
}
printf("请在10秒内输入一些文字: ");
fflush(stdout); // 确保提示信息立即显示
// 设置10秒超时
alarm(10);
// read 函数可能会阻塞,直到有输入或信号中断
bytes_read = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);
// 如果因为信号而中断,read 会返回 -1,errno 会被设置为 EINTR
if (bytes_read == -1 && errno == EINTR) {
if (timeout) {
// 是我们设置的 alarm 信号导致的超时
printf("操作因超时被取消,\n");
} else {
// 是其他信号导致的
perror("read 被信号中断");
}
} else if (bytes_read > 0) {
// 成功读取
buffer[bytes_read] = '\0'; // 确保字符串以 null
printf("你输入了: %s\n", buffer);
}
// 取消闹钟,防止它在读取完成后还触发
alarm(0);
return 0;
}
分析:
- 我们设置了
alarm(10)。 - 程序调用
read等待用户输入。read会阻塞程序。 - 情况A: 用户在10秒内输入了内容。
read返回,我们处理输入,然后调用alarm(0)取消闹钟。 - 情况B: 10秒过去了,
alarm触发,发送SIGALRM信号。timeout_handler被调用,打印 "输入超时!" 并设置timeout = 1。read函数被信号中断,它立即返回-1,errno被设置为EINTR(Interrupted system call)。main函数中的if条件判断为真,我们检查timeout标志位,确认是超时导致的,并打印相应消息。
alarm 与 sleep 的关系
你可能注意到 sleep 函数也能实现延时。sleep 的内部实现通常就依赖于 alarm。
sleep(n)的工作原理大致是:- 调用
alarm(n)设置一个闹钟。 - 调用
pause()函数(在<unistd.h>中),pause会让进程休眠,直到接收到任何信号。 - 当
SIGALRM信号到达时,pause返回,进程被唤醒。 sleep函数会检查alarm的返回值,看看是否提前被唤醒,然后计算出实际休眠的时间并返回。
- 调用
关键区别:
alarm是非阻塞的,设置后立即返回。sleep是阻塞的,直到指定时间结束或被信号中断。
| 特性 | 描述 |
|---|---|
| 头文件 | <unistd.h> |
| 功能 | 设置一个定时器,到期后向进程发送 SIGALRM 信号。 |
| 核心机制 | 信号。alarm 本身不阻塞程序,需要配合信号处理函数才能实现复杂逻辑。 |
| 限制 | 一个进程只能有一个活动的 alarm,新的调用会取消旧的。 |
| 主要用途 | 实现超时控制(如网络编程中的超时读/写)。 实现周期性任务(通过在信号处理函数中重新调用 alarm)。简单的延时和提醒功能。 |
| 注意事项 | 信号处理函数必须简洁,避免调用非异步安全函数。 |
alarm 是 Linux/Unix 系统编程中一个基础且强大的工具,理解它与信号的交互方式是掌握高级 C 语言编程的关键一步。
