☰
Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC
2026/10/18 15:10:40 网站建设 项目流程

〇、全景:内核计时需要一个基准,TSC 是最快的那一个

内核要计时(ktime、jiffies、调度器的纳秒时间戳……),底层得有一个单调递增的"时间基准",这个基准叫clocksource(时钟源)。硬件提供了好几种时钟源,精度、速度各不相同;其中TSC(Time Stamp Counter,时间戳计数器)是 x86 CPU 内置的、读起来最快的一个——一条rdtsc指令就能读到。

但 TSC 不能拿来就用,它有两个先天问题:只给 cycle 数、不给纳秒(要校准出频率才能换算),以及历史上不可靠(老 CPU 的 TSC 会随频率切换漂移、多核可能不同步)。所以内核的时钟初始化,就是一条"先校准、再校验、通过了才把 TSC 扶正"的路:

早期: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读一次的开销备注
TSC300一条rdtsc指令最快,但要校准 + 校验
HPET250MMIO 读(慢)精度高,过渡用
ACPI PM timer200端口读(慢)更慢的兜底
PIT更低端口读(很慢)最古老的兜底

所以一旦 TSC 通过校验被注册(rating 300,全场最高),timekeeping就自动把它切为主时钟源,替换掉过渡期的 HPET/PIT。这就是"从 PIT/HPET 切到 TSC"的本质——不是手动切换,是 rating 竞争的结果。


二、TSC:最快,但只给 cycle 数

TSC 是每个 CPU 内置的一个 64 位计数器,每个 CPU 时钟周期加 1。读它就是一条指令:

// arch/x86/include/asm/msr.h (v6.6, line 180)static__always_inlineunsignedlonglongrdtsc(void){// … asm volatile("rdtsc" : EAX_EDX_RET(val, low, high));}

TSC 作为 clocksource 的read()就是这个rdtsc(tsc.c:1167的.read = read_tsc)。

它的两个特点,正好引出后面的两件事:

  1. 快:一条指令读 64 位,比 HPET(要 MMIO 读)快一个数量级。所以它值得被扶正为主时钟源。
  2. 只给 cycle 数:rdtsc返回的是"计了多少个 cycle",不是"过了多少纳秒"。要把 cycle 换算成纳秒,必须知道 TSC 的频率——这就是下一节的"校准"。

三、校准:把 cycle 数换算成纳秒

TSC 频率(内核里叫tsc_khz,单位 kHz)是换算的钥匙:ns = cycles × 10^6 / tsc_khz。所以拿到tsc_khz是启用 TSC 的前提。现代 x86 的校准方式是直接问 CPUID 要,而不是老式地数 PIT 拍数:

// arch/x86/kernel/tsc.c (v6.6, line 658)unsignedlongnative_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;// …returncrystal_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 有两个坑——

  1. 随频率漂移:CPU 切 P-state(省电降频)时,老 CPU 的 TSC 也会跟着变速,导致"同样多 cycle ≠ 同样长时间",计时会漂;
  2. 多核不同步:各核的 TSC 各自计数,可能起点不同、速率不同,跨核比较时间就对不上。

所以现代 CPU 用几个 CPUID 能力位来声明"我的 TSC 没问题",tsc_init里据此判断(tsc.c:1230的check_system_tsc_reliable):

能力位含义
CONSTANT_TSCTSC 频率恒定,不随 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 快,但要被盯着"的完整闭环。

4.1 运行时校验:check_tsc_sync怎么测出多核 TSC 不同步

上面说"多核不同步就放弃 TSC",但怎么知道不同步?靠arch/x86/kernel/tsc_sync.c里一个点对点的 warp 测量。它有两个角色(源码头注释tsc_sync.c:9):

  • source CPU:发起 bootup 的 CPU(已在线,通常是 BSP);
  • target CPU:新启动的 CPU(就是start_secondary里check_tsc_sync_target那台 AP)。

入口check_tsc_sync_target(tsc_sync.c:433,AP 侧)分三段:

// arch/x86/kernel/tsc_sync.c (v6.6, line 433,节选)voidcheck_tsc_sync_target(void){if(unsynchronized_tsc())// ① 已判定不同步,直接走return;// ② 先试"软同步":TSC_ADJUST MSR 能不能直接掰平本核 TSCif(tsc_store_and_check_tsc_adjust(false)||tsc_clocksource_reliable)return;// ③ 软同步不行 → 把 source 检查派到 BSP,双方同时跑 warp 测量smp_call_function_single(cpumask_first(cpu_online_mask),check_tsc_sync_source,…);retry:atomic_inc(&start_count);// ④ 和 source 握手:同时进入测量while(atomic_read(&start_count)!=cpus)cpu_relax();cur_max_warp=check_tsc_warp(loop_timeout(cpu));// ⑤ 跑 warp 测量// ⑥ 测出 warp → TSC_ADJUST 补偿(写 MSR)→ retry,最多 3 次}

