Published at 2024-07-28 | Last Update 2024-07-28
本文整理了一些 Linux 时钟源 tsc
相关的软硬件知识,在一些故障排查场景可能会用到。
Fig. Scaling up crystal frequency for different components of a computer. Image source Youtube
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
~20MHz
的石英晶体谐振器(quartz crystal resonator)石英晶体谐振器是利用石英晶体(又称水晶)的压电效应 来产生高精度振荡频率的一种电子器件。
- 1880 年由雅克·居里与皮埃尔·居里发现压电效应。
- 一战期间 保罗·朗之万首先探讨了石英谐振器在声纳上的应用。
- 1917 第一个由晶体控制的电子式振荡器。
- 1918 年贝尔实验室的 Alexander M. Nicholson 取得专利,虽然与同时申请专利的 Walter Guyton Cady 曾有争议。
- 1921 年 Cady 制作了第一个石英晶体振荡器。
Wikipedia 石英晶体谐振器
现在一般长这样,焊在计算机主板上,
Fig. A miniature 16 MHz quartz crystal enclosed in a hermetically sealed HC-49/S package, used as the resonator in a crystal oscillator. Image source wikipedia
受物理特性的限制,只有几十 MHz。
计算机的内存、PCIe 设备、CPU 等等组件需要的工作频率不一样(主要原因之一是其他组件跟不上 CPU 的频率), 而且都远大于几十 MHz,因此需要对频率做提升。工作原理:
有个视频解释地很形象,
Fig. Scaling up crystal frequency for different components of a computer. Image source Youtube
图中的 clock generator 是个专用芯片,也是焊在主板上,一般跟晶振挨着。
~20MHz
提升到 ~3GHz
的本节稍微再开展一下,看看 CPU 频率是如何提升到我们常见的 ~3GHz 这么高的。
CLK
引脚结合上面的图,时钟信号的传递/提升路径:
~20MHz
)时钟信号连接到 CPU 的一个名为 CLK
的引脚。
两个具体的 CLK 引脚实物图:
Intel 486 处理器(1989
)
Fig. Intel 486 pin mapImage Source
这种 CPU 引脚今天看来还是很简单的,CLK 在第三行倒数第三列。
AMD SP3 CPU Socket (2017
)
EPYC 7001/7002/7003 系列用的这种。图太大了就不放了,见 SP3 Pin Map。
现代 CPU 内部一般还有一个 clock generator
,可以继续提升频率,
最终达到厂商宣传里的基频(base frequency)或标称频率(nominal frequency),例如 EPYC 6543 的 2795MHz。
这跟原始晶振频率比,已经提升了上百倍。
介绍点必要的背景知识,有基础的可跳过。
Fig. 32-bit x86 general purpose registers [1]
计算机执行的所有代码,几乎都是经由通用寄存器完成的。 进一步了解:简明 x86 汇编指南(2017)。
如名字所示,用于特殊目的,一般也需要配套的特殊指令读写。大致分为几类:
mode-specific registers (MSR)
接下来我们主要看下 MSR 类型。
MSR
)MSR 是 x86 架构中的一组控制寄存器(control registers),
设计用于 debugging/tracing/monitoring 等等目的,以下是 AMD
的一些系统寄存器,
其中就包括了 MSR 寄存器们,来自 AMD64 Architecture Programmer’s Manual, Volume 3 (PDF),
Fig. AMD system registers, which include some MSR registers
几个相关的指令:
RDMSR/WRMSR
指令:读写 MSR registers;CPUID
指令:检查 CPU 是否支持某些特性。RDMSR/WRMSR 指令使用方式:
- 需要 priviledged 权限。
- Linux
msr
内核模块创建了一个伪文件/dev/cpu/{id}/msr
,用户可以读写这个文件。还有一个msr-tools
工具包。
MSR
之一:TSC
今天我们要讨论的是 MSR 中与时间有关的一个寄存器,叫 TSC (Time Stamp Counter)。
Time Stamp Counter
(TSC) 是 X86 处理器
(Intel/AMD/…)中的一个 64-bit 特殊目的 寄存器,属于 MRS 的一种。
还是 AMD 编程手册中的图,可以看到 MSR 和 TSC 的关系:
Fig. AMD system registers, which include some MSR registers
注意:在多核情况下(如今几乎都是多核了),每个物理核(processor)都有一个 TSC register,
或者说这是一个 per-processor register
。
cycles
数量前面已经介绍过,时钟信号经过层层提升之后,最终达到 CPU 期望的高运行频率,然后就会在这个频率上工作。
这里有个 CPU cycles
(指令周期)的概念:
频率没经过一个周期(1Hz),CPU cycles 就增加 1 —— TSC 记录的就是从 CPU 启动(或重置)以来的累计 cycles。
这也呼应了它的名字:时间戳计数器。
根据以上原理,如果 CPU 频率恒定且不存在 CPU 重置的话,
所以无怪乎 TSC 被大量用户空间程序当做开销地高精度的时钟。
本质上用户空间程序只需要一条指令(RDTSC
),就能读取这个值。非常简单的几行代码:
unsigned long long rdtsc() {
unsigned int lo, hi;
__asm__ volatile ("rdtsc" : "=a" (lo), "=d" (hi));
return ((unsigned long long)hi << 32) | lo;
}
就能拿到当前时刻的 cpu cycles。所以统计耗时就很直接:
start = rdtsc();
// business logic here
end = rdtsc();
elapsed_seconds = (end-start) / cycles_per_sec;
以上的假设是 TSC 恒定,随着 wall time 均匀增加。
如果 CPU 频率恒定的话(也就是没有超频、节能之类的特殊配置),cycles 就是以恒定速率增加的, 这时 TSC 确实能跟时钟保持同步,所以可以作为一种获取时间或计时的方式。 但接下来会看到,cycles 恒定这个前提条件如今已经很难满足了,内核也不推荐用 tsc 作为时间度量。
乱序执行会导致 RDTSC 的执行顺序与期望的顺序发生偏差,导致计时不准,两种解决方式:
- 插入一个同步指令(a serializing instruction),例如
CPUID
,强制前面的指令必现执行完,才能才执行 RDTSC;- 使用一个变种指令 RDTSCP,但这个指令只是对指令流做了部分顺序化(partial serialization of the instruction stream),并不完全可靠。
如果一台机器只有一个处理器,并且工作频率也一直是稳定的,那拿 TSC 作为计时方式倒也没什么问题。 但随着下面这些技术的引入,TSC 作为时钟就不准了:
还有其他一些方面的挑战,都会导致无法保证一台机器多个 CPU 的 TSC 严格同步。
解决方式之一,是一种称为恒定速率(constant rate) TSC 的技术,
cat /proc/cpuinfo | grep constant_tsc
来判断;较新的 Intel、AMD 处理器都支持这个特性。
但是,constant_tsc 只是表明 CPU 有提供恒定 TSC 的能力, 并不表示实际工作 TSC 就是恒定的。后面会详细介绍。
从上面的内容已经可以看出, TSC 如其名字“时间戳计数器”所说,确实本质上只是一个计数器, 记录的是 CPU 启动以来的 cpu cycles 次数。
虽然在很多情况下把它当时钟用,结果也是正确的,但这个是没有保证的,因为影响它稳定性的因素太多了 —— 不稳拿它计时也就不准了。
另外,它是一个 x86 架构的特殊寄存器,换了其他 cpu 架构可能就不支持,所以依赖 TSC 的代码可移植性会变差。
以上几节介绍的基本都是硬件问题,很好理解。接下来设计到软件部分就复杂了,一部分原因是命名导致的。
clocksource
)配置我们前面提到不要把 tsc 作为时钟来看待,它只是一个计数器。但另一方面,内核确实需要一个时钟,
gettimeofday() / clock_gettime()
。在底层,内核肯定是要基于启动以来的计数器,这时 tsc 就成为它的备选之一(而且优先级很高)。
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
tsc
:优先ns
级别;hpet
:性能开销太大原理暂不展开,只说结论:相比 tsc,hpet 在很多场景会明显导致系统负载升高。所以能用 tsc 就不要用 hpet。
turbostat
查看实际 TSC 计数前面提到用户空间程序写几行代码就能方便地获取 TSC 计数。所以对监控采集来说,还是很方便的。 我们甚至不需要自己写代码获取 TSC,一些内核的内置工具已经实现了这个功能,简单地执行一条 shell 命令就行了。
turbostat
是 Linux 内核自带的一个工具,可以查看包括 TSC 在内的很多信息。
turbostat 源码在内核源码树中:tools/power/x86/turbostat/turbostat.c。
不加任何参数时,turbostat 会 5s
打印一次统计信息,内容非常丰富。
我们这里用精简模式,只打印每个 CPU 在过去 1s 的 TSC 频率和所有 CPU 的平均 TSC:
# sample 1s and only one time, print only per-CPU & average TSCs
$ turbostat --quiet --show CPU,TSC_MHz --interval 1 --num_iterations 1
CPU TSC_MHz
- 2441
0 2445
64 2445
1 2445
用合适的采集工具把以上数据送到监控平台(例如 Prometheus/VictoriaMetrics),就能很直观地看到 TSC 的状态。 例如下面是 1 分钟采集一次,每次采集过去 1s 内的平均 TSC,得到的结果:
Fig. TSC runnning average of an AMD EPYC 7543 node
constant_tsc
: a feature, not a runtime guaranteeAMD EPYC 7543 CPU 信息:
$ cat /proc/cpuinfo
...
processor : 127
vendor_id : AuthenticAMD
model name : AMD EPYC 7543 32-Core Processor
cpu MHz : 3717.449
flags : fpu ... tsc msr rdtscp constant_tsc nonstop_tsc cpuid tsc_scale ...
flags 里面显式支持 constant_tsc
和 nonstop_tsc
,所以按照文档的描述 TSC 应该是恒定的。
但是,看一下下面的监控,都是这款 CPU,机器来自两个不同的服务器厂商,
Fig. TSC fluctuations (delta of running average) of AMD EPYC 7543 nodes, from two server vendors
可以看到,
这个波动可能有几方面原因,比如各厂商的 BIOS 逻辑,或者 SMI 中断风暴。
TSC 可写,所以某些 BIOS 固件代码会修改 TSC 值,导致操作系统时序不同步(或者说不符合预期)。
例如,2010 年内核社区的一个讨论 x86: Export tsc related information in sysfs 就提到,某些 BIOS SMI handler 会通过修改 TSC value 的方式来隐藏它们的执行。
为什么要隐藏?
前面提到,恒定 TSC 特性只是说处理器提供了恒定的能力,但用不用这个能力,服务器厂商有非常大的决定权。
某些厂商的固件代码会在 TSC sync 逻辑中中修改 TSC 的值。 这种修改在固件这边没什么问题,但会破坏内核层面的时序视角,例如内核调度器工作会出问题。 因此,内核最后引入了一个 patch 来处理 ACPI suspend/resume,以保证 TSC sync 机制在操作系统层面还是正常的,
x86, tsc, sched: Recompute cyc2ns_offset's during resume from sleep states
TSC's get reset after suspend/resume (even on cpu's with invariant TSC
which runs at a constant rate across ACPI P-, C- and T-states). And in
some systems BIOS seem to reinit TSC to arbitrary large value (still
sync'd across cpu's) during resume.
This leads to a scenario of scheduler rq->clock (sched_clock_cpu()) less
than rq->age_stamp (introduced in 2.6.32). This leads to a big value
returned by scale_rt_power() and the resulting big group power set by the
update_group_power() is causing improper load balancing between busy and
idle cpu's after suspend/resume.
This resulted in multi-threaded workloads (like kernel-compilation) go
slower after suspend/resume cycle on core i5 laptops.
Fix this by recomputing cyc2ns_offset's during resume, so that
sched_clock() continues from the point where it was left off during
suspend.
上一节提到,BIOS SMI handler 通过修改 TSC 隐藏它们的执行。如果有大量这种中断(可能是有 bug), 就会导致大量时间花在中断处理时,但又不会计入 TSC,最终导致系统出现卡顿等问题。
AMD 的机器比较尴尬,看不到 SMI 统计(试了几台 Intel 机器是能看到的),
$ turbostat --quiet --show CPU,TSC_MHz,SMI --interval 1 --num_iterations 1
CPU TSC_MHz
- 2441
0 2445
64 2445
1 2445
...
例如
本文整理了一些 TSC 相关的软硬件知识,在一些故障排查场景可能会用到。