资讯动态

Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC

发布时间:2026/10/18 15:10:46 来源:尧图企业网站定制
〇、全景内核计时需要一个基准TSC 是最快的那一个内核要计时ktime、jiffies、调度器的纳秒时间戳……底层得有一个单调递增的时间基准这个基准叫clocksource时钟源。硬件提供了好几种时钟源精度、速度各不相同其中TSCTime Stamp Counter时间戳计数器是 x86 CPU 内置的、读起来最快的一个——一条rdtsc指令就能读到。但 TSC 不能拿来就用它有两个先天问题只给 cycle 数、不给纳秒要校准出频率才能换算以及历史上不可靠老 CPU 的 TSC 会随频率切换漂移、多核可能不同步。所以内核的时钟初始化就是一条先校准、再校验、通过了才把 TSC 扶正的路早期setup_archtsc_early_init 检测 TSC过渡late_time_inithpet_time_initHPET 优先PIT 兜底扶正tsc_init校准频率 校验可靠性 → 注册 TSC升级精确校准后clocksource_tsc 替换 early 版clocksource_tsc_earlyrating 299clocksource_tscrating 300最高一句话主线内核的计时基准是 clocksource靠 rating质量分自动择优TSC 是最快但需要校准频率 校验可靠性的时钟源——早期用 PIT/HPET 兜底计时tsc_init里把 TSC 校准、校验通过后注册为 rating 最高的 clocksourcetimekeeping 就自动切到 TSC。这篇讲的就是这条从 PIT/HPET 切到 TSC的路。一、clocksource 抽象内核怎么看时间1.1 一个时钟源 read 换算 质量分内核把所有硬件时钟统一抽象成struct clocksourceinclude/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()就是这个rdtsctsc.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 拍数// 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_khzecx_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 精确换算成纳秒。老式 x86CPUID 不报频率才退回用已知频率的 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_TSCTSC 频率恒定不随 P-state 变化NONSTOP_TSC进 C-state睡眠时 TSC 也不停TSC_ADJUST有寄存器可检测 TSC 被篡改/漂移只有这些位都齐了才认为 TSC 足够可靠、可以关掉 watchdogtsc.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:9source CPU发起 bootup 的 CPU已在线通常是 BSPtarget CPU新启动的 CPU就是start_secondary里check_tsc_sync_target那台 AP。入口check_tsc_sync_targettsc_sync.c:433AP 侧分三段// 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_warpcheck_tsc_warp(loop_timeout(cpu));// ⑤ 跑 warp 测量// ⑥ 测出 warp → TSC_ADJUST 补偿写 MSR→ retry最多 3 次}① 软同步tsc_store_and_check_tsc_adjusttsc_sync.c:180现代 CPU 有MSR_IA32_TSC_ADJUST软件写它就能微调本核 TSC 偏移。AP 先读自己的 TSC_ADJUST和同 package 已在线 CPU的adjusted值比较不一样就直接写 MSR 掰平、然后跳过昂贵的 warp 测量——能硬件直接对齐就不跑测量的快速路径。⑤ warp 测量check_tsc_warptsc_sync.c:267软同步没用时才跑。两个 CPU同时进入循环用一把全局自旋锁 一个全局last_tsc交替读 TSC// arch/x86/kernel/tsc_sync.c (v6.6, line 267节选for(i0;;i){arch_spin_lock(sync_lock);// 拿全局锁prevlast_tsc;// 读上一次可能是另一颗 CPU读的 TSCnowrdtsc_ordered();// 读自己的 TSClast_tscnow;// 存下供另一颗 CPU 下次比较arch_spin_unlock(sync_lock);// …if(unlikely(prevnow))// 倒退了两核 TSC 不同步warp/* 记录 max_warp / nr_warps */;}原理很巧妙若两核 TSC 严格同步now一定 ≥ 上次任何核读到的prev时间单调一旦prev now“时间倒退”就证明两核计数有偏差——这就是 warp。测量跑loop_timeouttsc_sync.c:341毫秒同 core 已有 sibling 在线跑 2ms已有该 socket 信息否则跑 20ms。⑥ 补偿或放弃测出 warp 后若支持 TSC_ADJUST就把 warp 加到adjusted里写回 MSR 补偿、retry最多 3 次test_runs若重试还不对、或随机 warprandom_warps两核交替观察到倒退就没救了——check_tsc_sync_sourcetsc_sync.c:356里schedule_work触发mark_tsc_unstable(check_tsc_sync_source failed)tsc_sync.c:346-348TSC 被踢、退回 HPET。运行后的持续监控warp 只在 AP 上线时跑一次之后 TSC 还可能被坏 BIOS 篡改所以有个周期定时器tsc_sync_check_timertsc_sync.c:94SYNC_CHECK_INTERVAL 10 分钟定期轮转所有在线 CPU调tsc_verify_tsc_adjusttsc_sync.c:50检查 TSC_ADJUST 有没有被动过、动过就恢复。加定时器的原因源码注释tsc_sync.c:82正常每次进 idle 都会检查但系统可能一直忙、或故意不进 idle得有个定时器兜底。五、初始化流程三阶段把 TSC 扶正把前面串起来x86 的时钟初始化分三个阶段衔接 #1 的启动链路5.1 阶段一setup_arch里的早期检测setup_archarch/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_initx86_late_time_init;// 只注册回调真正的活在 late_time_init 里}真正干活的是late_time_init()init/main.c:1031调用→x86_late_time_inittime.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_initHPET 优先、PIT 兜底x86_init.irqs.intr_mode_init();tsc_init();// ← 正式校准 注册 TSC}hpet_time_inittime.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_inittsc.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_khzCPUID 或 PIT 计数…check_system_tsc_reliable();// ② 校验可靠性constant/nonstopif(unsynchronized_tsc()){mark_tsc_unstable(TSCs unsynchronized);// 多核不同步放弃return;}// …clocksource_register_khz(clocksource_tsc_early,tsc_khz);// ③ 注册 early 版}注意这里注册的是clocksource_tsc_earlyrating 299而不是正式版rating 300。两个版本的区别tsc.c:1163vs:1184clocksource_tsc_earlyclocksource_tscrating299300频率初步校准值可能略糙精确校准值VALID_FOR_HRES无不能用于高精度计时有为什么分两版因为精确频率要花点时间才能拿到tsc_init先用初步频率把 TSC 立起来299 已经比 HPET 的 250 高了立即抢班。之后正式版300在两条路径之一注册如果 TSC 频率已知TSC_KNOWN_FREQCPUID 直接报了频率init_tsc_clocksource直接注册并注销 early 版否则tsc_refine_calibration_work一个异步 delayed worktsc.c:1372精调出更准的频率后再注册。源码注释tsc.c:1179说得很直白提前给正式版标上VALID_FOR_HRES这样一旦注销 early 版正式版能立刻无缝接手。六、为什么这样设计Why 层6.1 为什么用 rating 竞争而不是硬编码某个时钟源不同机器能用的时钟源不一样有的没 HPET、有的 TSC 不可靠硬编码一个最佳时钟源行不通。rating 机制把谁最好这个决策交给一个可比的数字每个时钟源自报分数内核永远选当前最高的。新时钟源注册时只需报个分timekeeping自动判断要不要换、并保证切换时时间连续——这是多候选 自动择优的典型解耦。6.2 为什么 TSC 要分 early299/ 正式300两版核心是**“先上车、再精调”**精确校准要时间但内核不想等——TSC 的初步频率已经足够让它以 299 分超过 HPET250抢班等精确频率出来再升级到 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/:75tsc_init/tsc_early_initarch/x86/kernel/tsc.c:1574/:1562native_calibrate_tscCPUID 0x15 校准arch/x86/kernel/tsc.c:658check_system_tsc_reliable/unsynchronized_tscarch/x86/kernel/tsc.c:1230/:1269clocksource_tsc_earlyrating 299 /clocksource_tscrating 300arch/x86/kernel/tsc.c:1163/:1184rdtsc读 TSCarch/x86/include/asm/msr.h:180mark_tsc_unstablearch/x86/kernel/tsc.c:1201check_tsc_sync_target/check_tsc_sync_source多核 TSC 同步校验arch/x86/kernel/tsc_sync.c:433/:356check_tsc_warpwarp 测量循环arch/x86/kernel/tsc_sync.c:267tsc_store_and_check_tsc_adjustTSC_ADJUST 软同步arch/x86/kernel/tsc_sync.c:180tsc_sync_check_timer_fn/tsc_verify_tsc_adjust周期监控arch/x86/kernel/tsc_sync.c:94/:50loop_timeout同 core 2ms / 否则 20msarch/x86/kernel/tsc_sync.c:341HPET clocksourcerating 250arch/x86/kernel/hpet.c:860ACPI PM timerrating 200drivers/clocksource/acpi_pm.c:68time_init/late_time_init调用点init/main.c:980/:1031x86_init.timers.timer_init hpet_time_initarch/x86/kernel/x86_init.c:96

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