① 软同步(tsc_store_and_check_tsc_adjust,tsc_sync.c:180):现代 CPU 有MSR_IA32_TSC_ADJUST,软件写它就能微调本核 TSC 偏移。AP 先读自己的 TSC_ADJUST,和同 package 已在线 CPU的adjusted值比较,不一样就直接写 MSR 掰平、然后跳过昂贵的 warp 测量——"能硬件直接对齐就不跑测量"的快速路径。

⑤ warp 测量(check_tsc_warp,tsc_sync.c:267):软同步没用时才跑。两个 CPU同时进入循环,用一把全局自旋锁 + 一个全局last_tsc交替读 TSC:

// arch/x86/kernel/tsc_sync.c (v6.6, line 267,节选)for(i=0;;i++){arch_spin_lock(&sync_lock);// 拿全局锁prev=last_tsc;// 读上一次(可能是另一颗 CPU)读的 TSCnow=rdtsc_ordered();// 读自己的 TSClast_tsc=now;// 存下,供另一颗 CPU 下次比较arch_spin_unlock(&sync_lock);// …if(unlikely(prev>now))// 倒退了!两核 TSC 不同步(warp)/* 记录 max_warp / nr_warps */;}

原理很巧妙:若两核 TSC 严格同步,now一定 ≥ 上次任何核读到的prev(时间单调);一旦prev > now(“时间倒退”),就证明两核计数有偏差——这就是 warp。测量跑loop_timeout(tsc_sync.c:341)毫秒:同 core 已有 sibling 在线跑 2ms(已有该 socket 信息),否则跑 20ms。

⑥ 补偿或放弃:测出 warp 后,若支持 TSC_ADJUST,就把 warp 加到adjusted里写回 MSR 补偿、retry(最多 3 次test_runs);若重试还不对、或"随机 warp"(random_warps,两核交替观察到倒退),就没救了——check_tsc_sync_source(tsc_sync.c:356)里schedule_work触发mark_tsc_unstable("check_tsc_sync_source failed")(tsc_sync.c:346-348),TSC 被踢、退回 HPET。

运行后的持续监控:warp 只在 AP 上线时跑一次,之后 TSC 还可能被坏 BIOS 篡改,所以有个周期定时器tsc_sync_check_timer(tsc_sync.c:94,SYNC_CHECK_INTERVAL = 10 分钟)定期轮转所有在线 CPU,调tsc_verify_tsc_adjust(tsc_sync.c:50)检查 TSC_ADJUST 有没有被动过、动过就恢复。加定时器的原因(源码注释tsc_sync.c:82):正常每次进 idle 都会检查,但系统可能一直忙、或故意不进 idle,得有个定时器兜底。


五、初始化流程:三阶段把 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)本身几乎啥也不干,只是注册了一个"晚一点再做"的回调:

// arch/x86/kernel/time.c (v6.6, line 111)void__inittime_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):

// arch/x86/kernel/time.c (v6.6, line 85)static__initvoidx86_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:

// arch/x86/kernel/time.c (v6.6, line 75)void__inithpet_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:

// arch/x86/kernel/tsc.c (v6.6, line 1574)void__inittsc_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:1163vs:1184):

clocksource_tsc_earlyclocksource_tsc
rating299300
频率初步校准值(可能略糙)精确校准值
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.1 展开讲了 source/target 双角色 + warp 测量机制)
CPU #5 cpuidleTSC 的NONSTOP_TSC能力位关系到"进 C-state 时 TSC 停不停",和 cpuidle 的 C-state 直接相关

附:本篇关键源码索引

符号位置
time_init/x86_late_time_init/hpet_time_initarch/x86/kernel/time.c:111/:85/:75
tsc_init/tsc_early_initarch/x86/kernel/tsc.c:1574/:1562
native_calibrate_tsc(CPUID 0x15 校准)arch/x86/kernel/tsc.c:658
check_system_tsc_reliable/unsynchronized_tscarch/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_unstablearch/x86/kernel/tsc.c:1201
check_tsc_sync_target/check_tsc_sync_source(多核 TSC 同步校验)arch/x86/kernel/tsc_sync.c:433/:356
check_tsc_warp(warp 测量循环)arch/x86/kernel/tsc_sync.c:267
tsc_store_and_check_tsc_adjust(TSC_ADJUST 软同步)arch/x86/kernel/tsc_sync.c:180
tsc_sync_check_timer_fn/tsc_verify_tsc_adjust(周期监控)arch/x86/kernel/tsc_sync.c:94/:50
loop_timeout(同 core 2ms / 否则 20ms)arch/x86/kernel/tsc_sync.c:341
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_initarch/x86/kernel/x86_init.c:96

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询