编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

    • Linux系统编程

      • Linux系统编程基础
      • 系统调用原理精讲
      • 文件IO深度解析
      • 进程管理深度解析
      • 线程并发深度解读
      • 信号处理深度解读
      • 进程间通信IPC
      • 网络IO多路复用
      • 内存管理深度解读
      • 动态链接与共享库
      • Linux与定时器
      • 综合项目实战

    • IoT智能硬件开发

    • Apps
    • Linux应用开发
    • Linux系统编程
    杨充
    2025-07-02
    目录

    Linux与定时器

    # 10.Linux与定时器

    时间是一切实时系统的基础——嵌入式应用中的动画帧率、传感器采集周期、看门狗喂狗、超时重传全部依赖时钟。本节覆盖从传统 alarm 到现代 timerfd 的全套定时器方案。

    # 目录介绍

    • 10.1 案例引入
      • 10.1.1 用户调时间后仪表盘动画卡死
      • 10.1.2 核心问题
    • 10.2 Linux 时间体系
      • 10.2.1 CLOCK_REALTIME 壁钟时间
      • 10.2.2 CLOCK_MONOTONIC 单调时间
      • 10.2.3 CLOCK_MONOTONIC_RAW 无 NTP 校正
      • 10.2.4 CLOCK_BOOTTIME 休眠统计
      • 10.2.5 gettimeofday 的废弃与 clock_gettime
    • 10.3 NTP 时间跳变与防护
      • 10.3.1 NTP slewing vs stepping
      • 10.3.2 时间倒退的防御策略
      • 10.3.3 嵌入式设备首次上电 1970 年问题
    • 10.4 传统定时器
      • 10.4.1 alarm 与 SIGALRM 的局限性
      • 10.4.2 setitimer 三种定时器
      • 10.4.3 usleep/nanosleep/clock_nanosleep 精度对比
    • 10.5 timerfd:把定时器变成文件描述符
      • 10.5.1 timerfd_create/settime/gettime
      • 10.5.2 timerfd + epoll 事件驱动
      • 10.5.3 单次 vs 周期性定时器
      • 10.5.4 timerfd 在 Qt 事件循环中的集成
    • 10.6 高精度定时器
      • 10.6.1 hrtimer 内核支持
      • 10.6.2 POSIX 定时器 timer_create
      • 10.6.3 嵌入式实时性要求与 PREEMPT_RT
    • 10.7 嵌入式定时器最佳实践
      • 10.7.1 看门狗喂狗的定时器设计
      • 10.7.2 动画帧调度与 vsync 对齐
      • 10.7.3 低功耗设备上的定时器唤醒策略
    • 10.8 关键结论与速查表

    # 10.1 案例引入

    # 10.1.1 用户调时间后仪表盘动画卡死

    某车载仪表盘的转速表动画使用 QElapsedTimer(基于 CLOCK_MONOTONIC)实现平滑旋转。一切正常,直到某天用户把系统时间从 2025 年调成 2038 年——仪表盘卡死 5 秒不动。

    排查发现 QML 中另一个计时器组件使用了 new Date().getTime()(基于 CLOCK_REALTIME)。时间跳变引发了绑定链条的级联失效——NTP 的 stepping 模式直接让壁钟倒退 13 年(从 2038 回到 NTP 服务器纠正的 2025),所有依赖壁钟的状态机集体崩溃。

    正确做法:UI 动画一律用 CLOCK_MONOTONIC,只有需要显示给用户看的"日历时间"才用 CLOCK_REALTIME。

    两种时钟在时间跳变时的表现对比:

    CLOCK_REALTIME:
      用户调时: 2025 → 2038  ──→  值跳变 +13 年
      NTP 纠正: 2038 → 2025  ──→  值倒退 -13 年  ← 所有定时器/动画崩溃
    
    CLOCK_MONOTONIC:
      用户调时: 不影响 ──→  继续单调递增
      NTP 纠正: 不影响 ──→  继续单调递增
      只有系统重启时才从 0 开始
    

    # 10.1.2 核心问题

    1. 五种时钟类型 REALTIME / MONOTONIC / MONOTONIC_RAW / BOOTTIME / PROCESS_CPUTIME 应该怎么选?
    2. timerfd 为什么是 epoll 事件循环的最佳拍档?相比 setitimer 有什么优势?
    3. 嵌入式设备首次上电时间是 1970 年——如何优雅处理?
    4. NTP 的 slewing 和 stepping 有什么区别?如何防止时间倒退引发的 bug?

    # 10.2 Linux 时间体系

    # 10.2.1 CLOCK_REALTIME 壁钟时间

    CLOCK_REALTIME 是"人类世界的时间"——1970-01-01 至今的秒数。它的值可以被 settimeofday()、NTP、date 命令修改:

    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    printf("Unix timestamp: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
    

    CLOCK_REALTIME 的行为特征:

    操作 对 CLOCK_REALTIME 的影响
    date -s 手动改时间 瞬间跳变 (stepping)
    NTP slewing(慢调) 每秒加速/减速最多 500 ppm (~43ms/天)
    NTP stepping(快调) 瞬间跳到正确值
    闰秒 加/减 1 秒
    系统挂起/休眠 恢复后跳到当前时间

    适用场景:日志时间戳、文件 mtime、用户界面显示的时间、证书过期检查。

    # 10.2.2 CLOCK_MONOTONIC 单调时间

    CLOCK_MONOTONIC 保证"只往前走,绝不后退"——从系统启动到现在的单调秒数:

    struct timespec t1, t2;
    clock_gettime(CLOCK_MONOTONIC, &t1);
    do_work();
    clock_gettime(CLOCK_MONOTONIC, &t2);
    
    uint64_t elapsed_ns = (t2.tv_sec - t1.tv_sec) * 1000000000ULL
                        + (t2.tv_nsec - t1.tv_nsec);
    printf("elapsed: %lu ns\n", elapsed_ns);
    

    CLOCK_MONOTONIC 受 NTP 的 slewing 影响——NTP 慢调时会微调频率,但从不往回跳。如果你需要完全不受 NTP 影响的单调时钟,用 CLOCK_MONOTONIC_RAW。

    适用场景:动画计时、性能测量、超时检测、定时器、游戏/动画引擎。

    # 10.2.3 CLOCK_MONOTONIC_RAW 无 NTP 校正

    CLOCK_MONOTONIC_RAW 是纯粹的硬件时钟计数——不受 NTP 的 adjtime() 影响,完全以硬件频率递增:

    // 需要不受 NTP 干扰的精确测量时用 RAW
    clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
    usleep(1000);
    clock_gettime(CLOCK_MONOTONIC_RAW, &t2);
    

    MONOTONIC vs MONOTONIC_RAW 的差异:

    特性 MONOTONIC MONOTONIC_RAW
    单调递增 ✅ ✅
    受 NTP slewing 影响 ✅(频率微调) ❌
    跨系统同步 接近(经 NTP 校准) 独立(可能漂移)
    适用场景 超时、动画、定时器 精确性能测量

    # 10.2.4 CLOCK_BOOTTIME 休眠统计

    CLOCK_BOOTTIME 与 MONOTONIC 相同,但包含系统休眠期间的时长:

    // 想知道设备实际运行了多少秒(含休眠)
    clock_gettime(CLOCK_BOOTTIME, &ts);
    

    适用场景:判断设备总运行时长(含待机)、电池寿命统计、定期"自设备启动以来"的维护任务。

    # 10.2.5 gettimeofday 的废弃与 clock_gettime

    gettimeofday 已过时——三个致命缺陷:

    缺陷 说明
    精度低 毫秒级(struct timeval),无纳秒支持
    不是单调 返回壁钟时间,会跳变
    Y2038 问题 32 位 time_t 在 2038-01-19 溢出(32 位系统)

    始终用 clock_gettime 替代:

    // ❌ 旧式
    struct timeval tv;
    gettimeofday(&tv, NULL);
    
    // ✅ 现代
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    

    时钟选择的黄金法则:

    用途 时钟
    显示给用户的时间 CLOCK_REALTIME
    动画/定时器/超时 CLOCK_MONOTONIC
    精确性能测量 CLOCK_MONOTONIC_RAW
    总运行时长(含休眠) CLOCK_BOOTTIME
    进程 CPU 使用时间 CLOCK_PROCESS_CPUTIME_ID

    # 10.3 NTP 时间跳变与防护

    # 10.3.1 NTP slewing vs stepping

    NTP 有两种纠正本地时钟的方式:

    方式 行为 持续时间 对 timer/动画的影响
    slewing 每秒加速/减速 ≤ 500 ppm (~43ms/天) 数小时(平滑) 无感知
    stepping 瞬间跳到正确值 瞬间 ❌ 所有依赖壁钟的组件崩溃

    ntpd 的默认策略:偏差 < 128ms → slewing;偏差 ≥ 128ms → stepping。

    嵌入式设备禁用 stepping:

    # chrony 配置
    echo "maxslewrate 1000" >> /etc/chrony.conf   # 允许更快的 slewing
    echo "makestep 1 -1" >> /etc/chrony.conf       # 只在启动时 stepping
    
    # ntpd 配置
    echo "tinker panic 0" >> /etc/ntp.conf         # 任何偏差都不 stepping
    

    # 10.3.2 时间倒退的防御策略

    防御规则——只有 CLOCK_REALTIME 会倒退:

    // 1. 测量时间差:永远用 MONOTONIC
    uint64_t start = get_monotonic_us();
    do_work();
    uint64_t elapsed = get_monotonic_us() - start;  // 绝不为负
    
    // 2. 需要日历时间的场合:检查倒退
    time_t last = time(NULL);
    while (1) {
        time_t now = time(NULL);
        if (now < last) {
            fprintf(stderr, "WARN: time went backwards! %ld → %ld\n", last, now);
            // 恢复正常操作——不依赖 now
        }
        last = now;
    }
    
    // 3. 定时任务:用 MONOTONIC 驱动,REALTIME 只做显示
    uint64_t next_fire = get_monotonic_ms() + 1000;  // 1 秒后触发
    // 即使 REALTIME 跳变,next_fire 依然准确
    

    # 10.3.3 嵌入式设备首次上电 1970 年问题

    嵌入式设备无 RTC 电池——首次上电时间是 1970-01-01:

    time_t now = time(NULL);
    if (now < 946684800) {  // 2000-01-01 之前 = 显然不对
        printf("RTC not set, using fallback time\n");
        // 策略 A:使用文件系统最后一次修改时间
        struct stat st;
        if (stat("/etc/config", &st) == 0) {
            now = st.st_mtime;  // 至少不会比上次配置更早
        }
        // 策略 B:使用编译时间(__DATE__ __TIME__)
        // 见下面的例子
    }
    

    编译时间回退——嵌入式"至少不倒退"的技巧:

    // 链接时嵌入编译时间戳
    const char build_time[] = __DATE__ " " __TIME__;
    
    time_t get_min_valid_time() {
        struct tm tm = {0};
        strptime(build_time, "%b %d %Y %H:%M:%S", &tm);
        time_t build_ts = mktime(&tm);  // 编译时的 Unix 时间戳
    
        time_t now = time(NULL);
        return (now < build_ts) ? build_ts : now;
        // 保证至少不早于编译日期
    }
    

    # 10.4 传统定时器

    # 10.4.1 alarm 与 SIGALRM 的局限性

    alarm 是最古老的定时器——秒级精度,只能设一个:

    void handle_timeout(int sig) {
        printf("timeout\n");
    }
    
    signal(SIGALRM, handle_timeout);
    alarm(5);              // 5 秒后触发 SIGALRM
    // 只有 1 个定时器:再次调 alarm() 会取消前一个
    

    alarm 的四大局限:

    局限 说明
    秒级精度 不能设毫秒/纳秒
    只有一个 不能同时设多个定时器
    信号上下文 只能在信号处理器中处理(async-signal-safe 受限)
    与 sleep 冲突 sleep() 内部使用 SIGALRM,混用导致未定义行为

    # 10.4.2 setitimer 三种定时器

    setitimer 是 alarm 的升级版——微秒精度,支持三种类型:

    struct itimerval timer = {
        .it_value = {.tv_sec = 0, .tv_usec = 500000},      // 首次 500ms
        .it_interval = {.tv_sec = 0, .tv_usec = 100000}    // 之后每 100ms
    };
    setitimer(ITIMER_REAL, &timer, NULL);   // 真实时间定时器
    

    三种定时器类型:

    类型 时钟基准 信号 适用
    ITIMER_REAL 真实时间(壁钟) SIGALRM 通用定时
    ITIMER_VIRTUAL 进程用户态 CPU 时间 SIGVTALRM CPU 性能分析
    ITIMER_PROF 进程总 CPU 时间(用户+内核) SIGPROF 代码性能 profiling

    setitimer 仍有限制:每个进程每种类型只有一个定时器;仍依赖信号机制;微秒精度而非纳秒。

    # 10.4.3 usleep/nanosleep/clock_nanosleep 精度对比

    三种精确睡眠——timerfd 尚未出现时的选择:

    // usleep —— 已废弃,微秒精度
    usleep(100000);          // 100 ms
    
    // nanosleep —— POSIX,纳秒精度,但只能相对时间
    struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000000};
    nanosleep(&ts, NULL);    // 100 ms
    
    // clock_nanosleep —— POSIX,纳秒精度,支持绝对时间
    struct timespec deadline;
    clock_gettime(CLOCK_MONOTONIC, &deadline);
    deadline.tv_nsec += 100000000;  // +100 ms
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL);
    

    三者的精度对比(ARM Cortex-A7, Linux 5.10):

    函数 名义精度 实际抖动量 Y2038 安全
    usleep 1 μs ±50-200 μs ❌(32位)
    nanosleep 1 ns ±30-100 μs ✅(64位 timespec)
    clock_nanosleep(ABSTIME) 1 ns ±20-80 μs ✅

    clock_nanosleep(TIMER_ABSTIME) 的优势——不受被信号中断唤醒的时间累积影响。如果你用相对睡眠被信号打断 3 次,每次重试都有累计误差;绝对时间的 clock_nanosleep 说"在 wall-clock ts X 唤醒我"——无论被提前唤醒多少次。


    # 10.5 timerfd:把定时器变成文件描述符

    # 10.5.1 timerfd_create/settime/gettime

    timerfd 是 Linux 特有的定时器接口——定时器是一个文件描述符:

    #include <sys/timerfd.h>
    
    // 创建基于 MONOTONIC 的 timerfd
    int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
    
    // 设置:1 秒后首次触发,之后每 500ms 触发一次
    struct itimerspec its = {
        .it_value    = {.tv_sec = 1, .tv_nsec = 0},            // 首次:1s
        .it_interval = {.tv_sec = 0, .tv_nsec = 500000000}     // 周期:500ms
    };
    timerfd_settime(tfd, 0, &its, NULL);
    
    // 消费定时器事件(读 tfd)
    uint64_t expirations;
    ssize_t n = read(tfd, &expirations, sizeof(expirations));
    // expirations = 触发次数(如果读取不及时,可能 > 1)
    

    timerfd_gettime 查询剩余时间:

    struct itimerspec remaining;
    timerfd_gettime(tfd, &remaining);
    printf("next fire in: %ld.%09ld s\n",
           remaining.it_value.tv_sec, remaining.it_value.tv_nsec);
    

    # 10.5.2 timerfd + epoll 事件驱动

    timerfd 与 epoll 天然融合——定时器和 I/O 统一处理:

    int epfd = epoll_create1(0);
    
    struct epoll_event ev = {.events = EPOLLIN, .data.fd = tfd};
    epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
    
    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == tfd) {
                uint64_t exp;
                read(tfd, &exp, sizeof(exp));       // 消费事件
                periodic_task();
            } else {
                handle_io(events[i].data.fd);
            }
        }
    }
    

    timerfd 相比传统定时器的四大优势:

    优势 说明
    事件驱动 与 epoll/select 统一,不依赖信号
    无信号上下文限制 处理器可调任意函数(printf/malloc 都行)
    多实例 每个 timerfd_create() 创建一个独立定时器
    可读可查 read 消费、timerfd_gettime 查询剩余时间
    支持 fork/exec 继承 可传给子进程

    # 10.5.3 单次 vs 周期性定时器

    单次(one-shot)——it_interval = {0, 0}:

    struct itimerspec once = {
        .it_value    = {.tv_sec = 5, .tv_nsec = 0},
        .it_interval = {.tv_sec = 0, .tv_nsec = 0}      // ← 不重复
    };
    timerfd_settime(tfd, 0, &once, NULL);
    // 5 秒后触发一次,之后 tfd 不再可读
    

    周期性——it_interval != {0, 0}:

    struct itimerspec periodic = {
        .it_value    = {.tv_sec = 0, .tv_nsec = 100000000},    // 首次 100ms
        .it_interval = {.tv_sec = 0, .tv_nsec = 100000000}     // 每 100ms
    };
    timerfd_settime(tfd, 0, &periodic, NULL);
    

    单次定时器的典型场景——超时管理:

    // 每个网络请求配一个单次 timerfd
    int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    struct itimerspec timeout = {
        .it_value = {.tv_sec = 5, .tv_nsec = 0},  // 5 秒超时
        .it_interval = {0, 0}                       // 不重复
    };
    timerfd_settime(timer_fd, 0, &timeout, NULL);
    
    // epoll 中同时监听 socket fd 和 timer fd
    // 哪个先就绪就处理哪个——完美实现超时机制
    

    # 10.5.4 timerfd 在 Qt 事件循环中的集成

    Qt 5 起,QSocketNotifier 可以把任何 fd 集成到 Qt 事件循环:

    #include <QSocketNotifier>
    #include <sys/timerfd.h>
    #include <unistd.h>
    
    class TimerFdNotifier : public QObject {
        Q_OBJECT
    public:
        TimerFdNotifier(int intervalMs, QObject *parent = nullptr)
            : QObject(parent), m_tfd(timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK))
        {
            struct itimerspec its = {
                .it_value    = {.tv_sec = intervalMs / 1000,
                                .tv_nsec = (intervalMs % 1000) * 1000000},
                .it_interval = {.tv_sec = intervalMs / 1000,
                                .tv_nsec = (intervalMs % 1000) * 1000000}
            };
            timerfd_settime(m_tfd, 0, &its, NULL);
    
            m_notifier = new QSocketNotifier(m_tfd, QSocketNotifier::Read, this);
            connect(m_notifier, &QSocketNotifier::activated, this, [this]() {
                uint64_t exp;
                read(m_tfd, &exp, sizeof(exp));
                emit timeout();
            });
        }
    
        ~TimerFdNotifier() { close(m_tfd); }
    
    signals:
        void timeout();
    
    private:
        int m_tfd;
        QSocketNotifier *m_notifier;
    };
    
    // 用法:TimerFdNotifier *t = new TimerFdNotifier(100, this);
    //       connect(t, &TimerFdNotifier::timeout, this, &MyObject::onTick);
    

    # 10.6 高精度定时器

    # 10.6.1 hrtimer 内核支持

    hrtimer(High Resolution Timer)是内核 2.6.16 引入的纳秒级定时器子系统。它是 timerfd、nanosleep、clock_nanosleep 的底层实现。

    检查 hrtimer 是否激活:

    cat /proc/timer_list | head -20
    # 如果看到 .resolution: 1 nsecs → hrtimer 已激活
    # 如果看到 .resolution: 10000000 nsecs → 只有 tick (10ms)
    
    # 确认高精度模式
    cat /sys/devices/system/clocksource/clocksource0/current_clocksource
    # tsc 或 arch_sys_counter (ARM)
    

    hrtimer 在 ARM 上的特殊配置——必须启用内核 CONFIG_HIGH_RES_TIMERS=y。嵌入式内核默认通常已开启。

    # 10.6.2 POSIX 定时器 timer_create

    POSIX 定时器——比 timerfd 更灵活(跨平台),但接口更复杂:

    #include <signal.h>
    #include <time.h>
    
    // 创建定时器
    struct sigevent sev = {
        .sigev_notify = SIGEV_SIGNAL,
        .sigev_signo = SIGRTMIN,         // 用实时信号通知
        .sigev_value.sival_ptr = &timer_id
    };
    timer_t timerid;
    timer_create(CLOCK_MONOTONIC, &sev, &timerid);
    
    // 设置定时器
    struct itimerspec its = {
        .it_value = {.tv_sec = 0, .tv_nsec = 500000000},
        .it_interval = {.tv_sec = 0, .tv_nsec = 500000000}
    };
    timer_settime(timerid, 0, &its, NULL);
    
    // 删除
    timer_delete(timerid);
    

    timerfd vs POSIX timer 选择:

    特性 timerfd POSIX timer (timer_create)
    可移植性 Linux 独有 POSIX(Linux/macOS/QNX)
    通知方式 fd 可读(epoll 原生支持) 信号 / 线程 / 无
    API 复杂度 极简 复杂
    多实例 每个 timerfd_create 一个 每个 timer_create 一个
    嵌入式推荐 ✅ Linux 平台首选 ⚠️ 跨平台才用

    # 10.6.3 嵌入式实时性要求与 PREEMPT_RT

    标准 Linux 内核的定时器精度受限于 tick:

    • 默认 CONFIG_HZ=250(tick 间隔 4ms)
    • 即使 timerfd 设了 100μs 的定时器,实际触发抖动 ±2ms

    PREEMPT_RT 补丁——使 Linux 成为硬实时 OS:

    # 检查是否打了 RT 补丁
    uname -v | grep PREEMPT_RT
    # 或
    cat /sys/kernel/realtime
    
    # RT 内核下:
    # - 定时器抖动:±50 μs → ±5 μs
    # - 中断线程化:所有中断处理变为内核线程(可抢占)
    # - spinlock 变 rt_mutex
    

    嵌入式 RT 选型:

    场景 内核 典型抖动
    仪表盘动画(60fps) 标准 PREEMPT ±2-4 ms
    CAN 总线(1ms 周期) PREEMPT_RT ±50-100 μs
    电机控制(50μs 周期) PREEMPT_RT + 独立核 ±5-10 μs
    硬实时(< 10μs) Xenomai / FreeRTOS ±1-5 μs

    # 10.7 嵌入式定时器最佳实践

    # 10.7.1 看门狗喂狗的定时器设计

    看门狗需要一个独立于主事件循环的定时器——主事件循环卡住时依然能喂狗:

    // 方案:独立线程 + timerfd + 喂狗
    void *watchdog_thread(void *arg) {
        int wdt_fd = open("/dev/watchdog", O_RDWR);
        int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
    
        struct itimerspec its = {
            .it_value    = {.tv_sec = 5, .tv_nsec = 0},    // 5 秒喂一次
            .it_interval = {.tv_sec = 5, .tv_nsec = 0}
        };
        timerfd_settime(tfd, 0, &its, NULL);
    
        while (1) {
            uint64_t exp;
            read(tfd, &exp, sizeof(exp));
            ioctl(wdt_fd, WDIOC_KEEPALIVE, 0);              // 喂狗
        }
    }
    
    // 主线程卡死 → 喂狗线程依然运行 → 5 秒后继续喂狗 → 不重启
    // 真正全部卡死(调度器崩溃/中断关闭)→ 看门狗 30s 超时 → 硬件重启
    

    # 10.7.2 动画帧调度与 vsync 对齐

    QML 动画的理想定时器——DRM vblank 事件(vsync):

    60 Hz 显示屏 → vsync 每 16.67ms 发生一次
    QML 渲染循环 → 应在 vsync 后立刻开始渲染,在下一个 vsync 前完成
    
    嵌入式优化:用 DRM/KMS 的 vblank fd + epoll
    

    嵌入式 vsync 对齐的 timerfd 技巧:

    // 用 timerfd 模拟 vsync 节拍(如果硬件不支持 DRM vblank fd)
    int vsync_tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    struct itimerspec vsync = {
        .it_value    = {.tv_sec = 0, .tv_nsec = 16666667},   // 60 Hz = 16.67ms
        .it_interval = {.tv_sec = 0, .tv_nsec = 16666667}
    };
    timerfd_settime(vsync_tfd, 0, &vsync, NULL);
    

    # 10.7.3 低功耗设备上的定时器唤醒策略

    嵌入式低功耗设备的矛盾——需要休眠省电,又需要定时器唤醒采集数据。

    timerfd + CLOCK_BOOTTIME_ALARM(Linux 3.11+)——允许定时器在系统休眠时唤醒设备:

    // 使用支持唤醒的时钟
    int tfd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK);
    struct itimerspec its = {
        .it_value = {.tv_sec = 60, .tv_nsec = 0}     // 60 秒后唤醒
    };
    timerfd_settime(tfd, TFD_TIMER_ABSTIME, &its, NULL);
    
    // 现在可以安全调 suspend:
    // system("echo mem > /sys/power/state");
    // 60 秒后 timerfd 会触发 CPU 中断 → 唤醒设备 → epoll_wait 返回
    

    唤醒策略总结:

    设备类型 定时器周期 方案
    持续供电(车载) 10-100ms timerfd + epoll
    电池供电(传感器) 1-60 分钟 CLOCK_BOOTTIME_ALARM + suspend
    太阳能的功耗极度受限 10-60 分钟 MCU 外部 RTC 芯片唤醒 Linux 主机
    看门狗 1-10s 独立线程 timerfd

    # 10.8 关键结论与速查表

    核心结论五条:

    1. 动画/定时器/超时一律用 CLOCK_MONOTONIC——绝不倒退,不受用户调时和 NTP stepping 影响
    2. timerfd + epoll 是嵌入式定时器的最佳方案——事件驱动、无信号限制、与 I/O 统一处理
    3. clock_nanosleep(TIMER_ABSTIME) 比相对睡眠更准确——指定唤醒时刻而非睡眠时长,避免累积误差
    4. 嵌入式 RTC 缺失 = 1970 年问题——用编译时间或文件系统时间设置"最低有效时间"兜底
    5. 看门狗喂狗用独立线程 timerfd——主事件循环卡死不影响硬件看门狗喂狗

    五种时钟速查表:

    时钟 会倒退 受NTP 含休眠 适用
    REALTIME ✅ ✅ ❌ 显示时间
    MONOTONIC ❌ 仅 slewing ❌ 定时/动画
    MONOTONIC_RAW ❌ ❌ ❌ 性能测量
    BOOTTIME ❌ 仅 slewing ✅ 运行总时长
    PROCESS_CPUTIME ❌ ❌ ❌ CPU 占用分析

    定时器方案速查表:

    方案 精度 多实例 epoll 友好 嵌入式推荐
    alarm 秒 ❌ ❌ ❌
    setitimer μs ❌(每种1个) ❌ ❌
    nanosleep 纳秒(nom) ✅(阻塞) ❌ ⚠️ 仅简单阻塞等待
    clock_nanosleep 纳秒(nom) ✅(阻塞) ❌ ⚠️ 需绝对时间
    timerfd 纳秒 ✅ ✅ ✅ 最佳
    POSIX timer_create 纳秒 ✅ ❌(用信号) ⚠️ 跨平台

    时间 API 版本速查:

    废弃 替代 原因
    gettimeofday clock_gettime(REALTIME) Y2038 + 非单调
    usleep nanosleep / clock_nanosleep 精度低 + 废弃
    alarm timerfd_create 独一份秒级
    setitimer timerfd_create 信号驱动 + 单实例
    sleep nanosleep / clock_nanosleep 精度低

    下一步:把你的 Qt 应用中的所有 QTimer 在 Linux 嵌入式目标上的底层实现(默认就是 timerfd + epoll)做一次 strace 验证——strace -e timerfd_create,timerfd_settime,epoll_wait ./myapp,亲眼看到每个 QTimer 对应一个 timerfd。


    延伸阅读:

    • man 2 clock_gettime —— 时间获取
    • man 2 timerfd_create —— timerfd 接口
    • man 2 clock_nanosleep —— 高精度睡眠
    • man 7 time —— Linux 时间系统概览
    • man 2 timer_create —— POSIX 定时器
    上次更新: 2026/07/12, 19:39:51
    动态链接与共享库
    综合项目实战

    ← 动态链接与共享库 综合项目实战→

    最近更新
    01
    audit
    07-27
    02
    C++入门教程全章思考题汇编
    07-24
    03
    12.技术团队建设能力
    07-21
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式