各类资料学习下载合集
链接:https://pan.quark.cn/s/7c8c391011eb
在 Linux 系统编程中,信号 (Signal) 和 时间 (Time) 是两个密不可分的话题。我们经常需要在特定的时间点触发某个操作,或者统计程序的运行效率。
本篇文章将深入探讨
alarm 函数的用法、父子进程中定时器的特性,并通过一个经典的“数数”实验,揭示 I/O 操作对程序性能的巨大影响。
alarm 函数除了键盘按键和
kill 系统调用,软件条件也是产生信号的重要来源。其中最典型的就是
alarm 函数。
alarm 函数类似于一个倒计时闹钟。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:设置一个定时器,在
seconds 秒后,内核会向当前进程发送 SIGALRM (14号信号)。
SIGALRM 的默认动作是终止进程。
参数:倒计时的秒数。
如果传入
0,表示取消当前已设置的闹钟。
返回值:
如果此前设置过闹钟,返回上一个闹钟剩余的秒数。如果此前未设置闹钟,返回
0。
特性:每个进程有且只有一个闹钟。后一次调用的
alarm 会覆盖前一次的设置。
/* alarm_basic.c */
#include <stdio.h>
#include <unistd.h>
int main() {
// 设置一个5秒的闹钟
unsigned int remaining = alarm(5);
printf("设置5秒闹钟,返回值: %d
", remaining);
sleep(2);
// 2秒后,再次设置一个3秒的闹钟
// 此时上一个闹钟还剩 3 秒 (5 - 2)
remaining = alarm(3);
printf("2秒后重设3秒闹钟,上一个闹钟剩余: %d
", remaining);
while(1) {
printf("运行中...
");
sleep(1);
}
return 0;
}
运行结果:
$ ./alarm_basic
设置5秒闹钟,返回值: 0
2秒后重设3秒闹钟,上一个闹钟剩余: 3
运行中...
运行中...
Alarm clock <-- 3秒后收到 SIGALRM 信号,程序退出
这是一个非常经典的实验,不仅展示了
alarm 的用法,更直观地体现了 I/O 操作对性能的影响。
统计当前计算机在 1 秒钟内,可以进行多少次
i++ 自增运算。
我们在每次自增后都打印当前的计数值。
/* count_io.c */
#include <stdio.h>
#include <unistd.h>
int main() {
// 1秒后发送 SIGALRM 信号,终止进程
alarm(1);
int i = 0;
while(1) {
// 每次循环都进行屏幕输出 (I/O 操作)
printf("%d
", i++);
}
return 0;
}
运行结果(直接运行):
$ ./count_io
...
194520
194521
Alarm clock
可以看到,最后数到了 19万 左右(这个数值因机器配置而异)。
为什么才数到 19 万?因为
printf 是一个 I/O 操作。
CPU 的运算速度极快,但将数据输出到终端(屏幕)的速度相对极慢。程序的大部分时间并没有花在
i++ 上,而是花在了等待 I/O 完成上。
为了验证这一点,我们使用
time 命令来测量程序的时间消耗。
$ time ./count_io > /dev/null
Alarm clock
real 0m1.004s (实际经过时间:约1秒)
user 0m0.150s (用户态CPU时间:仅0.15秒)
sys 0m0.080s (内核态CPU时间:仅0.08秒)
结论:
real (1.0s) >
user +
sys (0.23s)。剩下的 0.77s 都在等待 I/O!
如果我们去掉打印,只在最后输出结果呢?或者通过重定向避开终端显示?
方法一:代码级优化
只在捕捉到信号后打印一次结果(需要用到信号捕捉,这里暂不展开)。
方法二:输出重定向
我们可以将程序的输出重定向到文件,或者直接丢弃(
/dev/null)。但在上面的
time 测试中,即使重定向到
/dev/null,
printf 函数内部依然发生了系统调用,开销依然存在。
让我们把
printf 移出循环,看看纯计算的性能。
/* count_pure.c */
// 注意:这个程序不会自动打印结果,需要配合 gdb 或信号捕捉查看
// 这里为了演示,我们通过估算来对比
#include <stdio.h>
#include <unistd.h>
int main() {
alarm(1);
int i = 0;
while(1) {
i++;
}
return 0;
}
由于无法直接看到结果,我们可以根据经验推断:现代 CPU 1秒钟的
i++ 次数通常在 数亿次 级别。相比于带
printf 的 19 万次,性能提升了 几千倍!
工程启示:在追求高性能的程序中(如高并发服务器),应严控 I/O 操作的频率,避免频繁调用
printf 或写日志。
在使用
fork 创建子进程时,信号相关的属性是如何继承的?这是一个常考点。
SIGINT,子进程也屏蔽。未决信号集:不继承。父进程收到了信号没处理(处于未决态),
fork 出来的子进程是干净的,未决集清空。闹钟 (alarm):独立。父进程设置的闹钟,不会传递给子进程。子进程拥有自己独立的定时器。
/* fork_alarm.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
// 父进程设置5秒闹钟
alarm(5);
printf("父进程: 设置了5秒闹钟
");
pid_t pid = fork();
if (pid == 0) {
// 子进程
// 检查子进程是否有继承闹钟
// alarm(0) 返回上一个闹钟剩余时间,如果为0说明没有闹钟
int remain = alarm(0);
printf("子进程: 检测到的闹钟剩余时间为 %d
", remain);
} else {
wait(NULL); // 等待子进程结束
printf("父进程: 子进程结束了,我的闹钟还在倒计时...
");
// 实际上父进程会在5秒后被 SIGALRM 杀死
while(1);
}
return 0;
}
运行结果:
$ ./fork_alarm
父进程: 设置了5秒闹钟
子进程: 检测到的闹钟剩余时间为 0 <-- 证明子进程没有继承闹钟
父进程: 子进程结束了,我的闹钟还在倒计时...
Alarm clock <-- 父进程依然被杀死
| 知识点 | 核心内容 | 备注 |
|---|---|---|
| alarm 函数 |
unsigned int alarm(unsigned int seconds) | 返回上次剩余时间 |
| alarm 特性 | 每个进程唯一,后覆盖前 |
alarm(0) 取消定时 |
| I/O 瓶颈 | I/O 操作 (printf) 极其耗时 | 优化重点 |
| 时间测量 |
real =
user +
sys +
wait | 使用
time 命令 |
| fork 与 alarm | 定时器不继承,各自独立 | 未决集也不继承 |
| fork 与 信号 | 处理动作和屏蔽字继承 |
通过理解
alarm 和
time,我们不仅掌握了简单的定时机制,更深刻理解了 I/O 对程序性能的巨大拖累。在未来的系统编程中,“少写日志,多做计算” 将是一条重要的准则。