4. 内核抢占与低延迟

说到Linux内核里的定时器,很多工程师第一反应就是 timer_list。嗯,这个老朋友确实够用,但它的精度受限于内核的HZ值。HZ=100时,精度只有10ms;就算HZ=1000,也就1ms。对于实时性要求高的场景——比如工业控制、音频处理、机器人控制——这点精度根本不够看。

高精度定时器(hrtimer)就是为了解决这个问题而生的。它不依赖内核的周期性时钟滴答,而是直接使用硬件的高精度时钟源(比如TSC、HPET、ACPI PM定时器)。精度可以达到纳秒级。我在做一款高速数据采集卡驱动时,就吃过标准定时器的亏——采样周期抖动太大,数据全乱了。后来换成hrtimer,问题直接解决。

4.1 hrtimer与标准定时器的核心差异

先看一张对比表,把两者的区别说清楚:

对比维度 标准定时器(timer_list) 高精度定时器(hrtimer)
精度基础 依赖内核HZ(jiffies) 依赖硬件高精度时钟源
理论精度 1/HZ(通常1ms~10ms) 纳秒级(硬件支持)
触发方式 在时钟中断中统一处理 独立的高精度时钟中断
回调上下文 软中断(softirq) 硬中断上下文或软中断
是否可抢占 不可在回调中睡眠 取决于模式(HRTIMER_MODE_ABS_HARD等)
适用场景 普通超时、轮询 实时控制、音频、高精度采样

说白了,标准定时器是“搭便车”——跟着内核的时钟滴答走。hrtimer是自己开车,想什么时候到就什么时候到。

4.2 hrtimer框架核心结构

hrtimer的核心数据结构就一个——struct hrtimer。别被它吓到,其实字段不多:

struct hrtimer {
    struct timerqueue_node      node;       // 红黑树节点,用于排序
    ktime_t                     _softexpires; // 软过期时间
    enum hrtimer_restart        (*function)(struct hrtimer *); // 回调函数
    struct hrtimer_clock_base   *base;      // 时钟基准
    u8                          state;      // 状态标志
    u8                          is_rel;     // 是否为相对定时器
    u8                          is_soft;    // 是否为软中断模式
};

这里有个关键点:_softexpires是“软过期时间”。什么意思?hrtimer允许一定范围的“松弛”——比如你设了100μs后触发,但系统允许在95μs到105μs之间触发。这个范围由hrtimer_start_range_ns()的第三个参数控制。我建议你除非有特殊需求,否则把这个范围设成0,保证最精确的触发。

核心要点:hrtimer使用红黑树管理所有定时器,按过期时间排序。每次高精度时钟中断到来时,内核从树中取出所有已过期的定时器,执行它们的回调函数。时间复杂度是O(log n),效率很高。

4.3 hrtimer的工作流程

我画了一张流程图,帮你理清hrtimer从创建到触发的完整路径:

hrtimer工作流程图 hrtimer_init() 设置回调函数 hrtimer_start() 插入红黑树,等待触发 时钟中断 → 执行回调 回调返回值 HRTIMER_NORESTART:单次 HRTIMER_RESTART:循环 启动模式 HRTIMER_MODE_ABS:绝对时间 HRTIMER_MODE_REL:相对时间

流程看起来简单,但实际使用时有几个坑。我一个个说。

4.4 在驱动中使用hrtimer

直接上代码。这是一个典型的驱动中使用hrtimer的例子:

#include <linux/hrtimer.h>
#include <linux/ktime.h>

struct my_device {
    struct hrtimer timer;
    // 其他设备相关字段...
};

// 定时器回调函数
static enum hrtimer_restart my_timer_callback(struct hrtimer *timer)
{
    struct my_device *dev = container_of(timer, struct my_device, timer);

    // 处理定时任务
    // 注意:这里不能调用可能导致睡眠的函数!
    // 比如:kmalloc(GFP_KERNEL)、mutex_lock() 等

    // 如果需要循环触发,重新启动定时器
    hrtimer_forward_now(timer, ktime_set(0, 1000000)); // 1ms后再次触发
    return HRTIMER_RESTART;
}

// 初始化定时器
static int my_device_probe(struct platform_device *pdev)
{
    struct my_device *dev;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    // ... 其他初始化 ...

    // 初始化hrtimer
    hrtimer_init(&dev->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    dev->timer.function = my_timer_callback;

    return 0;
}

// 启动定时器
static void start_my_timer(struct my_device *dev)
{
    ktime_t kt = ktime_set(0, 500000); // 500μs

    hrtimer_start(&dev->timer, kt, HRTIMER_MODE_REL);
}

// 停止定时器
static void stop_my_timer(struct my_device *dev)
{
    hrtimer_cancel(&dev->timer);
}

个人经验:我在写一个SPI总线的高精度时序驱动时,发现hrtimer的回调函数里绝对不能调用msleep()或usleep_range()。这些函数会触发调度,而hrtimer的回调默认是在硬中断上下文执行的。想睡眠?系统直接崩溃给你看。

4.5 避坑指南

这些年用hrtimer踩过的坑,我总结成几条:

  • 回调函数不能睡眠——这是最常见的坑。如果你需要在回调里做耗时操作,用schedule_work()把任务推到工作队列去。
  • 注意时钟源选择——CLOCK_MONOTONIC是单调递增的,适合测量时间间隔;CLOCK_REALTIME是墙上时间,可能被adjtimex()调整。我建议驱动里统一用CLOCK_MONOTONIC。
  • 停止定时器要小心——hrtimer_cancel()会等待回调执行完毕,如果在中断上下文调用会导致死锁。这时候用hrtimer_try_to_cancel()。
  • 循环定时器的漂移问题——如果你用hrtimer_forward_now()做周期定时,它不会累积误差。但如果你每次重新计算绝对时间,时间长了会有漂移。

我曾经踩过的坑:有一次我在一个网络驱动里用hrtimer做数据包重传定时器,回调里直接调用了dev_queue_xmit()发送数据包。结果发现系统在高负载下频繁死锁。查了两天才发现——dev_queue_xmit()内部会获取一个锁,而这个锁在软中断上下文中已经被占用了。解决方案是把数据包发送操作放到一个专用工作队列里。

4.6 性能对比:实测数据

我在一块ARM Cortex-A72开发板上做过对比测试,结果如下:

测试项目 标准定时器 hrtimer
最小触发间隔 1ms(HZ=1000) 50μs(硬件限制)
触发抖动(标准差) ±500μs ±5μs
单次启动开销 ~200ns ~800ns
回调延迟 受软中断排队影响 直接硬中断触发

看到没?hrtimer的启动开销虽然比标准定时器大一点(因为要操作红黑树),但触发精度和抖动控制完全是两个数量级。对于实时性要求高的场景,这点开销完全可以接受。

4.7 调试技巧

最后分享几个调试hrtimer的小技巧:

  • 查看系统中所有hrtimer——cat /proc/timer_list,能看到每个hrtimer的过期时间、回调函数地址、所属进程等信息。
  • 测量实际触发时间——在回调函数入口处用ktime_get()记录时间,与期望的过期时间对比,就能算出抖动。
  • 使用tracepoint——echo 1 > /sys/kernel/debug/tracing/events/timer/hrtimer_expire_entry/enable,可以追踪所有hrtimer的触发情况。
  • 注意内核配置——确保内核开启了CONFIG_HIGH_RES_TIMERS。没开的话,hrtimer会退化为使用jiffies,精度大打折扣。

嗯,hrtimer的内容差不多就这些。记住一句话:精度不够,hrtimer来凑;但别在回调里睡觉,否则系统给你脸色看。