本篇定位 :定时器从裸机"一个 mtimecmp"跳到"hrtimer + clocksource + tick + NOHZ"多层。本篇把"Linux 时间子系统"对你已知的增量讲透。读完能写 hrtimer 驱动、能配 NOHZ 省电、能解释 FreeRTOS tick 和 Linux tick 的差异。
FreeRTOS tick 是 Linux 时间子系统的简化版FreeRTOS tick:你配 mtimecmp,周期中断 →
xTaskIncrementTick→ 调度。Linux 时间子系统复杂得多:clocksource (时钟源)+ clock_event (时钟事件)+ tick (调度心跳)+ hrtimer (高精度定时器)+ NOHZ(tickless 省电)。核心增量:① 多时钟源抽象;② 高精度定时器(纳秒级,不是固定 tick);③ tickless 省电(空闲时停 tick)。
一、时间子系统全景
┌─────────────────────────────────────┐
│ 硬件时钟源(每个 SoC 多个) │
│ - RISC-V: rdtime 寄存器(高精度计数)│
│ - ARM: Generic Timer(物理计数器) │
│ - HPET / TSC(x86) │
└────────────┬────────────────────────┘
│
┌─────────▼──────────┐
│ clocksource │ ← 读时间(单调递增计数器)
│ (只读,提供 ns) │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ timekeeping │ ← 维护当前时间(墙上时间/单调时间)
└─────────┬──────────┘
│
┌─────────▼──────────┐ ┌──────────────────┐
│ tick 设备 │<───│ clock_event │ ← 触发下次中断
│ (周期/单次事件) │ │ (可编程中断源) │
└─────────┬──────────┘ └──────────────────┘
│
┌─────────▼──────────┐
│ tick: 调度心跳 │ ← 周期中断,Scheduler_tick
│ hrtimer: 高精度定时│ ← 红黑树,ns 级
└────────────────────┘
三个核心抽象
| 抽象 | 作用 | 硬件 |
|---|---|---|
| clocksource | 读时间(单调递增计数器) | RISC-V rdtime / ARM CNTVCT |
| clock_event_device | 触发中断(可编程) | RISC-V sbi_set_timer / ARM CNTV_TVAL |
| tick_device | 基于 clock_event,周期或单次 | 调度心跳 |
二、clocksource:读时间
2.1 clocksource 是什么
- 一个单调递增的计数器,提供"当前 ns"
- 只读,不触发中断
- RISC-V:
rdtime指令读timeCSR(你 \[05-中断系统 PLIC-CLINT-AIA] 学的 CLINT mtime)
2.2 RISC-V clocksource
c
// arch/riscv/kernel/time.c(简化)
static u64 riscv_clocksource_read(struct clocksource *cs) {
return get_cycles(); // 读 rdtime
}
static struct clocksource riscv_clocksource = {
.name = "riscv_clocksource",
.rating = 300,
.read = riscv_clocksource_read,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
2.3 多 clocksource 与 rating
- 一个 SoC 可能有多个时钟源(TSC + HPET / rdtime + 外部 RTC)
- 每个 clocksource 有
rating(质量分),内核选最高的 - 运行时可切换(若更高 rating 的可用)
2.4 timekeeping:维护墙上时间
- 基于 clocksource,维护
xtime(墙上时间,UTC) - 和
monotonic(自启动单调时间) - NTP 调整频率
bash
# 看当前 clocksource
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# arch_sys_counter (RISC-V rdtime)
# 可用 clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
三、clock_event_device:触发中断
3.1 是什么
- 可编程中断源:设"下次多久后中断"
- 用于 tick(周期)或 hrtimer(单次精确)
- RISC-V:SBI 调用设定时器(
sbi_set_timer),底层写 mtimecmp
3.2 RISC-V timer 驱动
c
// drivers/clocksource/timer-riscv.c(简化)
static int riscv_timer_set_next_event(unsigned long delta,
struct clock_event_device *ce) {
// 设下次中断 = 当前 rdtime + delta
sbi_set_timer(get_cycles() + delta);
return 0;
}
static struct clock_event_device riscv_clock_event = {
.name = "riscv_timer_clockevent",
.features = CLOCK_EVT_FEAT_ONESHOT, // 单次模式
.rating = 100,
.set_next_event = riscv_timer_set_next_event,
};
3.3 对照你 RISC-V mtimecmp
| 你裸机 | Linux RISC-V timer |
|---|---|
| 写 mtimecmp = mtime + delta | sbi_set_timer(rdtime + delta) |
| 中断 → 你 handler | 中断 → hrtimer/tick 框架 |
| 直接寄存器 | 经 SBI(因为 Linux 在 S 态,mtimecmp 在 M 态) |
Linux RISC-V timer 走 SBI
你裸机在 M 态直接写 mtimecmp。Linux 在 S 态,mtimecmp 是 M 态寄存器,不能直接写------经 SBI 调用(ecall to M),OpenSBI 替你写 mtimecmp。这是RISC-V 专题(S 态不能直接碰 M)在时间子系统的体现。
四、tick:调度心跳
4.1 tick 是什么
- 周期性中断(默认 100/250/1000 Hz)
- 每次 tick:① 更新时间统计;②
scheduler_tick(检查时间片/调度);③ 处理过期定时器
4.2 tick 频率
bash
# 看 CONFIG_HZ
cat /proc/interrupts | grep LOC # LOC 是 local timer(tick)
# 数每秒增长 = HZ
| HZ | 周期 | 用途 |
|---|---|---|
| 100 | 10ms | 服务器(吞吐,少切换) |
| 250 | 4ms | 桌面(默认) |
| 1000 | 1ms | 嵌入式/低延迟 |
| 300 | ~3.3ms | 某些 |
4.3 scheduler_tick
每次 tick(本核):
- 更新当前进程时间统计(utime/stime)
- 更新 CFS vruntime
- 检查时间片到没 → 设 need_resched
- 触发 RCU 回收检查
4.4 对照 FreeRTOS tick
| FreeRTOS tick | Linux tick | |
|---|---|---|
| 触发 | mtimecmp 周期中断 | clock_event 周期中断 |
| 做什么 | xTaskIncrementTick + 调度 | scheduler_tick + timekeeping |
| 频率 | configTICK_RATE_HZ(常 1000) | HZ(100~1000) |
| 时间片 | 优先级 + 抢占 | CFS vruntime |
| 你配 | port.c 配 mtimecmp | 内核自动(你选 HZ) |
嵌入式视角:你 port 层配的就是 Linux tick 底层
FreeRTOS V11.1 port 层配 mtimecmp 周期中断 → Linux RISC-V 的 clock_event 设 mtimecmp(经 SBI)→ 同一硬件机制。增量:Linux 在上面建了 clocksource/timekeeping/hrtimer 多层;FreeRTOS 直接 tick 调度。你的 port 层经验直接迁移。
五、hrtimer:高精度定时器 ⭐
5.1 为什么需要 hrtimer
- 传统定时器(timer_list)基于 tick,精度 = tick 周期(10ms/4ms/1ms)
- 有些场景要 ns 级精度(音视频/网络/实时)
- hrtimer(High-Resolution Timer)独立于 tick,基于 clocksource,ns 级
5.2 hrtimer 数据结构
- 每 CPU 一个 hrtimer 红黑树(按到期时间排序)
- 最左节点 = 最近到期
- clock_event 设单次中断,到点触发 → 遍历过期 hrtimer → 回调
5.3 hrtimer 用法
c
struct hrtimer my_timer;
// 回调
enum hrtimer_restart my_callback(struct hrtimer *t) {
printk("Timer fired!\n");
// 若要重复,return HRTIMER_RESTART 并重新 forward
return HRTIMER_NORESTART;
}
// 初始化 + 启动
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_timer.function = my_callback;
hrtimer_start(&my_timer, ms_to_ktime(100), HRTIMER_MODE_REL); // 100ms 后
// 取消
hrtimer_cancel(&my_timer);
5.4 周期 hrtimer
c
enum hrtimer_restart my_periodic(struct hrtimer *t) {
// 重新设下次
hrtimer_forward_now(t, ms_to_ktime(100)); // 再 100ms
do_work();
return HRTIMER_RESTART;
}
5.5 hrtimer 回调的约束
- 在硬中断上下文(或 softirq,看模式)
- 不能睡
- 要快(影响精度)
- 要睡用 hrtimer 的 softirq 模式或 workqueue 推迟
!important hrtimer 是你写定时驱动的首选
你裸机用 mtimecmp 做周期任务。Linux 用 hrtimer ------ns 级精度,红黑树管理,API 清晰。写驱动要周期任务(轮询/超时),优先 hrtimer。要睡的活从回调里 schedule_work 推迟。
六、timer_list(低精度定时器)
6.1 老接口,简单但精度低
c
struct timer_list my_timer;
timer_setup(&my_timer, my_fn, 0);
my_timer.expires = jiffies + msecs_to_jiffies(100); // 100ms 后(对齐 tick)
add_timer(&my_timer);
// 或
mod_timer(&my_timer, jiffies + HZ); // 改到期
del_timer(&my_timer);
- 基于 jiffies(tick 计数),精度 = tick
- 新代码优先 hrtimer,老代码常用 timer_list
七、jiffies vs ktime
7.1 jiffies
- 内核启动以来的 tick 数
HZ次/秒,即jiffies / HZ= 秒- 简单但精度低(tick 级)
c
unsigned long timeout = jiffies + HZ; // 1 秒后
if (time_after(jiffies, timeout)) { ... } // 超时
7.2 ktime_t
- 64 位 ns 级时间
- hrtimer 用
c
ktime_t kt = ktime_get(); // 当前单调 ns
ktime_t t = ms_to_ktime(100); // 100ms 转 ktime
s64 ns = ktime_to_ns(kt);
u64 us = ktime_get_ns() / 1000; // 微秒
7.3 选哪个
| 场景 | 用 |
|---|---|
| tick 级够(timer_list/超时判断) | jiffies |
| ns 级(hrtimer/精确测量) | ktime |
| 墙上时间(用户态) | gettimeofday/clock_gettime |
八、延时:忙等 vs 睡眠
8.1 忙等(短延时,不能睡时用)
c
udelay(10); // 10 微秒忙等
mdelay(5); // 5 毫秒忙等(不要长,占 CPU)
ndelay(100); // 100 纳秒
- 基于 CPU 自旋计数,不准但简单
- 中断上下文/持锁时用(不能睡)
- 别长(浪费 CPU)
8.2 睡眠(可睡时用)
c
msleep(100); // 100ms 睡(精度 tick)
msleep_interruptible(100); // 可被信号打断
usleep_range(100, 200); // 100-200us 睡(更省电,允许调度器合并)
schedule_timeout(timeout); // 通用
- 让出 CPU(不忙等)
- 精度取决于调度(tick 级,usleep_range 稍好)
- 进程上下文用
8.3 决策
能睡?
是 ──> msleep / usleep_range / schedule_timeout
否(中断/持锁) ──> udelay / mdelay(短)
中断里只能 udelay,不能 msleep
\[05-中断与下半部] 学的"中断不能睡"------中断里要延时只能
udelay(忙等)。msleep 会 schedule,中断里 schedule = panic。持自旋锁同理只能 udelay。
九、NOHZ(tickless,省电)⭐
9.1 为什么 NOHZ
- tick 每 HZ 次中断,即使 CPU 空闲也打断
- 空闲时 tick 浪费电(尤其嵌入式/移动)
- NOHZ:CPU 空闲时停 tick,直到下次 hrtimer 或中断
9.2 NOHZ_IDLE(空闲 tickless)
- CPU 进 idle 时关 tick
- 下次中断(若有 hrtimer/IO)才唤醒
- 省电,嵌入式必开
9.3 NOHZ_FULL(全 tickless)
- 即使 CPU 跑任务也不 tick(除非该 CPU 只有任务)
- 用于实时/HPC(减少 tick 干扰)
- 需至少一个 CPU 保留做 timekeeping(dedicated housekeeping CPU)
9.4 配置
bash
# 看 NOHZ
cat /proc/cmdline | grep nohz
# nohz=on / nohz_full=1-3(CPU1-3 全 tickless,CPU0 做 housekeeping)
# 看是否启用
cat /sys/devices/system/cpu/vulnerabilities/... # 不相关,看 NOHZ:
zcat /proc/config.gz | grep NOHZ
# CONFIG_NO_HZ_IDLE=y / CONFIG_NO_HZ_FULL=y
9.5 对照 FreeRTOS tickless
| FreeRTOS tickless | Linux NOHZ_IDLE | |
|---|---|---|
| 空闲时 | configUSE_TICKLESS_IDLE | NOHZ_IDLE |
| 停 tick | portSUPPRESS_TICKS_AND_SLEEP | 内核自动 |
| 配 mtimecmp 重设 | 你 port 层实现 | clock_event 自动 |
| 省电 | 是 | 是 |
嵌入式视角:你 FreeRTOS tickless 就是 Linux NOHZ
FreeRTOS tickless idle(port 层
portSUPPRESS_TICKS_AND_SLEEP)------停 tick,设下次 mtimecmp,进 WFI。Linux NOHZ_IDLE 同构 ------idle 时停 tick,设下次 clock_event,进 idle(WFI)。你 port 层实现的 tickless,就是 Linux NOHZ 的简化版。增量:Linux 多了 NOHZ_FULL(任务也停 tick)。
十、用户态时间 API
10.1 时间获取
c
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); // 单调时间(自启动)
clock_gettime(CLOCK_REALTIME, &ts); // 墙上时间(可被 NTP 调)
gettimeofday(&tv, NULL); // 墙上时间(老接口)
// 高精度
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
- CLOCK_MONOTONIC:单调(不回退),适合测耗时
- CLOCK_REALTIME:墙上(可被改),适合时间戳
10.2 VDSO 加速
gettimeofday/clock_gettime(CLOCK_MONOTONIC|REALTIME) 走 VDSO(06 篇),不进内核。
10.3 命令行
bash
date # 墙上时间
uptime # 启动以来
time ./cmd # 测耗时
十一、延时与定时速查
| 需求 | API | 上下文 |
|---|---|---|
| ns 级定时器 | hrtimer_start | 回调在硬中断 |
| tick 级定时器 | timer_list / add_timer | 回调在 softirq |
| us 短延时 | udelay | 任意(忙等) |
| ms 短延时 | mdelay | 任意(忙等,别长) |
| ms 睡眠 | msleep | 进程 |
| us 睡眠 | usleep_range | 进程 |
| 超时等待 | schedule_timeout | 进程 |
| 用户态时间 | clock_gettime | 用户 |
| 当前 ns | ktime_get | 内核 |
| 当前 jiffies | jiffies | 内核 |
十二、对照总表
| 概念 | FreeRTOS | RISC-V bare-metal | Linux |
|---|---|---|---|
| 时钟源 | mtime | rdtime/mtime | clocksource(rdtime) |
| 周期中断 | tick(mtimecmp) | 你配 mtimecmp | tick(clock_event) |
| 高精度定时 | 软定时器(基于 tick) | 你写 | hrtimer(ns 级) |
| tickless | tickless idle | 你写 WFI | NOHZ_IDLE/FULL |
| 延时忙等 | N/A | 你写 | udelay/mdelay |
| 延时睡眠 | vTaskDelay | N/A | msleep/usleep_range |
| 时间表示 | tick count | rdtime 值 | jiffies / ktime |
| 设置定时器 | port 层 | 写 mtimecmp | SBI 调用(经 M) |
十三、本篇小结
- 时间子系统三抽象:clocksource (读时间)+ clock_event (触发中断)+ tick_device(周期/单次)
- RISC-V:clocksource 用 rdtime,clock_event 经 SBI 写 mtimecmp(Linux 在 S 态,不能直接写 M 态)
- tick :周期中断,
scheduler_tick驱动调度;HZ=100~1000 - hrtimer:ns 级高精度定时器,红黑树,新代码首选(回调在硬中断,不能睡)
- timer_list:tick 级低精度,老接口
- jiffies(tick 计数)vs ktime(ns),按精度选
- 延时:udelay/mdelay 忙等(中断/持锁用),msleep/usleep_range 睡眠(进程用)
- NOHZ:空闲停 tick 省电(NOHZ_IDLE),全 tickless(NOHZ_FULL,实时/HPC)
- 用户态 clock_gettime 走 VDSO,不进内核
- FreeRTOS tickless idle = Linux NOHZ_IDLE 同构
速查表
| 想干啥 | 用什么 |
|---|---|
| 读当前 ns | ktime_get_ns() |
| 读当前 jiffies | jiffies |
| 100ms 后的 jiffies | jiffies + msecs_to_jiffies(100) |
| ns 级定时器 | hrtimer_start(t, ns, MODE_REL) |
| tick 级定时器 | timer_setup + add_timer |
| 中断里延时 | udelay(us) |
| 进程里睡眠 | msleep(ms) / usleep_range(min,max) |
| 超时等待 | schedule_timeout(jiffies) |
| 看当前 clocksource | cat /sys/devices/system/clocksource/clocksource0/current_clocksource |
| 看 HZ | zcat /proc/config.gz | grep CONFIG_HZ |
| 配 NOHZ | cmdline nohz=on / nohz_full=CPUs |
| 用户态测耗时 | clock_gettime(CLOCK_MONOTONIC) |
| 看定时器 | cat /proc/timer_list |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。