定时器与延时:内核定时器、hrtimer 与延时函数

定时器与延时:内核定时器、hrtimer 与延时函数

驱动里经常需要定时执行任务、等待硬件就绪。这篇讲 Linux 内核的定时机制:低精度定时器 timer_list、高精度定时器 hrtimer、以及各种延时函数的区别和用法。

大家好,我是黒漂技术佬。

驱动开发里定时和延时很常见:定时轮询传感器、LED 闪烁、等待硬件寄存器就绪、超时检测......都要用。

Linux 内核里有两套定时器:低精度的 timer_list(基于 jiffies,毫秒级)和高精度的 hrtimer(纳秒级)。还有各种延时函数,忙等待和睡眠延时要分清。

这篇讲定时器和延时的用法和区别。


一、jiffies 和 HZ

概念

内核有个全局滴答计数器 jiffies,每次时钟中断加 1。

HZ 是每秒的滴答数,也就是时钟中断的频率。常见值:100、250、300、1000。

复制代码
HZ=1000 → 每1ms一次中断,jiffies 每秒加 1000
HZ=100  → 每10ms一次中断

jiffies 的类型

c 复制代码
#include <linux/jiffies.h>

extern unsigned long volatile jiffies;

时间转换

c 复制代码
// 毫秒转 jiffies
msecs_to_jiffies(100)   // 100ms 等于多少个 jiffies

// 微秒转 jiffies
usecs_to_jiffies(500)

// jiffies 转毫秒
jiffies_to_msecs(j)

// 秒转 jiffies
HZ * 2   // 2秒

时间比较

jiffies 会溢出,不能直接比大小,要用专门的宏:

c 复制代码
time_after(a, b)     // a 在 b 之后?
time_before(a, b)    // a 在 b 之前?
time_after_eq(a, b)
time_before_eq(a, b)

超时判断

c 复制代码
unsigned long timeout = jiffies + HZ;  // 1秒后超时

// 循环等待,超时退出
while (!is_ready()) {
    if (time_after(jiffies, timeout)) {
        return -ETIMEDOUT;
    }
}

二、低精度定时器(timer_list)

特点

  • 基于 jiffies,精度取决于 HZ(一般毫秒级)
  • 软中断上下文执行(到期后在时钟中断底半部处理)
  • 不能睡眠
  • 简单易用,大部分场景够用

定义

c 复制代码
#include <linux/timer.h>

struct timer_list my_timer;

初始化

c 复制代码
void timer_func(struct timer_list *t);

timer_setup(&my_timer, timer_func, 0);

老内核用 init_timer + .function 赋值,新内核用 timer_setup。

启动定时器

c 复制代码
// 从现在起 expires 个 jiffies 后到期
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));

删除定时器

c 复制代码
del_timer(&my_timer);
del_timer_sync(&my_timer);  // 等正在执行的处理函数完成(安全,推荐卸载时用)

处理函数

c 复制代码
void timer_func(struct timer_list *t)
{
    struct my_dev *dev = from_timer(dev, t, timer);  // 类似container_of
    
    // 处理定时任务
    do_something(dev);
    
    // 如果需要周期性触发,重新启动
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}

from_timer 宏

从 timer_list 指针拿到外层结构体,和 container_of 类似:

c 复制代码
from_timer(外层指针变量名, timer指针, 成员名)

完整示例:周期性定时器

c 复制代码
struct my_dev {
    struct timer_list timer;
    int count;
};

static void my_timer_func(struct timer_list *t)
{
    struct my_dev *dev = from_timer(dev, t, timer);
    
    dev->count++;
    pr_info("定时器触发,第%d次\n", dev->count);
    
    // 重新启动,实现周期性
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}

// 初始化
struct my_dev *dev;
timer_setup(&dev->timer, my_timer_func, 0);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));

// 卸载时
del_timer_sync(&dev->timer);

注意事项

  1. 定时器只触发一次,要周期性的话在处理函数里重新 mod_timer
  2. 软中断上下文,不能睡眠
  3. 精度不高:毫秒级,误差跟 HZ 和系统负载有关
  4. 删除要同步:卸载模块用 del_timer_sync,确保处理函数执行完了再删

三、高精度定时器(hrtimer)

