Linux 系统编程:时间就是生命 —— `alarm` 函数与 I/O 性能优化

  • 时间:2025-11-25 23:10 作者: 来源: 阅读:0
  • 扫一扫,手机访问
摘要:各类资料学习下载合集 链接:https://pan.quark.cn/s/7c8c391011eb 在 Linux 系统编程中,信号 (Signal) 和 时间 (Time) 是两个密不可分的话题。我们经常需要在特定的时间点触发某个操作,或者统计程序的运行效率。 本篇文章将深入探讨 alarm 函数的用法、父子进程中定时器的特性,并通过一个经典的“数数”实验,揭示 I/O 操作对程序性能的巨大

各类资料学习下载合集
链接:https://pan.quark.cn/s/7c8c391011eb

在 Linux 系统编程中,信号 (Signal)时间 (Time) 是两个密不可分的话题。我们经常需要在特定的时间点触发某个操作,或者统计程序的运行效率。

本篇文章将深入探讨 alarm 函数的用法、父子进程中定时器的特性,并通过一个经典的“数数”实验,揭示 I/O 操作对程序性能的巨大影响。

一、 信号产生之软件条件: alarm 函数

除了键盘按键和 kill 系统调用,软件条件也是产生信号的重要来源。其中最典型的就是 alarm 函数。

1. 函数介绍

alarm 函数类似于一个倒计时闹钟。


#include <unistd.h>

unsigned int alarm(unsigned int seconds);
功能:设置一个定时器,在 seconds 秒后,内核会向当前进程发送 SIGALRM (14号信号) SIGALRM 的默认动作是终止进程参数:倒计时的秒数。 如果传入 0,表示取消当前已设置的闹钟。 返回值: 如果此前设置过闹钟,返回上一个闹钟剩余的秒数。如果此前未设置闹钟,返回 0特性每个进程有且只有一个闹钟。后一次调用的 alarm 会覆盖前一次的设置。

2. 代码案例:简单的定时器


/* 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 信号,程序退出

二、 性能实战:计算机1秒能数多少个数?

这是一个非常经典的实验,不仅展示了 alarm 的用法,更直观地体现了 I/O 操作对性能的影响。

1. 实验目标

统计当前计算机在 1 秒钟内,可以进行多少次 i++ 自增运算。

2. 代码实现 (未优化版)

我们在每次自增后都打印当前的计数值。


/* 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万 左右(这个数值因机器配置而异)。

3. 性能瓶颈分析

为什么才数到 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!

4. 优化方案:输出重定向

如果我们去掉打印,只在最后输出结果呢?或者通过重定向避开终端显示?

方法一:代码级优化
只在捕捉到信号后打印一次结果(需要用到信号捕捉,这里暂不展开)。

方法二:输出重定向
我们可以将程序的输出重定向到文件,或者直接丢弃( /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 对程序性能的巨大拖累。在未来的系统编程中,“少写日志,多做计算” 将是一条重要的准则。

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部