Linux 内核时间体系入门教程——从 CONFIG_HZ 到 hrtimer
读者对象:刚接触 Linux 内核的嵌入式开发者。读这份文档不需要任何内核时间子系统的前置知识,需要的预备知识已全部放在第 1 章。
环境背景:本文所有代码、结论、实验均基于 RK3588 + Linux 5.10.198(android12-5.10 分支)+ busybox 用户态,
rockchip_linux_defconfig(CONFIG_HZ=300)。其他平台/版本结论基本通用,个别数值会不同。如何读:新手按章节顺序读;有基础的可以直接跳第 4、6 章。每章开头有一句话导读。
第 1 章 预备知识
导读:这一章补齐理解后文必需的四个概念。已经熟悉的读者可以快速跳过,但建议至少扫一遍 1.2 的"上下文"概念,它是理解第 6 章的钥匙。
1.1 用户态与内核态:CPU 的两种"身份"
CPU 有特权级之分(ARM 上叫 EL0/EL1,x86 上叫 Ring3/Ring0):
- 用户态:你的应用程序跑的地方。权限受限,不能直接碰硬件、不能直接读别的进程的内存。
- 内核态:操作系统内核跑的地方。权限全开,直接操作硬件。
应用程序想做事(读文件、发网络包、睡觉),必须"申请"内核代劳,这个动作叫系统调用(syscall):
你的应用 内核 │ write(fd, buf, n) │ │ ──────── syscall ────→ │ ext4 文件系统 → 块层 → eMMC 驱动 │ ←────── 返回 ────────── │这个区分直接引出后文的 utime/stime(第 5 章):CPU 跑你的代码的时间记 utime,CPU 在内核里替你干活的时间记 stime。
1.2 中断:内核的"门铃"
想象两个查快递的方式:
- 轮询:每 5 分钟跑下楼看一次有没有快递。简单,但大部分白跑。
- 中断:在家干活,快递员按门铃你才下去。高效,事件驱动。
硬件设备(网卡、触摸屏、定时器)都用中断通知 CPU。中断处理分两级:
- 硬中断(hardirq):门铃响了先冲下去签个字——必须快,会打断当前正在跑的一切代码,期间不能睡眠、不能再被普通代码打断。
- 软中断(softirq):签字后发现的复杂事情(拆包裹、整理发票)记到小本本上,等"稍后安全的时候"再处理。这个"稍后"就是软中断上下文。
关键概念"上下文(context)"——指"当前这段代码是以什么身份在跑":
| 上下文 | 谁触发的 | 能否睡眠 |
|---|---|---|
| 进程上下文 | 系统调用、内核线程 | 可以睡 |
| 硬中断上下文 | 硬件中断 | 绝对不能 |
| 软中断上下文 | 硬中断"记账"延后的活 | 不能 |
"睡眠"指主动让出 CPU 等待某个条件(比如等 100ms)。理解了这张表,第 6 章"回调跑在哪个上下文"的讨论就不再是天书。
1.3 定时器与"闹钟"
- 硬件定时器:芯片里真实存在的电路,能"数数",也能"到点打铃"(产生中断)。就像一个自带闹钟功能的电子表。
- 软件定时器:内核里登记的"备忘录"——“到 X 时刻请帮我执行函数 F”。它本身不是硬件,到期还是要靠硬件闹钟提醒。
记住这句话,第 6 章会反复用到:内核里所有定时器,最终都靠同一颗硬件闹钟提醒;"软件"只是登记和回调的代码。
1.4 Kconfig 与 defconfig:内核的配置系统
内核有几千个功能开关,全部以CONFIG_开头:
CONFIG_HZ_300=y # 打开"300Hz 选项" # CONFIG_HZ_1000 is not set # 关闭"1000Hz 选项" CONFIG_HIGH_RES_TIMERS=y # 打开高精度定时器支持这些开关定义在Kconfig文件里(kernel/Kconfig.hz、kernel/time/Kconfig等),编译前由make xxx_defconfig生成最终的.config。改配置 → 重新编译 → 烧写内核,是内核开发的基本循环。
第 2 章 一切的源头:ARM 通用定时器
导读:先认识硬件,软件的一切都是围绕它搭建的。
RK3588 用的是ARM 通用定时器(arch_timer):全芯片共享一个系统计数器,每个 CPU 核各带一个自己的比较器(这正是第 7 章"hrtimer 柜台每个 CPU 一套"的硬件根基)。它有两个角色:
- 计数器(counter):一个硬件电路在永不停止地数数,纳秒级步进,不可写、不可清零。相当于一块一直在走的表。
- 比较器(comparator):可编程的闹钟——设一个目标值,计数器数到它,就产生一个中断。
在内核里,这两个功能有两个专业名字:
| 硬件功能 | 内核术语 | 用途 |
|---|---|---|
| 计数器(读数) | clocksource(时钟源) | 回答"现在几点了" |
| 比较器(打铃) | clockevent(时钟事件设备) | 回答"到时候叫我" |
后面所有内容——jiffies、hrtimer、tick——都是这两个角色的不同"租户"。
第 3 章 两套时间世界
导读:内核的时间管理分成粗、细两套体系。理解这张图,后面的章节都只是展开它。
硬件:ARM Generic Timer (arch_timer) ┌───────────────────────────────────────────┐ │ ① 免跑计数器 (counter):硬件一直在数 │ │ ② 比较器 (comparator):到点能打中断 │ └──────┬───────────────────────┬────────────┘ 读①│ 配②│ ▼ ▼ clocksource clockevent (oneshot) │ │ ▼ ├────────────────┐ timekeeper(时间管家) ▼ ▼ 管墙钟/单调时间, ns tick hrtimer │ jiffies_64++ (纳秒, 红黑树) │ 跑到期的 timer │ ▼ scheduler tick ├─ usleep_range() ┌───────────────────┐ (HZ=300 → 3.33ms) ├─ clock_nanosleep │ ktime_get() │ ├─ poll/epoll/select 超时 │ ktime_get_real_* │ ← /proc/uptime └─ POSIX timers │ ktime_get_boottime│ ← printk [ 123.456789 ] │ ktime_get_raw │ ← clock_gettime() └───────────────────┘ ══ 粗粒度世界 ══ ══ 细粒度世界 ══ timer wheel (timer.c) hrtimer (hrtimer.c) mod_timer / msleep hrtimer_start / usleep_range 粒度 = 1 拍 = 3.33ms 精度 = 纳秒 └─── 汇率换算: msecs_to_jiffies() ───┘逐块解读:
- 左边一条线:读计数器① → timekeeper 算出"现在是几点"(纳秒级)→ 提供给用户态的
clock_gettime()、/proc/uptime等。 - 右边一条线:编程比较器②让它到点打中断 → 中断里既喂"心跳"(jiffies++)也喂"高精度闹钟"(hrtimer)。
- 两个世界由同一颗硬件喂,所以不会各走各的漂移。
3.1 粗粒度世界:心跳计数器 jiffies
jiffies 是什么:内核的全局心跳计数器。定时器硬件每打一次铃(称为一次tick,“一拍”),它就 +1。它记录的是"开机以来总共跳了多少拍",单位是拍,不是秒。
它朴素得惊人,总共三段代码:
/* include/linux/jiffies.h —— 声明 */externu64 __cacheline_aligned_in_smp jiffies_64;externunsignedlongvolatile__cacheline_aligned_in_smp __jiffy_arch_data jiffies;/* kernel/time/timer.c —— 定义 */__visible u64 jiffies_64 __cacheline_aligned_in_smp=INITIAL_JIFFIES;/* kernel/time/timekeeping.c —— 每次时钟中断自增 */voiddo_timer(unsignedlongticks){jiffies_64+=ticks;/* 一次 tick,加 1 */calc_global_load();}三个要点:
jiffies不是另一个独立变量:链接脚本把jiffies符号直接指到jiffies_64的地址上。32 位平台上unsigned long只有 32 位,jiffies恰好盖住jiffies_64的低 32 位;在 arm64 这样的 64 位平台上,两者就是同一个 64 位变量(get_jiffies_64()在 64 位平台直接返回jiffies)。volatile是必须的——它被中断异步改写,编译器不许把对它的读操作缓存到寄存器里。- 它是个计数器,不是时间。想知道过了多久,永远是"现在读一次,减去之前读一次"。
换算:HZ 是"每秒拍数"(本文环境 HZ=300):
msecs_to_jiffies(1000)→300拍/* 1 秒 */msecs_to_jiffies(5)→2拍/* 注意!5/3.33=1.5 → 向上取整 */jiffies_to_msecs(1)→4ms/* 1 拍 ≈ 3.33ms,换算同样向上取整 */彩蛋:故意开机 5 分钟就"溢出"(include/linux/jiffies.h):
/* * Have the 32 bit jiffies value wrap 5 minutes after boot * so jiffies wrap bugs show up earlier. */#defineINITIAL_JIFFIES((unsignedlong)(unsignedint)(-300*HZ))jiffies的类型是unsigned long,宽度跟着平台走:32 位平台上它就是 32 位数,数到顶会回绕(wrap)归零。归零后5 + 大数用普通的>比较会得出荒谬结论,所以内核规定必须用time_after()这类专门处理回绕的宏。内核开发者还故意把初始值设成"离回绕只差 5 分钟"——在 32 位平台上,任何没用time_after()的 bug 开机 5 分钟内就会暴露,相当于给全内核打了疫苗。
rk3588平台的实情:arm64 是 64 位平台,jiffies与jiffies_64同体、都是 64 位,数到宇宙热寂也回绕不了,低 32 位那场"5 分钟翻转"在这里翻不起浪,疫苗只是保留了传统。但time_after()依旧是规定写法——代码要能移植到 32 位平台,而且它的语义(比较相对拍数差)也更明确。
驱动里最经典的超时写法:
unsignedlongtimeout=jiffies+msecs_to_jiffies(100);/* 记下"100ms 后的拍号" */while(hardware_not_ready()){if(time_after(jiffies,timeout))/* 当前拍号超过截止拍号? */return-ETIMEDOUT;udelay(10);}容易认错的"假 jiffies":
| 东西 | 是不是 jiffies | 真身 |
|---|---|---|
/proc/uptime | 不是 | ktime(纳秒级,休眠也计时) |
printk 时间戳[ 123.456789] | 不是 | local_clock(),纳秒级 |
times()返回值 | 单位像 | 用户态 ABI 固定 CLK_TCK=100,与内核 HZ 无关 |
| loadavg 采样 | 是 | 每 5 秒(LOAD_FREQ = 5×HZ+1 拍)采一次 |
驱动超时time_after(jiffies, ...) | 是 | 真玩家 |
最重要的一句:jiffies 是"相对心跳",不是墙钟时间。它不知道现在是几点,系统休眠时它也停跳。想算真实世界时间,找 timekeeper(ktime)。
3.2 细粒度世界:timekeeper 与 ktime
timekeeper 是内核的"时间管家",它读硬件计数器①,换算成纳秒,维护几种"钟":
| 时钟 | 名字 | 特点 |
|---|---|---|
| 墙钟 | CLOCK_REALTIME | 跟着现实世界走,NTP 校时会影响它 |
| 单调钟 | CLOCK_MONOTONIC | 只进不退,校时不影响,但休眠时不走 |
| 开机钟 | CLOCK_BOOTTIME | 单调 + 休眠也计时 |
| TAI | CLOCK_TAI | 单调,不吃闰秒 |
用户态的clock_gettime()、/proc/uptime、printk 时间戳,全部来自这套体系——和 HZ 完全无关。
3.3 NO_HZ:闲下来就不打心跳
心跳有什么代价?每次 tick 中断都要耗 CPU、耗电。但 CPU 睡觉(idle)的时候,心跳毫无用处——没有进程要调度,没有账要记。
CONFIG_NO_HZ_IDLE(即CONFIG_NO_HZ=y)的作用就是:CPU idle 时把心跳完全停掉,硬件闹钟只在下一个定时器真正到期时才响。这叫 dynticks(动态 tick)。
所以 HZ 的开销公式是忙碌核数 × HZ,不是核数 × HZ。RK3588 八核全忙、HZ=300 时是 2400 次/秒;挂机时接近 0。
第 4 章 CONFIG_HZ 详解
导读:本章回答三个问题——HZ 是什么、它到底影响什么、该怎么选。
4.1 它是什么
CONFIG_HZ 是"心跳频率"的配置,定义在kernel/Kconfig.hz,只有四档:
| 选项 | 一拍多长 | 典型用户 |
|---|---|---|
CONFIG_HZ_100 | 10 ms | 服务器、NVR(如本仓库rk3568_nvr.config) |
CONFIG_HZ_250 | 4 ms | 折中(本仓库机器人配置) |
CONFIG_HZ_300 | 3.33 ms | 多媒体甜点值(本仓库默认,rockchip_linux_defconfig) |
CONFIG_HZ_1000 | 1 ms | 低延迟(rk3308/px30 defconfig) |
300 这个"怪数字"的来历:它能被 PAL(50fps)和 NTSC(60fps)帧率整除,是视频/多媒体时代的折中方案。
4.2 它的五大职责
前提纠正:很多人以为 HZ 管"系统时间"——其实墙钟、uptime、printk 时间戳全走 timekeeper,HZ 管的是心跳节奏以及一切按拍计费的东西。
职责 1:定时器轮的刻度密度
内核里大量定时器(timer_list,见第 6 章)以拍为刻度。刻度越密,mod_timer()定的闹钟能越准。
职责 2:睡眠/超时的取整
内核的msleep()/schedule_timeout()按"拍"向上取整:
| 调用 | HZ=100 | HZ=300 | HZ=1000 |
|---|---|---|---|
msleep(1)实际睡 | 10~20 ms | 3.33~6.67 ms | 1~2 ms |
(设 n 拍超时,实际睡 n-1~n 拍——挂表时当前拍已经走掉的那一截不计入。)一个驱动循环里写msleep(1)轮询,HZ=300 下比 1000 慢 3 倍。这是 HZ 最常见的"隐性变慢"来源。顺带一提msleep的实现(kernel/time/timer.c):
voidmsleep(unsignedintmsecs){unsignedlongtimeout=msecs_to_jiffies(msecs)+1;...}那个+ 1不是防 0 拍——msecs_to_jiffies()本身向上取整,最小就是 1 拍。它防的是"短睡":如上所述,设 n 拍超时实际可能只睡到 n-1 拍出头,比请求的毫秒数短;+1 之后实际睡眠必然覆盖请求时长。
职责 3:进程 CPU 统计的量化粒度
内核采用 tick 记账(kernel/sched/cputime.c):
voidaccount_process_tick(structtask_struct*p,intuser_tick)每次 tick 看一眼 CPU 正跑在用户态还是内核态,给对应账本 +1 拍。所以top/ps里 CPU 时间的记账粒度是 1 拍:HZ=100 时一拍 10ms——一个跑了 5ms 的短进程可能记 0 拍或 1 拍,显示 0% 或 100%,全看记账时机(注意:展示给用户态时还要换算成固定 100Hz 的单位,见实验 3)。
职责 4:调度节拍(响应上限)
tick 里做抢占检查和负载均衡触发。一个不睡眠、霸占 CPU 的任务(“霸王龙任务”),最坏要等 1 拍才被调度器"巡视"到:HZ=100 → 最坏 10ms;HZ=1000 → 1ms。
注意:这只影响"正在跑的任务被周期性检查"这一条路;任务唤醒抢占(wakeup preemption)是事件驱动的,不走 tick。
职责 5:写死在协议/子系统里的 HZ 常数
/* include/net/tcp.h */#defineTCP_TIMEWAIT_LEN(60*HZ)/* TIME_WAIT 存活 60 秒 */#defineTCP_RTO_MAX((unsigned)(120*HZ))/* 重传超时上限 120 秒 */#defineTCP_RTO_MIN((unsigned)(HZ/5))/* 重传下限 200ms */这些常数的意图是秒级时长,实现是拍数,改 HZ 会自动换算,真实时长不变,但量化粒度变了。
4.3 不受 HZ 影响的东西(重要!)
| 东西 | 原因 |
|---|---|
墙钟、date、CLOCK_REALTIME | timekeeper 纳秒级 |
/proc/uptime、printk 时间戳 | timekeeper |
用户态select/poll/epoll/nanosleep超时 | 走 hrtimer |
usleep_range()、hrtimer_start() | 走 hrtimer |
udelay() | 忙等读硬件计数器,不经定时器 |
用户态sysconf(_SC_CLK_TCK) | ABI 固定返回 100,别被骗 |
4.4 选型:性能、功耗还是响应?
先破除一个误解:
高 HZ 不会卡顿——卡顿的根源恰恰是低 HZ 的粗粒度。但高 HZ 也不提高峰值性能,吞吐反而微降。
"性能"要拆成两个维度看:
| 维度 | HZ=100 | HZ=1000 |
|---|---|---|
| 吞吐(单位时间干完的活) | 略高 | 微降 ~0.5-1% |
| 延迟/响应(最坏等多久) | 差(10ms 粒度) | 好 10 倍 |
代价的具体账目(8 核全忙时):
| HZ=100 | HZ=300 | HZ=1000 | |
|---|---|---|---|
| tick 中断/秒 | 800 | 2400 | 8000 |
| 忙时 CPU 开销 | ~0.1% | ~0.3% | ~1% |
msleep(1)实际睡 | 10~20 ms | 3.33~6.67 ms | 1~2 ms |
注意是忙时开销——开了 NO_HZ_IDLE 后,挂机功耗各档没有区别。
高 HZ 的代价是"匀速多缴 1% 税",平稳且可预测,不会产生任何突发卡顿。卡顿的来源排查顺序是:调度延迟、内存抖动、IO 阻塞——其中"调度巡视太慢"只有低 HZ 会贡献。
决策经验:问自己"有没有msleep(1)级别的内核轮询在热路径上、有没有 <4ms 的硬性控制环"。有 → 1000;没有 → 300 白赚省 0.7% CPU;纯录像/吞吐设备 → 100。
第 5 章 utime 与 stime:进程的两本账
导读:上一章提到"记账",本章把这本账讲清楚。
每个进程的task_struct(内核的"进程档案")里有两个记账字段(include/linux/sched.h):
u64 utime;/* 用户态时间:跑"你自己的代码"花的 CPU */u64 stime;/* 内核态时间:内核"替你干活"花的 CPU */区分标准不是"哪个函数",而是"CPU 当时跑在哪一态":
| 你在做 | 记到哪 |
|---|---|
| 算哈希、memcpy、解码 | utime |
调write()写 eMMC,CPU 在 ext4/blk/mmc 路径里跑 | stime |
大量printf到串口,CPU 在 tty 驱动里折腾 | stime(串口慢速输出是 stime 大户) |
mmap后缺页,内核分配物理页 | stime |
在板上就能看:
time./myapp# user 即 utime,sys 即 stimecat/proc/<pid>/stat# 第 14、15 字段如果某程序 sys 远大于 user,说明它把时间都花在"求内核办事"上了(典型:高频小 IO、疯狂 syscall)。
第 6 章 内核定时器家族:公交与专车
导读:jiffies 世界和 hrtimer 世界的运行机制对比,是理解内核时间子系统的最后一层窗户纸。
6.1 timer wheel:公交车模式
内核里数量最大的定时器用timer_list(俗称 timer wheel,定时器轮)。工作方式像公交车:
- 定闹钟只是"登记到站牌"(挂进哈希表),不编程任何硬件;
- 每次心跳(tick)到站,停车检查:这一站该到期的,全部处理;
- 处理在软中断上下文执行(kernel/time/timer.c 的
run_timer_softirq)。
tick 硬中断 → raise_softirq(TIMER_SOFTIRQ) → 软中断阶段扫轮盘 → 回调特点:便宜(几乎零成本登记)、量大(O(1))、但精度只有 1 拍(3.33ms),且有软中断调度延迟。
6.2 hrtimer:专车模式
hrtimer(高精度定时器)像专车:
- 登记时挂进红黑树(按到期时间排序);
- 每次都把硬件闹钟(比较器)重编程到最早的到期时刻(纳秒精度,kernel/time/hrtimer.c 的
hrtimer_reprogram→tick_program_event); - 闹钟一响,回调直接在硬中断里执行(
hrtimer_interrupt→__run_hrtimer); - 只有回调处理不过来(过载)时,才把剩余的丢给
HRTIMER_SOFTIRQ软中断兜底。
所以有一个常见误区要纠正:
❌ “hrtimer 和普通定时器一样,依赖软中断实现”
✅ timer wheel 的回调 100% 在软中断;hrtimer 的回调正常路径在硬中断,软中断只是过载兜底(以及显式选择HRTIMER_MODE_SOFT的定时器——第 7 章那 4 个 soft 柜台就是给它们留的,它们本来就跑软中断)。
hrtimer 单闹钟轮转:CVAL 是绝对刻度,等待量随计数器缩小
循环 ③→①:回调跑完 → 新最左 B 的绝对刻度 6.0ms 写入 CVAL → 实际等待 TVAL = 6.0 − now 5.0 = 1.0ms,且随计数器前进自动缩小,无需任何人改写 → 1ms 后铃响重复 ②。整棵树无论挂多少颗定时器,始终只占这一颗硬件闹钟;tick_sched_timer 靠返回 RESTART 每次重挂自己,心跳才能每 3.33ms 响一次。
6.3 依赖关系是反的!
更颠覆的一点:高精度模式下,tick 本身就是一个 hrtimer。
/* kernel/time/tick-sched.c */hrtimer_init(&ts->sched_timer,CLOCK_MONOTONIC,HRTIMER_MODE_ABS_HARD);ts->sched_timer.function=tick_sched_timer;那个每 3.33ms 响一次、给 jiffies++ 的心跳,本体是架在 hrtimer 之上的。所以层次关系是:
硬件 arch_timer 比较器(真正的"硬"定时器,全系统唯一) ▲ │ 直接重编程(纳秒精度) ┌────┴─────┐ │ hrtimer │ ← 地基 └────┬─────┘ │ tick_sched_timer 是其中一个 hrtimer ▼ tick 中断 ──→ jiffies++ ──→ TIMER_SOFTIRQ ──→ timer wheel6.4 两兄弟对比总表
| timer wheel(公交) | hrtimer(专车) | |
|---|---|---|
| 组织结构 | 分级哈希轮盘,O(1) | 红黑树,O(log n) |
| 谁编程硬件 | 不编程,搭 tick 便车 | 每次入队重编程比较器 |
| 精度 | 1 拍(3.33ms)+ 软中断延迟 | 硬件计数器精度(纳秒) |
| 回调上下文 | softirq | hardirq(兜底才 softirq) |
| 典型 API | mod_timer()、msleep()、schedule_timeout() | hrtimer_start()、usleep_range() |
6.5 API 选择口诀(背这四条)
- 存时间戳永远用 ktime 族——jiffies 存时间戳有三宗罪:回绕(32 位平台)、休眠停摆、粒度 3.33ms。
- 睡眠:不在乎 ±1 拍 →
msleep();要准 →usleep_range(最小, 最大)(走 hrtimer)。 - 驱动定时器:ms 级、大量、频繁重挂 →
timer_list;要精确 →hrtimer。 - 忙等 <1ms:
udelay()(arch/arm64/lib/delay.c,读硬件计数器空转,不经过任何定时器子系统);能睡则睡,忙等是最后手段。
第 7 章 hrtimer 都有谁在用
导读:把 hrtimer 理解成基础设施,这一章看看它的"客户名单"。
柜台:每个 CPU 一套 8 个 clock base(kernel/time/hrtimer.c),4 个硬中断版 + 4 个软中断版,分别对应 MONOTONIC / REALTIME / BOOTTIME / TAI 四种时钟。入队时按你指定的 clockid 路由到对应柜台。
客户(本仓库实际扫描结果,grep -r "hrtimer_init("):
| 类别 | 用户 | 干什么 |
|---|---|---|
| 内核地基 | tick_sched_timer | 心跳本体(第 6 章讲过) |
| 内核地基 | CFS/RT/DL 调度器 | 精确抢占、带宽控制周期 |
| 内核地基 | 软锁狗(watchdog.c) | 检测 CPU 卡死 |
| 内核地基 | perf 采样 | PMU 轮转与采样事件的节拍 |
| 系统调用 | clock_nanosleep、timer_create、poll/epoll 超时、futex | 用户态的直接消费 |
| 协议栈 | TCP pacing、fq/netem 流量整形 | µs 级发包节拍(qdisc watchdog 同样架在 hrtimer 上) |
| 协议栈 | CAN(bcm/isotp/j1939) | 车载总线时序 |
| 协议栈 | TSN(taprio/act_gate) | 工业以太网门控,µs 级准时 |
| 驱动 | 8250 串口、softdog、Mali GPU(十几处)、Rockchip RGA3/RVE、USB | 本板子真身在用 |
规律:凡是"晚 3.33ms 会出事"的场景都在 hrtimer 上——调度公平性、发包节拍(TCP pacing、TSN 门控)、GPU 负载统计。而"晚 3.33ms 无所谓"的(超时保护、轮询延迟)都留在 timer wheel 上。TCP 自己就是最好的分界样本:pacing 这种 µs 级敏感的活用 hrtimer(tcp_sock里的pacing_timer);而重传/delayed-ACK/TIME_WAIT 这些百毫秒量级的定时器在 5.10 里仍是 jiffies 级的timer_list(inet_connection_sock里的icsk_retransmit_timer/icsk_delack_timer)——第 4 章职责 5 那几个*HZ常数正是给它们用的。
第 8 章 动手实验(RK3588 板上可做)
导读:busybox 环境即可完成,把理论对号入座。
实验 1:亲眼看到心跳
cat/proc/interrupts|greparch_timer# 等 1 秒,再来一次# 忙碌的核每秒大约 +300(HZ=300);idle 的核几乎不动(NO_HZ 生效)# 跑个死循环让 CPU 忙起来对比:whiletrue;do:;done&实验 2:看定时器世界的名册
cat/proc/timer_list|head-60# 能看到每个 CPU 的 hrtimer base、最早到期时间,# 以及 function: tick_sched_timer —— 印证"tick 是个 hrtimer"实验 3:感受 CPU 记账的量化
cat/proc/stat# cpu 开头的行:user/system/idle 等,单位是 1/100 秒(USER_HZ),不是内核的拍!# 让一个核忙满 1 秒再 cat 一次:user 列增量 ≈ 100,与 HZ 无关# /proc/stat 与 times() 同属用户态 ABI,一律按 100Hz 换算——想看 HZ 本尊,回到实验 1 数 arch_timer 中断第 9 章 常见误区 FAQ
Q1:jiffies 是当前时间吗?
不是。它是"开机以来的心跳拍数",相对量、粗粒度、休眠会停。存时间戳用 ktime 族。
Q2:改了 CONFIG_HZ,用户态的times()单位会变吗?
不会。用户态 ABI 固定 CLK_TCK=100,和内核 HZ 无关。
Q3:HZ 越大系统越流畅吗?
响应粒度越细(最坏 10ms → 1ms),但峰值吞吐微降 ~1%。流畅卡顿问题通常另有元凶(内存、IO、调度),不是 HZ。
Q4:HZ=1000 会更耗电吗?
挂机不会(NO_HZ_IDLE 停掉了心跳);只有忙时多 ~1% CPU 开销带来的功耗。
Q5:hrtimer 和 timer wheel 谁更"高级"?要全换 hrtimer 吗?
各司其职。timer wheel 便宜、O(1),适合海量 ms 级超时;hrtimer 精确但每颗都要编程硬件。全换 hrtimer 反而更慢。
Q6:hrtimer 的回调能睡吗?
不能,它在硬中断上下文(兜底的软中断也不能睡)。要睡请用工作队列(workqueue)。
Q7:msleep(1)到底睡多久?会不会"等于没睡"?
不会。msecs_to_jiffies()向上取整,最小 1 拍,绝不会取整成 0;msleep()的+1再保证不短睡(第 4 章职责 2)。HZ=300 下msleep(1)实际睡 3.33~6.67ms——要 1ms 级精度请改用走 hrtimer 的usleep_range()。真正不保证睡眠时长的是自己写schedule_timeout(0)。
Q8:printk 的时间戳能用来测 jiffies 吗?
不能,它来自local_clock()(纳秒级单调钟),比 jiffies 精确得多,两者是不同体系。
Q9:TIME_WAIT 是 60 秒,改 HZ 会变吗?
不会,TCP_TIMEWAIT_LEN = 60*HZ自动换算。变的只是量化精度。
Q10:怎么确认我的内核 HZ 是多少?
板上:用实验 1——让一个核忙起来,看/proc/interrupts里arch_timer每秒增量(≈HZ);或zcat /proc/config.gz | grep "CONFIG_HZ="(需编译时开了 IKCONFIG_PROC);或直接查.config里的CONFIG_HZ_XXX。注意/proc/stat看不出 HZ——它的单位是固定的 USER_HZ=100。
第 10 章 术语表
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| tick / 拍 | tick | 定时器硬件的一次心跳中断 |
| HZ | HZ | 每秒心跳次数(编译期常量) |
| jiffies | jiffies | 全局心跳计数器,一拍 +1 |
| 心跳 | timer tick | CPU 忙时的周期性定时中断 |
| 时钟源 | clocksource | 一直走的硬件计数器,回答"现在几点" |
| 时钟事件设备 | clockevent | 可编程硬件闹钟,回答"到时候叫我" |
| 时间管家 | timekeeper | 读 clocksource,维护墙钟/单调钟等 |
| 高精度定时器 | hrtimer | 纳秒级定时器,回调默认跑硬中断 |
| 定时器轮 | timer wheel | jiffies 级定时器,回调跑软中断 |
| 动态 tick | dynticks / NO_HZ | CPU 空闲时停掉心跳 |
| 回绕 | wraparound | 32 位计数器数到顶归零 |
| 用户态/内核态 | user/kernel mode | CPU 的两种特权级 |
| utime / stime | user/system time | 进程的用户态/内核态 CPU 账本 |
| 硬中断/软中断 | hardirq/softirq | 中断的快处理/延后处理两级 |
| 上下文 | context | 当前代码的"身份"(进程/硬中断/软中断) |
| 睡眠 | sleep | 主动让出 CPU 等条件;中断上下文禁止 |
附录:源码地图
本文档引用的关键文件(基于rk3588开源仓库 5.10 源码),建议按序阅读:
| 文件 | 内容 |
|---|---|
kernel/Kconfig.hz | HZ 四档选项定义 |
include/linux/jiffies.h | jiffies 声明、INITIAL_JIFFIES彩蛋、time_after宏、换算函数 |
kernel/time/timer.c | jiffies_64 定义、timer wheel 全部实现、msleep、schedule_timeout |
kernel/time/timekeeping.c | timekeeper、do_timer()(jiffies 自增处) |
kernel/time/hrtimer.c | hrtimer 全部实现、8 个 clock base、hrtimer_reprogram |
kernel/time/tick-sched.c | tick_sched_timer——"tick 是个 hrtimer"的证据 |
kernel/sched/cputime.c | account_process_tick——tick 记账 |
arch/arm64/lib/delay.c | udelay忙等的真身 |
arch/arm64/configs/rockchip_linux_defconfig | 本板当前配置(HZ_300、NO_HZ、HIGH_RES_TIMERS) |
本文档由 CONFIG_HZ / jiffies / 内核时间体系的技术讨论整理而成,示例与代码行号对应 Linux 5.10.198 源码。