〇、全景:内核计时需要一个基准,TSC 是最快的那一个
内核要计时(ktime、jiffies、调度器的纳秒时间戳......),底层得有一个单调递增的"时间基准" ,这个基准叫 clocksource(时钟源) 。硬件提供了好几种时钟源,精度、速度各不相同;其中 TSC(Time Stamp Counter,时间戳计数器)是 x86 CPU 内置的、读起来最快的一个 ------一条 rdtsc 指令就能读到。
但 TSC 不能拿来就用,它有两个先天问题:只给 cycle 数、不给纳秒 (要校准出频率才能换算),以及历史上不可靠 (老 CPU 的 TSC 会随频率切换漂移、多核可能不同步)。所以内核的时钟初始化,就是一条"先校准、再校验、通过了才把 TSC 扶正"的路:
#mermaid-svg-baQuzrOJjn9W95BH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-baQuzrOJjn9W95BH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-baQuzrOJjn9W95BH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-baQuzrOJjn9W95BH .error-icon{fill:#552222;}#mermaid-svg-baQuzrOJjn9W95BH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-baQuzrOJjn9W95BH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-baQuzrOJjn9W95BH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-baQuzrOJjn9W95BH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-baQuzrOJjn9W95BH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-baQuzrOJjn9W95BH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-baQuzrOJjn9W95BH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-baQuzrOJjn9W95BH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-baQuzrOJjn9W95BH .marker.cross{stroke:#333333;}#mermaid-svg-baQuzrOJjn9W95BH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-baQuzrOJjn9W95BH p{margin:0;}#mermaid-svg-baQuzrOJjn9W95BH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-baQuzrOJjn9W95BH .cluster-label text{fill:#333;}#mermaid-svg-baQuzrOJjn9W95BH .cluster-label span{color:#333;}#mermaid-svg-baQuzrOJjn9W95BH .cluster-label span p{background-color:transparent;}#mermaid-svg-baQuzrOJjn9W95BH .label text,#mermaid-svg-baQuzrOJjn9W95BH span{fill:#333;color:#333;}#mermaid-svg-baQuzrOJjn9W95BH .node rect,#mermaid-svg-baQuzrOJjn9W95BH .node circle,#mermaid-svg-baQuzrOJjn9W95BH .node ellipse,#mermaid-svg-baQuzrOJjn9W95BH .node polygon,#mermaid-svg-baQuzrOJjn9W95BH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-baQuzrOJjn9W95BH .rough-node .label text,#mermaid-svg-baQuzrOJjn9W95BH .node .label text,#mermaid-svg-baQuzrOJjn9W95BH .image-shape .label,#mermaid-svg-baQuzrOJjn9W95BH .icon-shape .label{text-anchor:middle;}#mermaid-svg-baQuzrOJjn9W95BH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-baQuzrOJjn9W95BH .rough-node .label,#mermaid-svg-baQuzrOJjn9W95BH .node .label,#mermaid-svg-baQuzrOJjn9W95BH .image-shape .label,#mermaid-svg-baQuzrOJjn9W95BH .icon-shape .label{text-align:center;}#mermaid-svg-baQuzrOJjn9W95BH .node.clickable{cursor:pointer;}#mermaid-svg-baQuzrOJjn9W95BH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-baQuzrOJjn9W95BH .arrowheadPath{fill:#333333;}#mermaid-svg-baQuzrOJjn9W95BH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-baQuzrOJjn9W95BH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-baQuzrOJjn9W95BH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-baQuzrOJjn9W95BH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-baQuzrOJjn9W95BH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-baQuzrOJjn9W95BH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-baQuzrOJjn9W95BH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-baQuzrOJjn9W95BH .cluster text{fill:#333;}#mermaid-svg-baQuzrOJjn9W95BH .cluster span{color:#333;}#mermaid-svg-baQuzrOJjn9W95BH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-baQuzrOJjn9W95BH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-baQuzrOJjn9W95BH rect.text{fill:none;stroke-width:0;}#mermaid-svg-baQuzrOJjn9W95BH .icon-shape,#mermaid-svg-baQuzrOJjn9W95BH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-baQuzrOJjn9W95BH .icon-shape p,#mermaid-svg-baQuzrOJjn9W95BH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-baQuzrOJjn9W95BH .icon-shape .label rect,#mermaid-svg-baQuzrOJjn9W95BH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-baQuzrOJjn9W95BH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-baQuzrOJjn9W95BH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-baQuzrOJjn9W95BH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 早期:setup_arch
tsc_early_init 检测 TSC
过渡:late_time_init
hpet_time_init(HPET 优先,PIT 兜底)
扶正:tsc_init
校准频率 + 校验可靠性 → 注册 TSC
升级:精确校准后
clocksource_tsc 替换 early 版
clocksource_tsc_early
rating 299
clocksource_tsc
rating 300(最高)
一句话主线:内核的计时基准是 clocksource,靠 rating(质量分)自动择优;TSC 是最快但需要"校准频率 + 校验可靠性"的时钟源 ------早期用 PIT/HPET 兜底计时,
tsc_init里把 TSC 校准、校验通过后注册为 rating 最高的 clocksource,timekeeping 就自动切到 TSC。这篇讲的就是这条"从 PIT/HPET 切到 TSC"的路。
一、clocksource 抽象:内核怎么"看"时间
1.1 一个时钟源 = read + 换算 + 质量分
内核把所有硬件时钟统一抽象成 struct clocksource(include/linux/clocksource.h),每个时钟源提供三样东西:
read():读当前计数值(一条硬件读指令,比如 TSC 的rdtsc);- 换算系数
mult/shift:把"计数值"换算成"纳秒"------ns = (cycles × mult) >> shift,用定点乘移位代替除法(除法太慢); rating:质量分,越高越好,内核用它给多个时钟源排序。
1.2 rating 决定谁当主时钟源
一个系统里往往同时有好几个可用的时钟源,内核自动选 rating 最高的那个当主时钟源。v6.6 x86 上的 rating 大致是:
| 时钟源 | rating | 读一次的开销 | 备注 |
|---|---|---|---|
| TSC | 300 | 一条 rdtsc 指令 |
最快,但要校准 + 校验 |
| HPET | 250 | MMIO 读(慢) | 精度高,过渡用 |
| ACPI PM timer | 200 | 端口读(慢) | 更慢的兜底 |
| PIT | 更低 | 端口读(很慢) | 最古老的兜底 |
所以一旦 TSC 通过校验被注册(rating 300,全场最高),timekeeping 就自动把它切为主时钟源,替换掉过渡期的 HPET/PIT。这就是"从 PIT/HPET 切到 TSC"的本质------不是手动切换,是 rating 竞争的结果。
二、TSC:最快,但只给 cycle 数
TSC 是每个 CPU 内置的一个 64 位计数器,每个 CPU 时钟周期加 1。读它就是一条指令:
c
// arch/x86/include/asm/msr.h (v6.6, line 180)
static __always_inline unsigned long long rdtsc(void)
{
// ... asm volatile("rdtsc" : EAX_EDX_RET(val, low, high));
}
TSC 作为 clocksource 的 read() 就是这个 rdtsc(tsc.c:1167 的 .read = read_tsc)。
它的两个特点,正好引出后面的两件事:
- 快:一条指令读 64 位,比 HPET(要 MMIO 读)快一个数量级。所以它值得被扶正为主时钟源。
- 只给 cycle 数 :
rdtsc返回的是"计了多少个 cycle",不是"过了多少纳秒"。要把 cycle 换算成纳秒,必须知道 TSC 的频率------这就是下一节的"校准"。
三、校准:把 cycle 数换算成纳秒
TSC 频率(内核里叫 tsc_khz,单位 kHz)是换算的钥匙:ns = cycles × 10^6 / tsc_khz。所以拿到 tsc_khz 是启用 TSC 的前提。现代 x86 的校准方式是直接问 CPUID 要,而不是老式地数 PIT 拍数:
c
// arch/x86/kernel/tsc.c (v6.6, line 658)
unsigned long native_calibrate_tsc(void)
{
// ...
/* CPUID 15H TSC/Crystal ratio, plus optionally Crystal Hz */
cpuid(0x15, &eax_denominator, &ebx_numerator, &ecx_hz, &edx);
// ...
crystal_khz = ecx_hz / 1000;
// ...
return crystal_khz * ebx_numerator / eax_denominator; // TSC 频率
}
CPUID 的 0x15 叶子直接报告"TSC 频率 = 参考晶振(crystal)× 一个比值"------ecx_hz 是晶振频率,ebx_numerator / eax_denominator 是 TSC 对晶振的倍率。算出 tsc_khz 后,clocksource 框架就能据此算出 mult / shift,把 cycle 精确换算成纳秒。
老式 x86(CPUID 不报频率)才退回"用已知频率的 PIT/HPET 做基准、数一段固定时间里的 TSC cycle 数"来反推频率。现代 CPU 直接问 CPUID,又快又准(源码注释称之为 "hardware reported frequency",最准确)。
四、可靠性:为什么 TSC 要"校验"才能用
如果 TSC 只是"快 + 校准完就能用",那 tsc_init 就太简单了。真正的复杂性在于历史包袱:早期多核 CPU 的 TSC 有两个坑------
- 随频率漂移:CPU 切 P-state(省电降频)时,老 CPU 的 TSC 也会跟着变速,导致"同样多 cycle ≠ 同样长时间",计时会漂;
- 多核不同步:各核的 TSC 各自计数,可能起点不同、速率不同,跨核比较时间就对不上。
所以现代 CPU 用几个 CPUID 能力位来声明"我的 TSC 没问题",tsc_init 里据此判断(tsc.c:1230 的 check_system_tsc_reliable):
| 能力位 | 含义 |
|---|---|
CONSTANT_TSC |
TSC 频率恒定,不随 P-state 变化 |
NONSTOP_TSC |
进 C-state(睡眠)时 TSC 也不停 |
TSC_ADJUST |
有寄存器可检测 TSC 被篡改/漂移 |
只有这些位都齐了,才认为 TSC 足够可靠、可以关掉 watchdog(tsc.c:1258-1262)。此外 unsynchronized_tsc()(tsc.c:1269)会判断多核 TSC 是否同步,不同步就直接 mark_tsc_unstable 放弃 TSC。
这里还有个"运行时兜底":TSC 的 clocksource 带了 CLOCK_SOURCE_MUST_VERIFY 标志(tsc.c:1169-1170 的 .flags 字段),意味着即使注册了,内核的 clocksource watchdog 还会拿它和另一个已知可靠的时钟源(比如 HPET)定期对表 ,一旦发现 TSC 漂移,就 clocksource_mark_unstable 把它踢掉、退回 HPET。这就是"TSC 快,但要被盯着"的完整闭环。
五、初始化流程:三阶段把 TSC 扶正
把前面串起来,x86 的时钟初始化分三个阶段(衔接 #1 的启动链路):
5.1 阶段一:setup_arch 里的早期检测
setup_arch(arch/x86/kernel/setup.c)早期就调 tsc_early_init()(setup.c:1042,实现于 tsc.c:1562)------先确认 CPU 有没有 TSC、能不能用早期可用的方法先估一个频率。这一步只是"摸底",还没正式启用 TSC。
5.2 阶段二:late_time_init 里初始化过渡时钟源
time_init()(time.c:111)本身几乎啥也不干,只是注册了一个"晚一点再做"的回调:
c
// arch/x86/kernel/time.c (v6.6, line 111)
void __init time_init(void)
{
late_time_init = x86_late_time_init; // 只注册回调,真正的活在 late_time_init 里
}
真正干活的是 late_time_init()(init/main.c:1031 调用)→ x86_late_time_init(time.c:85):
c
// arch/x86/kernel/time.c (v6.6, line 85)
static __init void x86_late_time_init(void)
{
x86_init.irqs.intr_mode_select();
x86_init.timers.timer_init(); // = hpet_time_init:HPET 优先、PIT 兜底
x86_init.irqs.intr_mode_init();
tsc_init(); // ← 正式校准 + 注册 TSC
}
hpet_time_init(time.c:75)是过渡时钟源:先 hpet_enable() 试 HPET,失败再 pit_timer_init() 退回 PIT:
c
// arch/x86/kernel/time.c (v6.6, line 75)
void __init hpet_time_init(void)
{
if (!hpet_enable()) {
if (!pit_timer_init())
return;
}
setup_default_timer_irq();
}
此时内核先用 HPET/PIT 计时(jiffies 就靠它),TSC 还没上位。
5.3 阶段三:tsc_init 把 TSC 扶正
最后 tsc_init(tsc.c:1574)做三件事,然后注册 TSC:
c
// arch/x86/kernel/tsc.c (v6.6, line 1574)
void __init tsc_init(void)
{
if (!cpu_feature_enabled(X86_FEATURE_TSC))
return; // ① 没有 TSC,直接放弃
// ... 校准 tsc_khz(CPUID 或 PIT 计数)...
check_system_tsc_reliable(); // ② 校验可靠性(constant/nonstop)
if (unsynchronized_tsc()) {
mark_tsc_unstable("TSCs unsynchronized"); // 多核不同步,放弃
return;
}
// ...
clocksource_register_khz(&clocksource_tsc_early, tsc_khz); // ③ 注册 early 版
}
注意这里注册的是 clocksource_tsc_early(rating 299) ,而不是正式版(rating 300)。两个版本的区别(tsc.c:1163 vs :1184):
clocksource_tsc_early |
clocksource_tsc |
|
|---|---|---|
| rating | 299 | 300 |
| 频率 | 初步校准值(可能略糙) | 精确校准值 |
VALID_FOR_HRES |
无(不能用于高精度计时) | 有 |
为什么分两版?因为"精确频率"要花点时间才能拿到,tsc_init 先用初步频率把 TSC 立起来(299 已经比 HPET 的 250 高了,立即抢班)。之后正式版(300)在两条路径之一注册:如果 TSC 频率已知(TSC_KNOWN_FREQ,CPUID 直接报了频率),init_tsc_clocksource 直接注册并注销 early 版;否则 tsc_refine_calibration_work(一个异步 delayed work,tsc.c:1372)精调出更准的频率后再注册。源码注释(tsc.c:1179)说得很直白:提前给正式版标上 VALID_FOR_HRES,这样一旦注销 early 版,正式版能立刻无缝接手。
六、为什么这样设计(Why 层)
6.1 为什么用 rating 竞争,而不是硬编码某个时钟源
不同机器能用的时钟源不一样(有的没 HPET、有的 TSC 不可靠),硬编码一个"最佳时钟源"行不通。rating 机制把"谁最好"这个决策交给一个可比的数字 :每个时钟源自报分数,内核永远选当前最高的。新时钟源注册时只需报个分,timekeeping 自动判断要不要换、并保证切换时时间连续------这是"多候选 + 自动择优"的典型解耦。
6.2 为什么 TSC 要分 early(299)/ 正式(300)两版
核心是**"先上车、再精调"**:精确校准要时间,但内核不想等------TSC 的初步频率已经足够让它以 299 分超过 HPET(250)抢班;等精确频率出来,再升级到 300 版并解锁高精度计时(VALID_FOR_HRES)。如果一开始就死等精确校准,这段启动时间里的时钟源还是慢吞吞的 HPET,白白浪费 TSC 的速度。
6.3 为什么 TSC 这么"快"却还要 watchdog 盯着
这是"信任但要验证":TSC 快,但它的可靠性依赖 CPU 自己声明的能力位(CONSTANT_TSC 等),而这些声明可能有 bug (历史上确实有 TSC 被发现在某些平台上漂移的案例)。所以光看 CPUID 位还不够,得用一个独立的可靠时钟源(HPET)做参照,运行时定期对表------一旦发现 TSC 漂移,立刻踢掉退回 HPET。"最快的时钟源"和"被 watchdog 盯着"不矛盾,前者是性能、后者是正确性。
6.4 为什么换算用 mult/shift 定点数,而不是除法
TSC 的 read() 在计时热路径上每秒被读无数次,ns = cycles × 10^6 / tsc_khz 里的除法(尤其 64 位除法)非常慢。mult/shift 把它变成一次乘法和一次移位((cycles × mult) >> shift),硬件执行快得多。这是"把热路径上的昂贵运算,预先折算成乘移位"的经典手法------mult/shift 在时钟源注册时算好,之后每次换算都只是乘和移位。
七、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| CPU #1 BSP 拉起 | 时钟初始化的三阶段(setup_arch → late_time_init → tsc_init)落在 #1 讲的启动链路上:tsc_early_init 在 setup_arch 里、tsc_init 在 start_kernel 后期的 late_time_init 里 |
| CPU #2 AP 拉起 | start_secondary 里有 check_tsc_sync_target()------AP 起来时要校验自己的 TSC 和 BSP 是否同步(本篇 §4 的"多核不同步"正是这个校验的动机) |
| CPU #5 cpuidle | TSC 的 NONSTOP_TSC 能力位关系到"进 C-state 时 TSC 停不停",和 cpuidle 的 C-state 直接相关 |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
time_init / x86_late_time_init / hpet_time_init |
arch/x86/kernel/time.c:111 / :85 / :75 |
tsc_init / tsc_early_init |
arch/x86/kernel/tsc.c:1574 / :1562 |
native_calibrate_tsc(CPUID 0x15 校准) |
arch/x86/kernel/tsc.c:658 |
check_system_tsc_reliable / unsynchronized_tsc |
arch/x86/kernel/tsc.c:1230 / :1269 |
clocksource_tsc_early(rating 299) / clocksource_tsc(rating 300) |
arch/x86/kernel/tsc.c:1163 / :1184 |
rdtsc(读 TSC) |
arch/x86/include/asm/msr.h:180 |
mark_tsc_unstable |
arch/x86/kernel/tsc.c:1201 |
| HPET clocksource(rating 250) | arch/x86/kernel/hpet.c:860 |
| ACPI PM timer(rating 200) | drivers/clocksource/acpi_pm.c:68 |
time_init / late_time_init 调用点 |
init/main.c:980 / :1031 |
x86_init.timers.timer_init = hpet_time_init |
arch/x86/kernel/x86_init.c:96 |