定时器与延时:内核定时器、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);
注意事项
- 定时器只触发一次,要周期性的话在处理函数里重新 mod_timer
- 软中断上下文,不能睡眠
- 精度不高:毫秒级,误差跟 HZ 和系统负载有关
- 删除要同步:卸载模块用 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
- 卸载模块一定要删除定时器,防止悬空指针
下一篇讲并发与同步:自旋锁、信号量、互斥锁,驱动里怎么处理多核并发问题。
我是黒漂技术佬。