从0开始理解Linux时间管理:jiffies到hrtimer的演进
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/
前言:内核中的时间是什么?
在用户空间编程时,我们习惯了用sleep()、usleep()这样的函数来等待,或者用gettimeofday()来获取当前时间。但在内核里,这些都不能用。
为什么?
因为内核是整个系统的管理者,它需要更高精度、更低开销的时间管理机制。而且,内核中的时间管理涉及到底层的硬件定时器、调度器、中断处理,比用户空间复杂得多。
老实说,我刚开始写内核驱动的时候,对时间管理一窍不通。我以为直接调用个delay()就行,结果编译都过不了。后来我花了很长时间才理解:内核中的时间管理是一套完整的体系,从硬件定时器到软件定时器,层层抽象。
这一节,我们从最基础的jiffies开始,逐步理解内核是如何管理时间的。
环境:基于Linux 7.0-rc4
| 项目 | 版本/信息 |
|---|---|
| 内核版本 | Linux 7.0-rc4 (主线内核) |
| 架构 | ARMv7-A (Cortex-A7) |
| 相关头文件 | include/linux/jiffies.h,include/linux/timer.h |
jiffies:内核的心跳
内核中有一个全局变量,叫jiffies。它记录了系统启动以来经过的「滴答数」。
你可以把它理解为内核的「心跳计数器」。每隔一段时间(这个间隔由HZ决定),硬件定时器会产生一个中断,jiffies就会加1。
HZ:每秒的滴答数
HZ是一个宏定义,表示每秒有多少次滴答。在我们的ARM Linux系统中,通常HZ=100或HZ=200。
/* 常见的HZ值 */#defineHZ100/* 每秒100次滴答,即每10ms一次 */#defineHZ200/* 每秒200次滴答,即每5ms一次 */#defineHZ1000/* 每秒1000次滴答,即每1ms一次 */为什么不能无限提高HZ?
HZ越高,时间精度越高,但开销也越大:
- 更多的定时器中断
- 更频繁的调度器运行
- 更多的功耗
jiffies的精度限制
由于jiffies是基于滴答数的,它的精度受限于HZ值。
| HZ | 滴答间隔 | 精度 |
|---|---|---|
| 100 | 10ms | 10ms |
| 200 | 5ms | 5ms |
| 1000 | 1ms | 1ms |
如果你的应用需要微秒级的精度,jiffies就不够用了。这时候需要用高精度定时器(hrtimer)。
时间单位转换
内核提供了一组宏来转换时间单位:
#include<linux/jiffies.h>/* 毫秒转jiffies */unsignedlongj=msecs_to_jiffies(100);/* 100ms *//* 秒转jiffies */j=msecs_to_jiffies(1000);/* 1秒 *//* 微秒转jiffies(注意精度损失) */j=usecs_to_jiffies(1000);/* 1ms *//* jiffies转毫秒(用于打印) */unsignedintms=jiffies_to_msecs(j);jiffies的回绕问题
jiffies是一个32位或64位的无符号整数。当它达到最大值后,会回绕到0。
32位jiffies: 0xFFFFFFFF → 0x00000000 大约每49.7天回绕一次(HZ=100时)这就是问题:如果你直接比较两个jiffies值,可能会在回绕时出错。
/* ❌ 错误的比较方式 */if(timeout<jiffies){/* 在回绕时,这个判断会出错! */}/* ✓ 正确的比较方式 */if(time_after(jiffies,timeout)){/* 这个宏正确处理了回绕 */}if(time_before(jiffies,timeout)){/* 同样正确 */}时间比较宏:
| 宏 | 描述 |
|---|---|
time_after(a, b) | a > b(正确处理回绕) |
time_before(a, b) | a < b(正确处理回绕) |
time_after_eq(a, b) | a >= b |
time_before_eq(a, b) | a <= b |
内核定时器:在指定时间执行回调
内核定时器是内核中最常用的延时执行机制。它允许你在指定的时间后执行一个回调函数。
定时器的结构
在Linux 7.0中,定时器用timer_list结构体表示:
structtimer_list{/* 内部字段 */structhlist_nodeentry;unsignedlongexpires;void(*function)(structtimer_list*timer);u32 flags;/* ... */};Linux 7.0的重大变化
⚠️ 重要:如果你有旧的4.x内核代码,需要注意以下变化:
init_timer()已废弃,必须使用timer_setup()data字段已移除,必须使用from_timer()宏获取包含结构体del_timer_sync()被timer_delete_sync()替代(虽然旧名字仍可用)
新的定时器API
| 函数 | 描述 |
|---|---|
timer_setup(timer, callback, flags) | 初始化定时器 |
void (*callback)(struct timer_list *) | 定时器回调函数签名 |
add_timer(timer) | 激活定时器 |
mod_timer(timer, expires) | 修改定时器的过期时间 |
timer_delete(timer) | 删除定时器(可能返回还在运行) |
timer_delete_sync(timer) | 删除定时器并等待回调完成 |
timer_pending(timer) | 检测定时器是否 pending |
定时器标志
#defineTIMER_DEFERRABLE0x00080000/* 可延迟定时器 */#defineTIMER_PINNED0x00100000/* 固定在当前CPU */#defineTIMER_IRQSAFE0x00200000/* 中断安全定时器 */- TIMER_DEFERRABLE:系统空闲时不会唤醒CPU
- TIMER_PINNED:定时器总是在指定的CPU上执行
- TIMER_IRQSAFE:回调在中断关闭的情况下执行
定时器示例
#include<linux/timer.h>#include<linux/jiffies.h>structmy_device{structtimer_listtimer;intcounter;/* ... 其他成员 ... */};/* 定时器回调函数 */staticvoidmy_timer_callback(structtimer_list*timer){/* 使用container_of或from_timer获取包含结构体 */structmy_device*dev=from_timer(dev,timer,timer);dev->counter++;pr_info("Timer fired, counter = %d\n",dev->counter);/* 如果需要周期性执行,重新设置定时器 */mod_timer(&dev->timer,jiffies+msecs_to_jiffies(1000));}/* 初始化定时器 */staticintmy_device_init(structmy_device*dev){/* ✓ Linux 7.0的正确方式 */timer_setup(&dev->timer,my_timer_callback,0);/* 设置1秒后过期 */dev->timer.expires=jiffies+msecs_to_jiffies(1000);/* 激活定时器 */add_timer(&dev->timer);return0;}/* 清理定时器 */staticvoidmy_device_cleanup(structmy_device*dev){/* 删除定时器并等待回调完成 */timer_delete_sync(&dev->timer);}旧API vs 新API对比
/* ❌ 旧API(Linux 4.14之前) */structtimer_listmy_timer;voidcallback(unsignedlongdata){structmy_device*dev=(structmy_device*)data;/* ... */}init_timer(&my_timer);my_timer.data=(unsignedlong)dev;my_timer.function=callback;my_timer.expires=jiffies+msecs_to_jiffies(1000);add_timer(&my_timer);/* ✓ 新API(Linux 4.14+,包括7.0) */structtimer_listmy_timer;voidcallback(structtimer_list*timer){structmy_device*dev=from_timer(dev,timer,timer);/* ... */}timer_setup(&my_timer,callback,0);my_timer.expires=jiffies+msecs_to_jiffies(1000);add_timer(&my_timer);高精度定时器:hrtimer
当你需要微秒甚至纳秒级的精度时,jiffies和timer_list就不够用了。这时候需要用高精度定时器(hrtimer)。
hrtimer的特点
- 纳秒级精度
- 基于高精度硬件时钟(如ARM的定时器)
- 不依赖HZ配置
hrtimer API(简介)
#include<linux/hrtimer.h>enumhrtimer_restartcallback(structhrtimer*timer);structhrtimer{/* ... */};/* 初始化hrtimer */voidhrtimer_init(structhrtimer*timer,clockid_tclock_id,enumhrtimer_modemode);/* 启动hrtimer */voidhrtimer_start(structhrtimer*timer,ktime_ttime,constenumhrtimer_modemode);/* 取消hrtimer */inthrtimer_cancel(structhrtimer*timer);clock_id选项:
CLOCK_MONOTONIC:单调时钟,不受系统时间调整影响CLOCK_REALTIME:实时时钟,可能被NTP调整CLOCK_BOOTTIME:包含休眠时间的单调时钟
⚠️ 注意:hrtimer的使用比较复杂,通常驱动代码用timer_list就够了。只有在需要极高精度时才考虑hrtimer。
短延迟:忙等待
有时候,你需要极短的延迟(比如微秒级),而且不需要很精确。内核提供了一些忙等待函数:
#include<linux/delay.h>/* 忙等待指定的纳秒/微秒/毫秒数 */voidndelay(unsignedlongnsecs);/* 纳秒延迟 */voidudelay(unsignedlongusecs);/* 微秒延迟 */voidmdelay(unsignedlongmsecs);/* 毫秒延迟 */⚠️ 注意:
- 这些函数会占用CPU,是忙等待
- 只在极短延迟时使用(通常小于1ms)
- 较长延迟应该用定时器或睡眠函数
睡眠延迟:让出CPU
如果你的延迟时间较长(毫秒级或更长),应该让出CPU,让其他进程运行。
可中断睡眠
#include<linux/delay.h>/* 睡眠指定毫秒数(可被信号中断) */voidmsleep(unsignedintmsecs);/* 睡眠指定毫秒数(不可中断) */voidmsleep_interruptible(unsignedintmsecs);/* 睡眠指定微秒数(上限2000us) */voidusleep_range(unsignedlongmin,unsignedlongmax);usleep_range:推荐的选择
usleep_range()是现代内核推荐的高精度睡眠函数:
/* 睡眠100-150微秒 */usleep_range(100,150);它给出一个范围,让调度器可以在范围内选择最佳唤醒时间,有助于省电和减少调度开销。
等待事件
有时候,你需要等待某个条件成立,而不是固定的延迟。这时候用等待队列:
#include<linux/wait.h>/* 等待条件成立,超时时间为jiffies */wait_event_timeout(wait_queue_head_twq,condition,timeout);/* 可中断版本 */wait_event_interruptible_timeout(wq,condition,timeout);实战:在驱动中使用定时器
让我们用一个实际例子来总结这些时间管理机制。
场景:LED闪烁驱动
#include<linux/module.h>#include<linux/timer.h>#include<linux/gpio/consumer.h>#include<linux/platform_device.h>structblink_led{structgpio_desc*gpio;structtimer_listtimer;bool led_on;unsignedlonginterval_ms;};staticvoidblink_timer_callback(structtimer_list*timer){structblink_led*bled=from_timer(bled,timer,timer);/* 切换LED状态 */bled->led_on=!bled->led_on;gpiod_set_value(bled->gpio,bled->led_on);/* 重新设置定时器 */mod_timer(&bled->timer,jiffies+msecs_to_jiffies(bled->interval_ms));}staticintblink_led_probe(structplatform_device*pdev){structblink_led*bled;bled=devm_kzalloc(&pdev->dev,sizeof(*bled),GFP_KERNEL);if(!bled)return-ENOMEM;/* 获取GPIO */bled->gpio=devm_gpiod_get(&pdev->dev,NULL,GPIOD_OUT_LOW);if(IS_ERR(bled->gpio))returnPTR_ERR(bled->gpio);/* 初始化定时器 */bled->interval_ms=500;/* 500ms闪烁 */bled->led_on=false;timer_setup(&bled->timer,blink_timer_callback,0);/* 启动定时器 */bled->timer.expires=jiffies+msecs_to_jiffies(bled->interval_ms);add_timer(&bled->timer);platform_set_drvdata(pdev,bled);pr_info("Blink LED driver loaded\n");return0;}staticintblink_led_remove(structplatform_device*pdev){structblink_led*bled=platform_get_drvdata(pdev);/* 删除定时器 */timer_delete_sync(&bled->timer);pr_info("Blink LED driver unloaded\n");return0;}/* ... platform_driver定义 ... */时间管理决策树
需要延时/定时? ├─ 极短延迟(<10微秒) │ └─ 用 ndelay/udelay(忙等待) ├─ 短延迟(10us - 1ms) │ └─ 用 usleep_range(睡眠) ├─ 中等延迟(1ms - 1秒) │ └─ 用 msleep 或 timer_list ├─ 长延迟(>1秒) │ └─ 用 timer_list 或 wait_queue └─ 高精度需求(<1us) └─ 用 hrtimer这一小节就到这里
Linux内核的时间管理是一套完整的体系,从基于滴答的jiffies,到高精度的hrtimer,满足不同场景的需求。
对于大多数驱动代码:
- 短延迟用
usleep_range() - 定时任务用
timer_list - 极高精度才用
hrtimer
下一节,我们会在实战中深入使用定时器,写一个完整的定时器驱动示例。
本章要点
- jiffies是内核的心跳计数器,精度受HZ限制(通常5-10ms)。
- 时间比较必须用宏(
time_after等),因为jiffies会回绕。 - Linux 7.0使用新的timer API:
timer_setup()替代init_timer(),from_timer()替代data字段。 timer_delete_sync()替代del_timer_sync(),确保回调完成后才返回。- 忙等待(udelay)用于极短延迟,睡眠(usleep_range/msleep)用于较长延迟。
- hrtimer提供纳秒级精度,但使用复杂,大多数驱动用timer_list足够。
相关阅读
- 嵌入式Linux嵌入式Linux驱动开发:设备树驱动改造——从硬编码到设备树的实战之旅 - 相似度 100%
- 嵌入式Linux驱动开发pinctrl篇(1)——从寄存器到子系统:驱动演进之路 - 相似度 100%
- 通用GUI编程技术——图形渲染实战(四十五)——D3D12资源与堆管理:从上传到驻留 - 相似度 100%