普通视图

发现新文章,点击刷新页面。
昨天以前首页

[译] 硬盘(HDD)内部结构与工作原理的动画展示(2022)

2024年11月16日 08:00

译者序

本文翻译自 2022 年 Branch Education 的一个科普视频 How do Hard Disk Drives Work? (Youtube), 强烈推荐观看原视频(上不了油管的,B 站也有搬运)。本文整理个图文版方便查阅与思考, 另外增加了点相关的 Linux 软件知识,方便与日常工作衔接起来,

其他优质文章:

  1. How does a hard drive work, https://www.explainthatstuff.com/, 2024

    除了硬件拆解和介绍工作原理,还对比了 HDD 和 SDD,并且更重要的,介绍了 IBM 发明硬盘的历史

  2. How a Hard Drive Works, cs.stanford.edu, 2012

    斯坦福的一个老师实物教学,开盖展示读写数据时,硬盘的工作过程(然后这个盘就废了)。

  3. HDD from Inside: Hard Drive Main Parts, https://hddscan.com/

    硬件拆解部分比本文更详细,想了解更多硬件细节的,可作为补充。

水平及维护精力所限,译文不免存在错误或过时之处,如有疑问,请查阅原视频。 传播知识,尊重劳动,年满十八周岁,转载请注明出处

以下是译文。



原视频由 PCBWay 赞助,感谢赞助商。

1 硬盘拆解

1.1 盘片(platter)

盘片是存储数据的地方,

Disk/platter

  • 根据存储容量的不同,硬盘可能会有多个盘片堆叠,如上面右图所示;
  • 磁盘由铝镁合金(aluminum magnesium alloy)和其他合金的多个涂层组成,

    Disk/platter

  • 磁性功能层是 120nm 的钴铬钽合金薄层(cobalt chromium tantalum alloy), 它由磁性微块组成,磁极方向能变,

    Disk/platter

  • 盘片安装在主轴上,主轴使用中心的无刷直流电机(brushless DC motor)以 7200rpm 的等速度旋转。

1.2 机械臂装置

机械臂装置包括好几个组成部分,分别来看下。

1.2.1 机械臂(arm)

每个盘(platter)上下各有一个臂(arm),

1.2.2 滑橇(slider)和读写头(read/write head)

每个臂的末端有一个称为 slider(滑橇、滑块)的模块,它里面又包括了一个读/写头 (注意,读头和写头是分开的两个部件,后面会详细介绍),

磁盘高速旋转产生的气流能使这个滑块(和读写头)浮起来, 稳定运行在离磁盘表面 15nm(约 100 个原子)的地方,如下面的动图所示,

Fig. 高速旋转的盘片产生的气流使滑橇和读写头飘起来

1.2.3 读写头停靠装置

只有当盘片全速旋转时(有数据读写任务),机械臂才会转到磁盘表面上。 平时盘片不旋转时(没有读写任务),机械臂会停在磁盘边上的一个小塑料装置上。

1.2.4 尾部音圈电机(马达)

机械臂的尾部有一个 音圈电机(voice coil motor),或称音圈马达,它由线圈(coil of wire)和上下两个强钕磁铁(strong neodymium magnets)组成,

VCM(Voice Coil Motor)一种特殊形式的直接驱动电机,原理和扬声器类似,固得名。 通电线圈在磁场内就会产生力,力的大小与施加在线圈上的电流成比例,运动轨迹可以是直线也可以是弧线。 具有结构简单、体积小、速度快、响应快等特点。译注。

线圈通电之后会产生一个力,使机械臂在磁盘上移动(可以正向也可以反向),

这种马达的速度和精度:

  • 速度:读/写头能够在不同磁道上来回移动 ~20 次/秒
  • 精度:读/写头位置精度 ~30nm

1.3 机械臂-电路板之间的数据线

如下图所示,一条柔性电线(a flexible ribbon of wires)沿着机型臂的侧面布线,

  • 一边连接到读/写头
  • 一边连接到一个连接器(connector),该 connector 进一步连接到硬盘的主板,或称印刷电路板(PCB)。

1.4 PCB 和上面的芯片

PCB 上面的东西如下图所示,

这里主要介绍三个芯片:

  1. 主处理器芯片
  2. 内存芯片,作为主处理器的 cache;
  3. 控制音圈马达磁盘主轴电机的芯片。

1.5 数据线接口(e.g. SATA)和电源线接口

PCB 边缘还有两个硬件接口,

  • 数据接口:例如 SATA 接口,用于和电脑主板相连传输数据;
  • 电源接口:用于给 HDD 供电。