特点

  • 高精度,纳秒级
  • 基于硬件时钟,不依赖 jiffies
  • 可以支持比 HZ 更高的精度
  • 适合对定时精度要求高的场景(音视频、PWM 模拟等)

时间单位:ktime_t

c 复制代码
#include <linux/ktime.h>

ktime_t t;

// 常用构造
ktime_set(sec, nsec)           // 秒+纳秒
ms_to_ktime(ms)                // 毫秒
ns_to_ktime(ns)                // 纳秒
ktime_get()                    // 当前时间

定义

c 复制代码
#include <linux/hrtimer.h>

struct hrtimer my_hrtimer;

初始化

c 复制代码
enum hrtimer_restart my_hrtimer_func(struct hrtimer *timer);

hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = my_hrtimer_func;

时钟类型:

  • CLOCK_MONOTONIC:单调时间,系统启动后一直递增(常用)
  • CLOCK_REALTIME:墙上时间,可以被修改

模式:

  • HRTIMER_MODE_REL:相对时间(从现在开始算)
  • HRTIMER_MODE_ABS:绝对时间

启动

c 复制代码
hrtimer_start(&my_hrtimer, ms_to_ktime(100), HRTIMER_MODE_REL);

取消

c 复制代码
hrtimer_cancel(&my_hrtimer);

处理函数

c 复制代码
enum hrtimer_restart my_hrtimer_func(struct hrtimer *timer)
{
    struct my_dev *dev = container_of(timer, struct my_dev, hrtimer);
    
    do_something(dev);
    
    // 重新启动(周期性)
    hrtimer_forward_now(timer, ms_to_ktime(100));
    return HRTIMER_RESTART;
    
    // 或者只触发一次
    // return HRTIMER_NORESTART;
}

返回值:

  • HRTIMER_RESTART:重新启动
  • HRTIMER_NORESTART:不重启

注意

  • 也是中断上下文(软中断),不能睡眠
  • 精度高,但系统开销也大一点
  • 普通驱动用 timer_list 就够了,精度要求高再用 hrtimer

四、延时函数

驱动里经常要等一小段时间,比如写寄存器后等硬件就绪。

1. 忙等待(忙循环)

CPU 一直等,不释放 CPU。中断上下文里只能用这个(因为不能睡眠)。

ndelay / udelay / mdelay
c 复制代码
// 纳秒级忙等
ndelay(100);   // 等100纳秒

// 微秒级忙等
udelay(50);    // 等50微秒

// 毫秒级忙等
mdelay(10);    // 等10毫秒

特点:

  • 忙等待,占着 CPU
  • 任何上下文都能用(包括中断)
  • 短延时(一般毫秒以内)用
  • 长时间忙等浪费 CPU,不推荐

2. 睡眠延时(让出 CPU)

进程上下文可以用,睡眠期间 CPU 干别的去了。

msleep
c 复制代码
msleep(100);  // 睡眠100毫秒
  • 至少睡这么久,可能更长(精度不高)
  • 进程上下文用
msleep_interruptible
c 复制代码
msleep_interruptible(100);

可以被信号打断的睡眠。

ssleep
c 复制代码
ssleep(2);  // 睡眠2秒

秒级的 msleep 封装。

3. 等待队列延时

配合等待队列,等事件或者超时:

c 复制代码
wait_event_interruptible_timeout(wq, condition, msecs_to_jiffies(1000));

条件满足了就返回,超时也返回。最常用的等待方式。

延时函数选型

函数 上下文 精度 CPU占用 适用
ndelay/udelay 任意 忙等占CPU 短延时(微秒级)、中断上下文
mdelay 任意 一般 忙等占CPU 短延时(毫秒级)、中断上下文
msleep 进程 释放CPU 长延时、不要求精确
usleep_range 进程 较高 释放CPU 微秒级睡眠延时
wait_event_timeout 进程 一般 释放CPU 等事件+超时

原则

  • 中断上下文 / 不能睡眠的场景:用 udelay/mdelay 忙等
  • 进程上下文、时间较长:用 msleep 睡眠,别忙等浪费 CPU
  • 等某个条件:用等待队列 + 超时,别自己循环查

五、超时检测模式

模式 1:忙等超时(短时间)

c 复制代码
unsigned long timeout = jiffies + msecs_to_jiffies(100);