1.6 防尘装置

再看一下硬盘的两个防尘装置,

  1. 垫圈:将磁盘密封起来;
  2. 灰尘过滤器:用于捕获灰尘颗粒。

密封和过滤都是非常必要的,因为读写头距离盘片仅 15nm, 而灰尘颗粒的大小可达 10,000nm, 如果与 7200rpm 高速旋转磁盘碰撞,可能会造成严重损坏,

Fig. 读写头正常运行时,距离盘片仅 15nm。

2 盘片的微观组成

了解了粗粒度的硬件构成之后,现在让来深入到盘片的内部,看看它的微观组成。

2.1 磁盘(disk) -> 磁道(track)

首先,每个磁盘以同心圆的方式分割为多个磁道(concentric circles of tracks),

Fig. 磁盘分割为大量磁道。

每个磁盘的磁道数量能达到 500,000 个甚至更多。

2.2 磁道(track) -> 扇区(sector)

然后,沿着直径的方向,所有磁道又被分割为多个扇区,

Fig. 磁道进一步分割为扇区。

2.3 扇区内

现在看一下每个扇区内的结构,

Fig. 每个扇区的内部结构。

如上图所示,每个扇区中,依次包含五部分。

2.3.1 前导/同步区(preamble or synchronization zone)

记录这个旋转磁盘的确切速度每个比特位的长度(length of each bit of data)。

2.3.2 地址区

帮助读/写头确定当前位于哪个磁道和扇区

2.3.3 数据区

扇区大小

扇区的大小因盘而异,例如老一些的盘是 512 字节或 2KB,新一些的通常是 4KB。

查看磁盘扇区大小(译注)

有很多工具可以查看,lsblk 指定显示磁盘名字、物理扇区大小和逻辑扇区大小:

$ lsblk -o NAME,PHY-SeC,LOG-SeC
NAME                   PHY-SEC LOG-SEC
sda                       4096     512  # 这块是 SATA SSD
sdb                        512     512  # 这块是 SATA HDD

fdisk -l,这个命令好记:

$ fdisk -l
Disk /dev/sdb: 2.18 TiB, 2399276105728 bytes, 4686086144 sectors
Disk model: XXX                                                    # 硬盘型号
Units: sectors of 1 * 512 = 512 bytes                              # 当前扇区大小
Sector size (logical/physical): 512 bytes / 512 bytes              # 逻辑值 & 物理支持的最大值
I/O size (minimum/optimal): 512 bytes / 512 bytes

iostat 磁盘读写带宽(译注)

可以通过 cat /proc/diskstats 查看磁盘的读写情况,其中就包括了每个磁盘已经读写的 sectors 数量:

$ cat /proc/diskstats
#                            r_sectors               w_sectors
   8       0 sda 31663 10807 2928442     8471 203024 106672     6765800 ...

这个数量乘以 sector 大小,就是已经读写的字节数,iostat 等工具显示的磁盘读写带宽,就是根据这个来计算(估算)的。

2.3.4 纠错码(ECC)区

Fig. 每个扇区的内部结构。

用于校验存储在块中的数据。

2.3.5 扇区之间的间隔区

给了读/写磁头一定的容错能力。

3 写数据

现在让我们进一步看看读/写磁头的内部机制,以及写头(write head)是是如何写数据的。

3.1 磁场微块和磁化

扇区是由一个个磁场微块组成的, 写头通过改变磁盘微块的磁化方向来实现数据写入,

每个磁盘微块大小约为 90nm x 100nm x 125nm

磁化之外,微块内原子的南北极是随机的; 磁化之,微块所有原子的北南极都指向同一方向,

每个微块对应的就是一个 bit 数据,

3.2 写入 1bit 的过程

下面具体看一下如何磁化一个微块(相当于写入 1bit 数据)。

电流施加到 write head 的线圈之后,就会在此处产生一个强磁场,

这个磁场沿着 write head 向下,聚焦到尖端的一个小点,改变它正下方的磁盘微块极性 (中间的缝隙就是前面提到过的读写头 15nm 悬浮高度),

磁化之后的微块变成永磁体,能保持这个状态很多年,也就是数据已经持久化, 以后可以重复用读头感应这个永久磁场,读出存储的数据。

3.3 覆盖写

原理跟上面一样,也是逐 bit 来。 如果新写入的 bit 跟已经存储的一样,磁极就不变,否则就改变一下方向。

4 读数据

再来看看如何从磁盘读数据。

4.1 如何表示 0 和 1

4.1.1 不是用南北极指向表示

前面我们假设了不同南北极的磁块分别表示 0 和 1,

这在概念上非常简单,但实际实现并非如此。

4.1.2 用南北极指向的变化表示

实际的 read head,检测的是相邻两个微块的磁极变化, 这是因为磁极变化的强度单个微块的磁场强度要大得多,所以这种方式的检测准确率非常高

所以,如上图所示,

  • 相邻微块磁场方向变化,表示 1;
  • 相邻微块磁场方向不变,表示 0。

4.2 读头(read head)内部结构

那么,检测这些磁场的读头内部结构是怎样的呢?

如上图所示,

  • 读头里面是多层导电材料,由铁磁材料和非磁性材料的交替组成。
  • 这种多层材料具有一种称为巨磁阻(giant magnetoresistance, GMR)的特性, 简单来说,穿过它的磁场强度发生变化时,它的电阻率就会变化

4.3 读取数据:GMR 和读头电阻率

基于 GMR 特性,根据读头的电阻率就能判断下面存储的 0 还是 1,

  • 电阻率较低时,表示读取头下方磁场变化强,对应存储的是 bit 1
  • 电阻率较高且无磁场时,对应存储的是 bit 0

4.4 连续 0 的问题

以上过程有一个问题:如果较长连续区域的磁极都一样,对应的就是一长串的 0,由于读头的精度,有可能会导致多读或少读几个 0,导致数据错乱。

解决方少:利用每个 sector 的前导区和纠错码区中的信息。

5 致谢

原作者 Branch Education 感谢所有个人赞助者和会员赞助商,让他们制作了如此精良的科普视频。

6 Linux 存储相关的子系统和软件栈(译注)

6.1 从进程 read/write 请求到 HDD 读写数据

来自 Linux Storage Stack Diagram, 涵盖了 3.x ~ 6.x 多个内核版本,这里先贴一个 3.x 的,因为简单, 方便看出从用户进程发出 read/write 请求到 HDD 读写数据的内核模块链路:

虚拟文件系统(VFS)里面分为几类:

  1. 常规文件系统(ext4, xfs, btrfs, …);
  2. 网络文件系统(NFS, CIFS, …);
  3. 伪文件系统(procfs, sysfs, …);
  4. 特殊文件系统(tmpfs, devtmpfs, …)。

再贴一个 kernel v6.9 的,

6.2 内核 block layer 深入解读

  1. A block layer introduction part 1: the bio layer, LWN.net, 2017
  2. A block layer introduction part 2: the request layer, LWN.net, 2017

Written by Human, Not by AI Written by Human, Not by AI

Linux 时钟源之 TSC:软硬件原理、使用场景、已知问题(2024)

2024年7月28日 08:00

本文整理了一些 Linux 时钟源 tsc 相关的软硬件知识,在一些故障排查场景可能会用到。

Fig. Scaling up crystal frequency for different components of a computer. Image source Youtube

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 计算机组件的运行频率

1.1 时钟源:~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

1.2 Clock generator:针对不同部分(内存、PCIe、CPU 等)倍频

计算机的内存、PCIe 设备、CPU 等等组件需要的工作频率不一样(主要原因之一是其他组件跟不上 CPU 的频率), 而且都远大于几十 MHz,因此需要对频率做提升。工作原理:

  1. What is a CPU clock physically?
  2. Wikipedia: Phase-locked_loop (PLL)

有个视频解释地很形象,

Fig. Scaling up crystal frequency for different components of a computer. Image source Youtube

图中的 clock generator 是个专用芯片,也是焊在主板上,一般跟晶振挨着。

1.3 CPU 频率是如何从 ~20MHz 提升到 ~3GHz

本节稍微再开展一下,看看 CPU 频率是如何提升到我们常见的 ~3GHz 这么高的。

1.3.1 传递路径:最终连接到 CPU CLK 引脚