while (!hw_ready()) {
    if (time_after(jiffies, timeout)) {
        return -ETIMEDOUT;
    }
    cpu_relax();  // 让出一点,减少总线压力
}

很短时间的等待用这个。

模式 2:睡眠等待超时(进程上下文)

c 复制代码
if (wait_event_interruptible_timeout(wait_q, ready_flag,
                                     msecs_to_jiffies(1000)) == 0) {
    return -ETIMEDOUT;  // 超时了
}

配合等待队列用,最常用。

模式 3:定时器 + 等待队列

异步超时,定时器到了设置超时标志,唤醒等待队列。


六、内核延时的精度问题

为什么 msleep 不准?

msleep 是基于 jiffies 的,至少睡你指定的时间,可能多睡一点:

  • 刚好错过一个 tick,就多睡一个 tick
  • 系统负载高,调度不及时

要高精度短延时用 usleep_range 或者 udelay。

HZ 越低精度越差

HZ=100 的内核,msleep(1) 可能睡 10ms。嵌入式系统一般 HZ=100 或 250。

hrtimer 精度高

真正需要高精度定时用 hrtimer,硬件支持的话能到微秒甚至纳秒级。


七、常见坑

坑 1:中断上下文用 msleep

msleep 会睡眠,中断上下文里用直接 panic。中断里要用 udelay/mdelay 忙等。

坑 2:长时间用 mdelay 忙等

mdelay(1000) 忙等 1 秒,CPU 被占死了,系统都卡了。长延时必须用 msleep(进程上下文)。

坑 3:jiffies 直接比较大小

jiffies 溢出后会回绕,直接 if (jiffies > timeout) 在溢出边界会错。要用 time_after/time_before 宏。

坑 4:定时器处理函数里睡眠

timer_list 和 hrtimer 都是软中断上下文,不能睡眠。要睡眠的活放 workqueue 里。

坑 5:卸载模块没删定时器

模块卸载了定时器还在,到期了访问已经释放的内存,直接 panic。卸载一定要 del_timer_sync 或 hrtimer_cancel。

坑 6:mod_timer 重复启动

定时器已经在运行也可以 mod_timer,会重新设置到期时间,不会叠加。


八、本篇小结

  • jiffies:内核滴答计数器,HZ 是每秒滴答数,低精度定时的基础
  • time_after / time_before:安全比较 jiffies,防止溢出错误
  • timer_list:低精度定时器,毫秒级,软中断上下文,不能睡眠,简单常用
  • hrtimer:高精度定时器,纳秒级,适合精度要求高的场景
  • 延时函数分两类:忙等(udelay/mdelay,任意上下文)和睡眠(msleep,进程上下文)
  • 原则:短时间/中断用忙等,长时间/进程用睡眠,等事件用等待队列+超时
  • 定时器处理函数不能睡眠,要睡眠的活放 workqueue
  • 卸载模块一定要删除定时器,防止悬空指针

下一篇讲并发与同步:自旋锁、信号量、互斥锁,驱动里怎么处理多核并发问题。

我是黒漂技术佬。

相关推荐
Luminous.1 小时前
51 单片机--day1
单片机·嵌入式硬件
风曦Kisaki1 小时前
# Linux实战笔记-HIS项目实战Day01:版本控制与Git基础
linux·笔记·git
三品吉他手会点灯2 小时前
嵌入式机器学习 - 学习笔记1.0.1 - 从 Alexa 到 TinyML:为什么机器学习开始跑进微控制器?
人工智能·笔记·嵌入式硬件·学习·机器学习
nuoxin1142 小时前
HR4988替代A4988-富利威
网络·人工智能·嵌入式硬件·fpga开发·dsp开发
KaifuZeng2 小时前
通信与接口协议面试十七、SFP光模块
单片机·嵌入式硬件·面试·通信与接口协议
jsons12 小时前
rocky8内网离线批量补丁(你之前搭建的架构,多台服务器首选,无订阅)
linux·运维·服务器
深圳尚鼎2 小时前
尚鼎防潮柜分析:三核EtherCAT实时控制MCU/DSP发展趋势
单片机·嵌入式硬件
阿拉斯攀登3 小时前
SPI 设备驱动开发
linux·驱动开发·嵌入式硬件·linux内核·嵌入式·linux驱动·spi
Dlrb12113 小时前
51单片机基础知识
单片机·嵌入式硬件·51单片机