结合上面的图,时钟信号的传递/提升路径

  1. 晶振(~20MHz
  2. 主板上的 clock generator 芯片
  3. 北桥芯片
  4. CPU

时钟信号连接到 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

1.3.2 CPU 内部:还有一个 clock generator

现代 CPU 内部一般还有一个 clock generator,可以继续提升频率, 最终达到厂商宣传里的基频(base frequency)或标称频率(nominal frequency),例如 EPYC 6543 的 2795MHz。 这跟原始晶振频率比,已经提升了上百倍。

2 x86 架构的寄存器

介绍点必要的背景知识,有基础的可跳过。

2.1 通用目的寄存器

Fig. 32-bit x86 general purpose registers [1]

计算机执行的所有代码,几乎都是经由通用寄存器完成的。 进一步了解:简明 x86 汇编指南(2017)

2.2 特殊目的寄存器

如名字所示,用于特殊目的,一般也需要配套的特殊指令读写。大致分为几类:

  • control registers
  • debug registers
  • mode-specific registers (MSR)

接下来我们主要看下 MSR 类型。

2.2.1 model-specific register (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 工具包。

2.2.2 MSR 之一:TSC

今天我们要讨论的是 MSR 中与时间有关的一个寄存器,叫 TSC (Time Stamp Counter)。

3 TSC(时间戳计数器)

3.1 本质:X86 处理器中的一个 特殊寄存器

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

3.2 作用:记录 cpu 启动以来累计的 cycles 数量

前面已经介绍过,时钟信号经过层层提升之后,最终达到 CPU 期望的高运行频率,然后就会在这个频率上工作。

这里有个 CPU cycles(指令周期)的概念: 频率没经过一个周期(1Hz),CPU cycles 就增加 1 —— TSC 记录的就是从 CPU 启动(或重置)以来的累计 cycles。 这也呼应了它的名字:时间戳计数器

3.3 实际:经常被当做(高精度)时钟用

根据以上原理,如果 CPU 频率恒定且不存在 CPU 重置的话,

  • TSC 记录的就是系统启动以来的 cycles 数量
  • cycles 可以精确换算成时间
  • 这个时间的精度还非常高!
  • 使用开销还很低(这涉及到操作系统和内核实现了)。

所以无怪乎 TSC 被大量用户空间程序当做开销地高精度的时钟

3.3.1 使用代码

本质上用户空间程序只需要一条指令(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;

3.3.1 潜在问题

以上的假设是 TSC 恒定,随着 wall time 均匀增加。

如果 CPU 频率恒定的话(也就是没有超频、节能之类的特殊配置),cycles 就是以恒定速率增加的, 这时 TSC 确实能跟时钟保持同步,所以可以作为一种获取时间或计时的方式。 但接下来会看到,cycles 恒定这个前提条件如今已经很难满足了,内核也不推荐用 tsc 作为时间度量。

乱序执行会导致 RDTSC 的执行顺序与期望的顺序发生偏差,导致计时不准,两种解决方式:

  • 插入一个同步指令(a serializing instruction),例如 CPUID,强制前面的指令必现执行完,才能才执行 RDTSC;
  • 使用一个变种指令 RDTSCP,但这个指令只是对指令流做了部分顺序化(partial serialization of the instruction stream),并不完全可靠。

3.4 挑战:TSC 的准确性越来越难以保证

如果一台机器只有一个处理器,并且工作频率也一直是稳定的,那拿 TSC 作为计时方式倒也没什么问题。 但随着下面这些技术的引入,TSC 作为时钟就不准了:

  • 多核处理器:意味着每个核上都有一个 TSC,如何保持这些 TSC 寄存器值的严格同步;
  • 不同处理器的温度差异也会导致 TSC 偏差
  • 超线程:一个处理器上两个硬件线程(Linux 中看就是两个 CPU);
  • 超频、降频等等功耗管理功能:导致时钟不再是稳定的;
  • CPU 指令乱序执行功能:获取 TSC 的指令的执行顺序和预期的可能不一致,导致计时不准;
  • 休眠状态:恢复到运行状态时重置 TSC;

还有其他一些方面的挑战,都会导致无法保证一台机器多个 CPU 的 TSC 严格同步

3.5 改进:引入 constant/invariant TSC

解决方式之一,是一种称为恒定速率(constant rate) TSC 的技术,

  • 在 Linux 中,可以通过 cat /proc/cpuinfo | grep constant_tsc 来判断;
  • 有这个 flag 的 CPU,TSC 以 CPU 的标称频率(nominal frequency)累积;超频或功耗控制等等导致的实际 CPU 时钟频率变化,不会影响到 TSC。

较新的 Intel、AMD 处理器都支持这个特性。

但是,constant_tsc 只是表明 CPU 有提供恒定 TSC 的能力, 并不表示实际工作 TSC 就是恒定的。后面会详细介绍。

3.5 小结:计数器(counter),而非时钟(clock)

从上面的内容已经可以看出, TSC 如其名字“时间戳计数器”所说,确实本质上只是一个计数器, 记录的是 CPU 启动以来的 cpu cycles 次数

虽然在很多情况下把它当时钟用,结果也是正确的,但这个是没有保证的,因为影响它稳定性的因素太多了 —— 不稳拿它计时也就不准了。

另外,它是一个 x86 架构的特殊寄存器,换了其他 cpu 架构可能就不支持,所以依赖 TSC 的代码可移植性会变差。

4 查看和监控 TSC 相关信息

以上几节介绍的基本都是硬件问题,很好理解。接下来设计到软件部分就复杂了,一部分原因是命名导致的。

4.1 Linux 系统时钟源(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

4.1.1 tsc:优先

  • 高精度:基于 cycles,所以精度是几个 GHz,对应 ns 级别;
  • 低开销:跟内核实现有关。

4.1.2 hpet:性能开销太大

原理暂不展开,只说结论:相比 tsc,hpet 在很多场景会明显导致系统负载升高。所以能用 tsc 就不要用 hpet。

4.2 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

turbostat 如果执行的时间非常短,比如 1s,统计到数据就不太准,偏差比较大; 持续运行一段时间后,得到的数据才比较准。

4.3 rdtsc/rdtscp 指令采集 TSC 计数

4.3.1 C 代码

完整代码:

#include <stdio.h>
#include <time.h>
#include <unistd.h>

// https://stackoverflow.com/questions/16862620/numa-get-current-node-core
unsigned long rdtscp(int *chip, int *core) {
    unsigned a, d, c;
    __asm__ volatile("rdtscp" : "=a" (a), "=d" (d), "=c" (c));

    *chip = (c & 0xFFF000)>>12;
    *core = c & 0xFFF;
    return ((unsigned long)a) | (((unsigned long)d) << 32);;
}

int main() {
    int sleep_us = 100000;
    unsigned long tsc_nominal_hz = 2795000000;
    unsigned long expected_inc = (unsigned long)(1.0 * sleep_us / 1000000 * tsc_nominal_hz);
    unsigned long low = (unsigned long)(expected_inc * 0.95);
    unsigned long high = (unsigned long)(expected_inc * 1.05);
    printf("Sleep interval: %d us, expected tsc increase range [%lu,%lu]\n", sleep_us, low, high);

    unsigned long start, delta;
    int start_chip=0, start_core=0, end_chip=0, end_core=0;

    while (1) {
        start = rdtscp(&start_chip, &start_core);
        usleep(sleep_us);
        delta = rdtscp(&end_chip, &end_core) - start;

        if (delta > high || delta < low) {
            time_t seconds = time(NULL); // seconds since Unix epoch (1970.1.1)
            struct tm t = *localtime(&seconds);
            printf("%02d-%02d %02d:%02d:%02d TSC jitter: %lu\n",
                    t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, delta);
            fflush(stdout);
        }
    }

    return 0;
}

几点说明:

  1. 程序 hardcode 了预期的 TSC 频率是 2795MHz
  2. 每 100ms 采集一次 TSC 计数,如果 TSC 计数的偏差超过 +/- 5%,就将这个异常值打印出来;
  3. 在哪个 chip/cpu 上执行的,这里没打印出来,有需要可以打印;
  4. 这个程序虽然采集很频繁,但开销很小,主要是因为 rdtscp 指令的开销很小。

4.3.2 执行效果

编译运行,

$ gcc tsc-checker.c -o tsc-checker

# print to stdout and copy to a log file, using stream buffer instead of line buffers
$ stdbuf --output=L ./tsc-checker | tee tsc.log
Sleep interval: 100000 us, expected tsc increase range [265525000,293475000]
08-05 19:46:31 303640792
08-05 20:13:06 301869652
08-05 20:38:27 300751948
08-05 22:40:39 324424884
...

可以看到这台机器(真实服务器)有偶发 TSC 抖动, 能偏离正常范围 324424884/2795000000 - 1 = 16%, 也就是说 100ms 的时间它能偏离 16ms,非常离谱。TSC 短时间连续抖动时, 机器就会出现各种奇怪现象,比如 load 升高、网络超时、活跃线程数增加等等,因为内核系统因为时钟抖动乱了。

4.4 监控

用合适的采集工具把以上数据送到监控平台(例如 Prometheus/VictoriaMetrics),就能很直观地看到 TSC 的状态。

4.4.1 基于 turbostat(不推荐)

例如下面是 1 分钟采集一次,每次采集过去 1s 内的平均 TSC,得到的结果:

Fig. TSC runnning average of an AMD EPYC 7543 node

但前面提到, turbostat 如果执行的时间非常短,统计到数据就不太准,偏差比较大; 持续运行一段时间后,得到的数据才比较准。但作为采集程序,可能不方便执行太长时间。

4.4.2 基于 rdtscp

基于上面的 rdtscp 自己写代码采集,就非常准确了,例如,下面是 1 分钟采集一次得到的结果展示:

Fig. TSC jitter of an AMD EPYC 7543 node

不过,要抓一些偶发抖动导致的问题,1 分钟采集一次粒度太粗了。比如我们上一小节的 C 程序是 100ms 采集一次, 相当于 1 分钟采集 600 次,一小时采集 3.6w 次。我们 3 个小时总共 10 万多次跑下来,也才能抓到几次抖动,这已经算很幸运了。

4.4.3 基于 rdtscp + 内核模块

还是 rdtscp,但作为内核模块 + 定时器运行,应该会比用户空间程序更准,可以避免 Linux 内核调度器的调度偏差。

5 TSC 若干坑

5.1 constant_tsc: a feature, not a runtime guarantee

5.1.1 Lenovo SR645 (AMD EPYC 7543 CPU) TSC 不稳定

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_tscnonstop_tsc,所以按照文档的描述 TSC 应该是恒定的。

但是,看一下下面的监控,都是这款 CPU,机器来自两个不同的服务器厂商,

Fig. TSC fluctuations (delta of running average) of AMD EPYC 7543 nodes, from two server vendors

可以看到,

  • 联想和浪潮的 TSC 都有波动,
  • 联想的偶尔波动非常剧烈(相对 base 2795MHz 偏离 16% 甚至更高);
  • 浪潮的相对较小(base 2445 MHz)。

这个波动可能有几方面原因,比如各厂商的 BIOS 逻辑,或者 SMI 中断风暴。

5.1.2 原因及解决方式

最后定位到是厂商 BIOS (UEFI) 设置导致的,做如下修改之后稳定多了,

No. Option Before After
1 OperatingModes.ChooseOperatingMode Maximum Efficiency Custom Mode
2 Processors.DeterminismSlider Performance Power
3 Processors.CorePerformanceBoost Enable Enable
4 Processors.cTDP Auto Maximum
5 Processors.PackagePowerLimit Auto Maximum
6 Processors.GlobalC-stateControl Enable Enable
7 Processors.SOCP-states Auto P0
8 Processors.DFC-States Enable Disable
9 Processors.P-state1 Enable Disable
10 Processors.SMTMode Enable Enable
11 Processors.CPPC Enable Enable
12 Processors.BoostFmax Auto Manual
13 Processors.BoostFmaxManual   0
14 Power EfficiencyMode Enable Disable
15 Memory.NUMANodesperSocket NPS1 NPS0

Note:

5.2 BIOS 设置致使 TSC 不恒定

除了以上具体配置,还有一些可能会导致 TSC 不稳的场景。

5.2.1 TSC 寄存器是可写的!

TSC 可写,所以某些 BIOS 固件代码会修改 TSC 值,导致操作系统时序不同步(或者说不符合预期)。

5.2.2 BIOS SMI handler 通过修改 TSC 隐藏它们的执行

例如,2010 年内核社区的一个讨论 x86: Export tsc related information in sysfs 就提到,某些 BIOS SMI handler 会通过修改 TSC value 的方式来隐藏它们的执行

为什么要隐藏?

5.2.3 服务器厂商出于功耗控制等原因在 BIOS 修改 TSC 同步逻辑

前面提到,恒定 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.

5.3 SMI 中断风暴导致 TSC 不稳

上一节提到,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
...

5.4 VM TSC 不稳

例如

  1. https://www.phoronix.com/news/AMD-Secure-TSC-Linux-Patches
  2. http://oliveryang.net/2015/09/pitfalls-of-TSC-usage/

6 总结

本文整理了一些 TSC 相关的软硬件知识,在一些故障排查场景可能会用到。

参考资料

  1. 简明 x86 汇编指南(2017)
  2. AMD64 Architecture Programmer’s Manual, Volume 3 (PDF)
  3. Linux 服务器功耗与性能管理(一):CPU 硬件基础(2024)
  4. Pitfalls of TSC usage, 2015
  5. Wikipedia MSR
  6. Wikipedia TSC
  7. Wikipedia Clock Generator

Written by Human, Not by AI Written by Human, Not by AI

❌
❌