本文采用 Linux 内核 v3.10 版本 x86_64 架构
一、概述
内存和 CPU 资源是 NUMA 节点的重要组成部分,所以内存和 CPU 拓扑信息对于构建 NUMA 系统至关重要。在 Linux Kernel:物理内存布局探测 一文中,我们介绍了物理内存布局的探测;本文我们会介绍 CPU 拓扑结构探测。在后续的文章中,我们会介绍 NUMA 节点的探测。
CPU 拓扑结构一般有 4 级:
- 逻辑处理器,比如超线程( HyperThreading,HT )
- Core
- Package
- NUMA 节点
习惯上,CPU 拓扑以冒号分割的形式表示。比如,"CPU 3:2:1:0" 表示 NUMA 节点 3 下的 Package2 下的 Core1 下的逻辑 CPU0 。
注:本文只关注前 3 级拓扑,NUMA 节点探测会在后续文章中介绍。
Intel 开发者手册(Intel 64 and IA-32 Architectures Software Developer Manuals,以下简称 Intel SDM)给出的处理器层级关系如下图所示:
在上图中,使用 SMT(Simultaneous MultiThreading)来表示逻辑处理器。SMT 是一种硬件多线程技术,Intel 公司将其实现为超线程( HyperThreading,HT)。
二、CPU 拓扑探测基本步骤
在介绍 CPU 拓扑探测步骤前,先介绍一些概念和原理。
注:以下内容都是基于 Intel 处理器的,AMD 和其它 x86 处理器厂商的内容与 Intel 处理器并不完全一致。
2.1 多处理器初始化协议(MP Initialization Protocol)
IA-32 架构定义了一个多处理器初始化协议,即 《Multiprocessor Specification Version 1.4》。通电启动后,系统会执行多处理器初始化协议对每一个逻辑处理器进行初始化。
该协议的主要操作如下:
-
基于系统拓扑结构,为每一个逻辑处理器分配一个唯一的 APIC ID。根据处理器支持的 APIC 模式不同,该值可能是 32 位或 8 位大小。
-
从可用的处理器中选出启动处理器 BSP ( Bootstrap Processor),其余的为应用处理器 AP(Application Processor)。
-
BSP 执行 BIOS 代码,创建 ACPI 表和/或 MP 表,并将自己的 APIC ID 添加到表中。
-
AP 执行 BIOS 初始化代码,并将自己的 APIC ID 添加到 ACPI 表和/或 MP 表中。
-
AP 完成 BIOS 初始化后,会保持在 halted 状态,等待来自 BSP 的唤醒信号。
-
BSP 执行完操作系统的初始化代码后,会发送启动信号唤醒 AP。
注:上述步骤为简化版本,详细的 MP 初始化流程,可参考 《Intel SDM: Volume 3A,9.4 MULTIPLE-PROCESSOR (MP) INITIALIZATION》。
2.2 使用 APIC ID 来标识处理器
对于现代 CPU,其内部一般都集成了高级可编程中断控制器(APIC)。APIC 分为 I/O APIC 和 Local APIC 两部分。其中,I/O APIC 集成在 Intel 系统芯片组,负责接收外部中断并转发给 Local APIC;Local APIC 集成在 CPU 内部,除了可以接收转发来的外部中断,还可以接收处理器间中断(Inter-processor interrupt ,IPI)、APIC计时器中断(APIC timer generated interrupt) 等多种中断。
在支持超线程的多处理器系统下,其中断系统架构如下图所示:
在通电启动时,系统硬件会为每一个 Local APIC 都分配唯一的 APIC ID。这些 APIC ID 是基于系统拓扑来分配的,包含系统的拓扑信息:Package ID、Core ID、SMT ID 等。在多处理器(MP)系统下,BIOS 和操作系统将 Local APIC ID 用作处理器 ID。
2.3 xAPIC 模式 vs x2APIC 模式
对于支持 APIC 的 x86 处理器,其中断模式分为 xAPIC 模式(xAPIC mode)以及 x2APIC 模式(x2APIC mode)。xAPIC 模式是基础模式,基于从 Pentium 4 以及 Intel Xeon 处理器开始使用的 xAPIC 架构;而 x2APIC 模式基于 x2APIC 架构,该架构兼容以前的 xAPIC 架构。xAPIC(eXtended APIC) 架构是对 APIC 架构(使用在 P6 family 处理器上)的扩展,而 x2APIC (eXtended xAPIC)架构是对 xAPIC 架构的扩展。
对于不同的模式,其 Local APIC ID 的位数是不同的:
Local APIC ID 保存在 Local APIC ID Register 中。在 xAPIC 模式下,对于 Pentium 4 、 Xeon 处理器以及更新的处理器来说,APIC ID 有 8 位,最多可以标识 255 个处理器;在 x2APIC 模式下,APIC ID 占用了 32 位,可以标识更多的处理器。
注:我们忽略掉古老的 P6 family 和 Pentium 处理器。
xAPIC 模式和 x2APIC 模式的另一个重大不同点,就是寄存器的使用方式不同。
Local APIC 中包含着一系列寄存器,比如 Local APIC ID Register(见上图)或者 Local APIC Version Register(见下图)。
在 xAPIC 模式下,Local APIC 的各种寄存器会映射到从物理地址 0xFEE00000 (该值可配置)开始的 4KB 内存区域。使用时,需要将该物理内存区域映射到系统的虚拟内存空间,即通过**内存映射(MMIO)**的方式访问。
当 x2APIC 模式下,这些寄存器被设计成了 MSR(Model Specific Register),不能通过内存映射(MMIO)的方式访问了,必须使用 rdmsr 和 wrmsr 指令访问。
不同模式下,寄存器地址的映射关系见下表:
比如,xAPIC 模式下,Local APIC ID Register 的偏移地址为 0x020,所以其物理地址为 0xFEE00000 + 0x020,即 0xFEE00020。在 x2APIC 模式下,对应的 MSR 地址为 0x802。
在上表中,我们只截取了前 2 个寄存器的映射关系,完整的映射请参考 《Intel SDM: Volume 3A, Table 11-6》。
2.4 CPUID 指令 -- 动态获取 CPU 信息
获取到 Local APIC ID 之后,还需要知道不同层级的位宽才能解析出拓扑信息。不同型号的处理器,各层级的位宽也是不同的,这些信息由处理器自身提供,这就要求我们能够在运行时动态获取位宽信息。Intel 处理器的 cpuid 指令提供了运行时获取处理器信息的能力,可用来帮助我们获取位宽并解析拓扑。
cpuid 指令需要在 EAX 寄存器中指定输入值(某些情况下,还需要在 ECX 寄存器中指定输入值),会在 EAX、 EBX、ECX、 EDX 寄存器中返回处理器的标识信息和能力特征。输入值不同,返回的结果也不同。
在下文中,我们将 EAX 寄存器的输入值称为查询向量 (vector),ECX 寄存器的输入值称为子查询向量(secondary vector)。将输入格式简写为 CPUID.vector.secondary_vector。比如当查询向量为 0x0 时,没有子查询,则输入简写为 CPUID.0x0;当查询向量为 0x4 ,子查询向量为 0x0 时,输入简写为 CPUID.0x4.0x0。将输出格式简写为 "输入格式:register[end_bit:start_bit]",比如当查询向量为 0x4 ,子查询向量为 0x0 时,输出结果 EAX 寄存器中位 31-26 的值,表示为 CPUID.0x4.0x0:EAX[31:26];如果输入比较明确,可以将输入格式省略,简化为 EAX[31:26]。
cpuid 指令的查询类型非常多,在解析 cpu 拓扑时,主要用到了以下几种查询。
2.4.1 CPUID EAX=0x00000000
该查询在 EBX、 EDX、 ECX 寄存器中返回生产商的标识字符串。对于 Intel 处理器,该字符串是“GenuineIntel”。
在 EAX 寄存器中返回 cpuid 指令支持的基础信息查询的最大向量。cpuid 的查询向量以 0x80000000 为分界线,分界线以下的是基础信息查询,分界线(包含)以上的是扩展信息查询。
"CPUID.0x0" 查询,输出结果如下:
| 寄存器 | 信息描述 |
|---|---|
| EAX | cpuid 指令支持的基础信息查询的最大输入值 |
| EBX | “Genu” |
| ECX | “ntel” |
| EDX | “ineI” |
EAX 寄存器中的返回结果,可用于检测处理器是否支持扩展拓扑查询(查询向量为 0xB)。
2.4.2 CPUID EAX=0x00000001
当查询向量为 0x1 时,输出结果如下:
| 寄存器 | 信息描述 |
|---|---|
| EAX | 版本信息:包括 Type、Family、Model、 Stepping ID。 |
| EBX | 位 07-00: 品牌索引 位 15-08: CLFLUSH 指令刷新的缓存行大小(以 8 字节为单位) 位 23-16: 当前 package 支持的最大逻辑处理器数量 位 31-24:初始 APIC ID(Initial APIC ID) |
| ECX | cpu 支持的能力信息 |
| EDX | cpu 支持的能力信息 |
提示:
- EBX[23:16] 需要向上圆整到 2 的整次幂,该值只有处理器支持超线程时(EDX[28] 为 1)才有效。
- EBX[31:24] 中保存的是的 8 位初始 APIC ID。如果查询向量 0xB 可用, 说明处理器支持扩展拓扑,需要用扩展拓扑中的 32 位 x2APIC ID 替换该值。换句话说,如果处理器支持扩展拓扑,APIC ID 应该用 32 位的 x2APIC ID;否则,使用 8 位的初始 APIC ID。
在输出结果中,我们感兴趣的是 EBX[23:16]、EDX[28] 以及 ECX[21]。EBX[23:16] 指示当前 package 支持的最大逻辑处理器数量;EDX[28] 是 HTT 标志位,指示处理器是否支持超线程;ECX[21] 是 x2APIC 标志位,指示处理器是否支持 x2APIC 功能。
2.4.3 CPUID EAX=0x0000000B
查询向量 0xB 用来获取处理器的扩展拓扑信息,在支持 x2APIC 功能的处理器上,该查询可以获取不同层级的位偏移。另外,该查询需要在 ECX 寄存器中指定子查询向量。该查询也是我们获取处理器拓扑的主要手段。
该查询的返回结果如下:
| 寄存器 | 信息描述 |
|---|---|
| EAX | 位 04-00:在 x2APIC ID 中获取下一层级的拓扑 ID 所要右移的位数 位 31-05:保留 |
| EBX | 位 15-00: 当前层级的逻辑处理器数量 位 31-16: 保留 |
| ECX | 位 07-00: 当前层级,与 ECX 寄存器中的输入值一致 位 15-08: 层级类型。0 - 无效;1 - SMT;2 - Core;其它 - 保留 位 31-16: 保留 |
| EDX | 位 31-00: 当前逻辑处理器的 x2APIC ID 。 |
对于三级拓扑来说,该函数需要调用 2 次。
第一次,EAX=0xB 且 ECX=0,返回结果中的 EAX[4:0] 就是 Core 层级的右移位数 core_id_shift。
第二次,EAX=0xB 且 ECX=1,返回结果中的 EAX[4:0] 就是 Package 层级的右移位数 package_id_shift。
尽管 Intel-64 处理器都支持 x2APIC 功能,但对于老式处理器来说,是否支持该功能是不确定的。另外,该功能也能够通过配置禁用掉。所以,在探测处理器拓扑时,可以通过以下步骤检测扩展拓扑功能是否可用:
- 检查 CPUID.0x0 查询返回的 EAX 寄存器值,即处理器支持的基本信息查询的最大向量。如果该值大于等于 0xB,则进行下一步
- CPUID.0x0B.0x0 查询返回值中 EBX 寄存器的值必须非 0。
如果以上 2 步全部满足,则说明扩展拓扑功能可用。
2.4.3 CPUID EAX=0x00000004
如果处理器不支持 ”CPUID.0xB“ 查询,那么就需要使用 ”CPUID.0x4“ 查询。该查询主要用来探测处理器的缓存参数,但返回结果中 EAX[26:31] 的值可用于探测拓扑信息。
该查询结果如下:
| 寄存器 | 信息描述 |
|---|---|
| EAX | 位 04-00:缓存类型 0 = Null - No more caches. 1 = Data Cache. 2 = Instruction Cache. 3 = Unified Cache. 4-31 = Reserved. 位 07-05:缓存级别,从 1 开始 位 08:Self Initializing cache level 位 09:Fully Associative cache 位 13-10:保留 位 25-14:共享当前缓存的最大逻辑处理器数量 位 31-26:当前 package 支持的最大 core 数量 |
| EBX | 与本文无关 |
| ECX | 与本文无关 |
| EDX | 与本文无关 |
提示:
- EAX[31:26] 中的值再加上 1,才是实际的结果(根据 Intel SDM 文档)
- EAX[31:26] + 1 的值需向上圆整到 2 的整次幂。比如 EAX[31:26] + 1 的值为 5,那实际值会向上圆整为 ,此时 core_id 占用的比特位数为 3,即 core_id_bits = 3。
我们在 CPUID.0x1:EBX[23:16] 中,获取到了package 中逻辑处理器的最大数量,该值同样需要向上圆整到 2 的整次幂。比如 EBX[23:16] = 10,那么要向上圆整到 ,说明实际可用的最大逻辑处理器数量为 16,此时 core_id 和 smt_id 总共占用的比特位数量为 4,即 core_plus_smt_bits(core_id_bits + smt_id_bits) 为 4。那么,就能就计算出 smt_id 所占用的位数为 1。
假如我们有一个函数 get_count_order 用于获取指定数值向上圆整到 2 的整次幂后的阶,那么 core_id 所占用的位数 core_id_bits 就可用通过 get_count_order(CPUID.0x4:EAX[31:26] + 1) 得到;而 smt_id 所占用的位数 smt_id_bits 就可以通过 get_count_order(CPUID.0x1:EBX[23:16]) - core_id_bits 得到。
各层级所占位数如下图所示。
core_plus_smt_bits:
core_id_bits:
smt_id_bits:
2.5 三级拓扑算法
结合 2.4 小节,我们能够得到三级拓扑结构的算法:
-
执行 “CPUID.0x0” 查询,获取处理器基本信息。
- EAX 寄存器中保存着 cpuid 指令所支持的基本信息查询的最大向量
- EBX、ECX、EDX 寄存器中保存处理器标识
-
执行 “CPUID.0x1” 查询,检查处理器是否支持超线程。
-
如果不支持 HTT 功能( EDX[28] 被清除),说明是不支持超线程的单核处理器。在这种情况下,smt_id_bits 和 core_id_bits 都为 0。拓扑探测完毕。
-
如果支持 HTT 功能,但 “CPUID.0x0” 的返回结果中 EAX 寄存器的值小于 0xB,说明不支持扩展拓扑,则跳到第 4 步执行。
-
-
执行 “CPUID.0xB” 查询,获取扩展拓扑信息。
-
如果 “CPUID.0xB.0x0” 的返回结果中,EBX 寄存器的值为 0,说明不支持扩展拓扑,则跳到第 4 步执行;
-
否则,说明支持扩展拓扑。通过改变 ECX 的输入值,可以获取到不同层级的位偏移 core_id_shift 以及 package_id_shift;
-
从 EDX[31:0] 中获取 32 位 x2APIC ID 的值。
-
拓扑探测完毕。
-
-
通过 “CPUID.0x4” 获取拓扑信息
如果处理器不支持扩展拓扑查询,那就会来到这一步。在这一步,由于需要将值向上圆整到 2 的整次幂,我们要定义一个函数 get_count_order 来获取圆整后的阶。
-
如果处理器不支持 “CPUID.0x4” 查询,说明这是一个支持超线程的单核处理器。则 smt_id 所占用的位数 smt_id_bits 可以通过get_count_order(CPUID.0x1:EBX[23:16]) 得到,此时 core_id_bits 为 0。拓扑探测完毕。
-
否则,执行 "CPUID.0x4" 查询,返回结果中 "EAX[31:26] + 1" 指示 package 支持的最大 core 数量。在 “CPUID.0x1:EBX[23:16]” 中 ,我们得到了 package 支持的最大逻辑处理器数量。此时,core_id 所占用的位数 core_id_bits 可以通过 get_count_order(CPUID.0x4:EAX[31:26] + 1) 得到;smt_id 所占用的位数 smt_id_bits 可以通过 get_count_order(CPUID.0x1:EBX[23:16]) - core_id_bits 得到。
-
从 “CPUID.0x1:EBX[31:24]” 获取 8 位 INITIAL APIC ID 的值。
-
拓扑探测完毕。
注:《Intel SDM: Volume 3A,9.9.4 Algorithm for Three-Level Mappings of APIC_ID》 中,提供了详细的伪代码。
2.6 MADT 格式
cpuid 指令能够动态获取当前处理器的拓扑信息,如果想获取完成的拓扑信息,就需要在每个处理器上执行 cpuid 指令。然后把所有处理器的信息收集起来,就能形成完整的拓扑结构。
根据多处理器初始化协议,AP 在 完成 BIOS 初始化后,会处于挂起等待状态;BSP 在完成操作系统初始化后,会唤醒 AP。在此之前,BSP 需要知道处理器的总数量,以决定要唤醒多少 AP。根据多处理器初始化协议,BSP 和 AP 在执行 BIOS 初始化时,会将各自的 APIC ID 写入 ACPI 中。所以,在 BSP 唤醒 AP 之前,所有处理器的 APIC ID 已经写入 ACPI ,具体的说,是写入了 ACPI 的 MADT(Multiple APIC Description Table) 表。所以,可以从 MADT 表中获取所有处理器的 APIC ID 信息,并计算出总数量。
MADT 描述了系统中所有的中断控制器的信息,由表头和不同类型的表项组成。
2.6.1 ACPI 通用表头
MADT 以一个 ACPI 通用表头开始,通用表头格式如下:
| Field | Byte Length | Byte Offset | Description |
|---|---|---|---|
| Signature | 4 | 0 | 表的签名,用来区分不同的表 |
| Length | 4 | 4 | 表的完整大小(包括表头) |
| Revision | 1 | 8 | Revision |
| Checksum | 1 | 9 | Entire table must sum to zero. |
| OEMID | 6 | 10 | OEM ID |
| OEM Table ID | 8 | 16 | OEM Table ID |
| OEM Revision | 4 | 24 | OEM Revision |
| Creator ID | 4 | 28 | Creator ID |
| Creator Revision | 4 | 32 | Creator Revision |
在通用表头中,我们只关注签名(Signature)和 长度(Length) 字段。签名字段用于区分不同的表,对于 MADT 表来说,该字段的值是 “APIC”。长度字段指示该表的总大小(包括表头)。
2.6.2 MADT 表头专用字段
在通用表头之后,是 MADT 表头的专用字段:
| Field | Byte Length | Byte Offset | Description |
|---|---|---|---|
| Local Interrupt Controller Address | 4 | 36 | The 32-bit physical address at which each processor can access its local interrupt controller. |
| Flags | 4 | 40 | Multiple APIC flags. |
”Local Interrupt Controller Address“ 字段是 32 位的 Local APIC 的物理基地址。如果 MADT 中定义了 ”Local APIC Address Override“ 表项,需要用此表项中的 64 位的 Local APIC 的物理基地址替换掉表头中的 32 位物理基地址。
如果 Flags 字段的位 0 被置位,说明系统具有 PC-AT 兼容的双 8259 中断模型。如果要启用 APIC 中断,那么必须屏蔽掉双 8259 中断。8259 系列是早期的中断控制器,详见 Intel 8259。
在 Flags 字段之后,是一系列表项,这些表项中保存着各种不同的中断功能信息。
MADT 中,与 Intel 处理器相关的表项主要有以下几种:
| Value | Description | **For Processor ** | For an I/O APIC |
|---|---|---|---|
| 0 | Processor Local APIC | yes | no |
| 1 | I/O APIC | no | yes |
| 2 | Interrupt Source Override | no | yes |
| 3 | Non-maskable Interrupt (NMI) Source | no | yes |
| 4 | Local APIC NMI | yes | no |
| 5 | Local APIC Address Override | no | no |
| 6 | I/O SAPIC | no | yes |
| 7 | Local SAPIC | yes | no |
| 8 | Platform Interrupt Sources | no | yes |
| 9 | Processor Local x2APIC | yes | no |
| 0xA | Local x2APIC NMI | yes | no |
| ...... | ...... | ...... | ...... |
在不同类型的表项中,我们主要关注与 Local APIC 相关的表项,即第 0、4、5、7、9、0xA 项。
其中,第 4 项和第 0xA 项,与 NMI 中断相关,本文未涉及。第 7 项 Local SAPIC(Streamlined Advanced Programmable Interrupt Controller)是给 Itanium (IA-64)处理器使用的,本文也未涉及。第 5 项保存着 64 位的 Local APIC 物理基地址,上文介绍过。
最后,只剩下第 0 项(Local APIC)和第 9 项(Local x2APIC),这 2 种结构是与 x86 架构相关的。Local APIC 中保存的是 8 位的 initial APIC ID,Local x2APIC 中保存的是 32 位的 x2APIC ID。具体使用哪个,需要根据一定逻辑来判断,判断逻辑详见 “2.4 CPUID 指令” 小节。
附:ACPI 规范支持的中断控制器类型:
- PC-AT 兼容的双 8259 中断控制器
- 基于Intel 处理器(x86 架构)的高级可编程中断控制器(Advanced Programmable Interrupt Controlle,APIC)
- 基于Intel 安腾处理器(Itanium,IA-64 架构)的 Streamlined Advanced Programmable Interrupt(SAPIC)
- 基于 ARM 处理的 Generic Interrupt Controller (GIC)
- 基于 LoongArch 处理器的 LoongArch Programmable Interrupt Controller (LPIC)
2.6.3 MADT 子表头
所有的 MADT 表项,都包含 2 个字段的通用子表头。第 1 个字段指示表项类型,占用 1 个字节;第 2 个字段指示表项大小,占用 1 个字节。
| Offset | Length | Description |
|---|---|---|
| 0 | 1 | Entry Type |
| 1 | 1 | Record Length |
2.6.4 Type 0: Processor Local APIC
| Field | Offset | Length | Description |
|---|---|---|---|
| Type | 0 | 1 | 0 |
| Length | 1 | 1 | 8 |
| ACPI Processor UID | 2 | 1 | ACPI Processor UID |
| APIC ID | 3 | 1 | The processor’s local APIC ID. |
| Flags | 4 | 4 | Flags (bit 0 = Processor Enabled) (bit 1 = Online Capable) |
该表项的 APIC ID 字段中,保存着 8 位的 APIC ID。
Flags 字段指示处理器是否可用。
| Local APIC Flags | Bit Length | Bit Offset | Description |
|---|---|---|---|
| Enabled | 1 | 0 | |
| Online Capable | 1 | 1 | |
| Reserved | 30 | 2 | Must be zero. |
如果 Flags 中的位 0 被置位,说明处理器可被系统使用。如果位 0 没有置位,就需要检查位 1。如果位 1 被置位,说明系统硬件支持操作系统在运行时启用该处理器;否则说明该处理器不能使用。
2.6.5 Type 5: Local APIC Address Override
该表项是可选的,如果定义了该表项,就需要用表项中的 64 位物理地址替换掉 MADT 表头中的 32 位地址。
| Field | Offset | Length | Description |
|---|---|---|---|
| Type | 1 | 1 | 5 |
| Length | 1 | 1 | 12 |
| Reserved | 2 | 2 | Reserved |
| Local APIC Address | 4 | 8 | 64-bit physical address of Local APIC |
2.6.6 Type 9: Processor Local x2APIC
该表项提供 x2APIC ID。
| Field | Offset | Length | Description |
|---|---|---|---|
| Type | 0 | 1 | 0 |
| Length | 1 | 1 | 16 |
| Reserved | 2 | 2 | Reserved - Must be zero |
| X2APIC ID | 4 | 4 | Processor's local x2APIC ID |
| Flags | 8 | 4 | Flags (same as the Local APIC flags) |
| ACPI Processor UID | 12 | 4 | ACPI Processor UID |
X2APIC ID 字段中,保存着 32 位的 X2APIC ID。
Flags 字段指示处理器是否可用,同 ”Processor Local APIC“ 表项。
2.7 总结
cpu 拓扑探测涉及到 APIC ID、CPUID 指令、MADT 表,其基本步骤如下:
- BSP 启动后,通过 cpuid 指令,获取自身的 APIC ID 、位宽以及中断模式等信息,解析出 BSP 的拓扑(解析过程详见 “2.4 CPUID 指令” 小节);
- BSP 从 ACPI 的 MADT 表中,获取到系统中所有处理器的 Local APIC ID,得到处理器总数量;
- BSP 按照处理器数量激活 AP ;
- AP 激活后,执行 cpuid 指令,解析处理器信息,得到 AP 的拓扑;
- 当所有处理器都执行过 cpuid 指令后,就得到系统完整的处理器拓扑。
三、数据结构
3.1 处理器相关数据结构
3.1.1 处理器信息的载体 -- struct cpuinfo_x86
cpuinfo_x86 结构体用来保存处理器信息,其定义如下:
// file: arch/x86/include/asm/processor.h
struct cpuinfo_x86 {
__u8 x86; /* CPU family */
__u8 x86_vendor; /* CPU vendor */
__u8 x86_model;
__u8 x86_mask;
#ifdef CONFIG_X86_32
......
#else
/* Number of 4K pages in DTLB/ITLB combined(in pages): */
int x86_tlbsize;
#endif
__u8 x86_virt_bits;
__u8 x86_phys_bits;
/* CPUID returned core id bits: */
__u8 x86_coreid_bits;
/* Max extended CPUID function supported: */
__u32 extended_cpuid_level;
/* Maximum supported CPUID level, -1=no CPUID: */
int cpuid_level;
__u32 x86_capability[NCAPINTS + NBUGINTS];
char x86_vendor_id[16];
char x86_model_id[64];
/* in KB - valid for CPUS which support this call: */
int x86_cache_size;
int x86_cache_alignment; /* In bytes */
int x86_power;
unsigned long loops_per_jiffy;
/* cpuid returned max cores value: */
u16 x86_max_cores;
u16 apicid;
u16 initial_apicid;
u16 x86_clflush_size;
/* number of cores as seen by the OS: */
u16 booted_cores;
/* Physical processor id: */
u16 phys_proc_id;
/* Core id: */
u16 cpu_core_id;
/* Compute unit id */
u8 compute_unit_id;
/* Index into per_cpu list: */
u16 cpu_index;
u32 microcode;
} __attribute__((__aligned__(SMP_CACHE_BYTES)));
由于要保存完整的 cpu 信息,所以该结构的字段很多,我们主要介绍以下几个字段:
-
x86_max_cores :package 支持的最大 core 数量;
-
x86_coreid_bits:处理器的 core_id 所占用的位数;
-
cpuid_level:cpuid 指令支持的基本信息查询的最大向量(0x80000000 以下);
-
extended_cpuid_level:cpuid 指令支持的扩展功能查询的最大向量(0x80000000 以上);
-
initial_apicid:xAPIC 模式下为 8 位 APIC ID,x2APIC 模式下为 32 位 x2APIC ID;
-
apicid:同 initial_apicid 字段;
-
phys_proc_id:Package ID;
-
cpu_core_id: Core ID;
-
cpu_index:操作系统定义的处理器编号,非硬件定义的;
-
x86_capability:处理器能力位图;
-
x86_virt_bits:处理器支持的最大虚拟地址位数;
-
x86_phys_bits:处理器支持的最大物理地址位数;
-
x86_cache_alignment:缓存行对齐字节;
-
x86_clflush_size:缓存行字节大小;
3.1.2 cpu_info
cpu_info 是 struct cpuinfo_x86 类型的 per-cpu 变量,其定义如下:
// file: arch/x86/include/asm/processor.h
DECLARE_PER_CPU_SHARED_ALIGNED(struct cpuinfo_x86, cpu_info);
既然是 per-cpu 变量,那么每个处理器都有一个单独的副本。
3.1.2.1 cpu_data
可以通过宏 cpu_data 来获取指定 cpu 下的 cpu_info:
// file: arch/x86/include/asm/processor.h
#define cpu_data(cpu) per_cpu(cpu_info, cpu)
3.1.3 boot_cpu_data
boot_cpu_data 是 struct cpuinfo_x86 的一个实例,用于存储启动处理器 BSP 的信息。
// file: arch/x86/kernel/setup.c
struct cpuinfo_x86 boot_cpu_data __read_mostly = {
.x86_phys_bits = MAX_PHYSMEM_BITS,
};
x86_phys_bits 字段指示处理器支持的最大物理地址位数,该字段初始化为 MAX_PHYSMEM_BITS(扩展为 46):
// file: arch/x86/include/asm/sparsemem.h
# define MAX_PHYSMEM_BITS 46
3.1.4 struct cpu_dev
由于 x86 处理器厂商众多,每家厂商的参数不完全相同。为了将不同厂商的设备数据格式进行统一,内核抽象出了 cpu_dev 结构体。
// file: arch/x86/kernel/cpu/cpu.h
struct cpu_dev {
const char *c_vendor;
/* some have two possibilities for cpuid string */
const char *c_ident[2];
struct cpu_model_info c_models[4];
void (*c_early_init)(struct cpuinfo_x86 *);
void (*c_bsp_init)(struct cpuinfo_x86 *);
void (*c_init)(struct cpuinfo_x86 *);
void (*c_identify)(struct cpuinfo_x86 *);
void (*c_detect_tlb)(struct cpuinfo_x86 *);
unsigned int (*c_size_cache)(struct cpuinfo_x86 *, unsigned int);
int c_x86_vendor;
};
其中,c_vendor 表示处理器的生产商,比如 "Intel"、"AMD" 等;c_ident 表示处理器标识,因为有的处理器有 2 个标识,所以数组成员为 2 个;c_x86_vendor 表示生产商编号。另外还有处理器特定的函数。
3.1.5 x86 处理器厂商编号
内核中定义的 x86 处理器厂商编号如下:
// file: arch/x86/include/asm/processor.h
#define X86_VENDOR_INTEL 0
#define X86_VENDOR_CYRIX 1
#define X86_VENDOR_AMD 2
#define X86_VENDOR_UMC 3
#define X86_VENDOR_CENTAUR 5
#define X86_VENDOR_TRANSMETA 7
#define X86_VENDOR_NSC 8
#define X86_VENDOR_NUM 9
#define X86_VENDOR_UNKNOWN 0xff
这些厂商中,最广为人知的就是 Intel 和 AMD。
3.1.6 __x86_cpu_dev_start && __x86_cpu_dev_end
由于每个厂商都有特定的处理器函数,所以也都有单独的 cpu_dev 实例。
3.1.6.1 intel_cpu_dev
Intel 相关的实例如下:
// file: arch/x86/kernel/cpu/intel.c
static const struct cpu_dev __cpuinitconst intel_cpu_dev = {
.c_vendor = "Intel",
.c_ident = { "GenuineIntel" },
#ifdef CONFIG_X86_32
......
#endif
.c_detect_tlb = intel_detect_tlb,
.c_early_init = early_init_intel,
.c_init = init_intel,
.c_x86_vendor = X86_VENDOR_INTEL,
};
3.1.6.2 amd_cpu_dev
AMD 相关的实例如下:
// file: arch/x86/kernel/cpu/amd.c
static const struct cpu_dev __cpuinitconst amd_cpu_dev = {
.c_vendor = "AMD",
.c_ident = { "AuthenticAMD" },
#ifdef CONFIG_X86_32
......
#endif
.c_early_init = early_init_amd,
.c_detect_tlb = cpu_detect_tlb_amd,
.c_bsp_init = bsp_init_amd,
.c_init = init_amd,
.c_x86_vendor = X86_VENDOR_AMD,
};
3.1.6.3 cpu_dev_register
这些实例地址通过 cpu_dev_register 宏注册到内核镜像文件中。
// file: arch/x86/kernel/cpu/amd.c
cpu_dev_register(amd_cpu_dev);
// file: arch/x86/kernel/cpu/intel.c
cpu_dev_register(intel_cpu_dev);
cpu_dev_register 宏定义如下:
// file: arch/x86/kernel/cpu/cpu.h
#define cpu_dev_register(cpu_devX) \
static const struct cpu_dev *const __cpu_dev_##cpu_devX __used \
__attribute__((__section__(".x86_cpu_dev.init"))) = \
&cpu_devX;
该宏定义了 struct cpu_dev 指针变量。由于使用了__attribute__((__section__(".x86_cpu_dev.init")))的组合来修饰该变量,这些变量会被编译到 .x86_cpu_dev.init 节中。
在链接时, .x86_cpu_dev.init 节中的数据被放置到符号__x86_cpu_dev_start 与 __x86_cpu_dev_end 之间。
// file: arch/x86/kernel/vmlinux.lds.S
.x86_cpu_dev.init : AT(ADDR(.x86_cpu_dev.init) - LOAD_OFFSET) {
__x86_cpu_dev_start = .;
*(.x86_cpu_dev.init)
__x86_cpu_dev_end = .;
}
由于这 2 个符号之间放置的都是 struct cpu_dev 指针,所以被声明为 struct cpu_dev * 数组:
// file: arch/x86/kernel/cpu/cpu.h
extern const struct cpu_dev *const __x86_cpu_dev_start[],
*const __x86_cpu_dev_end[];
3.2 MADT 相关数据结构
3.2.1 acpi_table_header
acpi_table_header 用于描述 ACPI 通用表头,每个字段的说明请参考 “2.6.1 ACPI 通用表头” 小节。
struct acpi_table_header {
char signature[ACPI_NAME_SIZE]; /* ASCII table signature */
u32 length; /* Length of table in bytes, including this header */
u8 revision; /* ACPI Specification minor version number */
u8 checksum; /* To make sum of entire table == 0 */
char oem_id[ACPI_OEM_ID_SIZE]; /* ASCII OEM identification */
char oem_table_id[ACPI_OEM_TABLE_ID_SIZE]; /* ASCII OEM table identification */
u32 oem_revision; /* OEM revision number */
char asl_compiler_id[ACPI_NAME_SIZE]; /* ASCII ASL compiler vendor ID */
u32 asl_compiler_revision; /* ASL compiler version */
};
我们主要关注 2 个字段:
- signature:表的签名,用来区分不同的表;
- length:表的总大小(包括表头);
3.2.2 MADT 表头 -- acpi_table_madt
acpi_table_madt 结构用于描述 MADT 表头。MADT 表头中,除了包含 ACPI 通用表头外,还有 2 个专用字段。专用字段的描述详见 “2.6.2 MADT 表头专用字段” 小节。
// file: include/acpi/actbl1.h
struct acpi_table_madt {
struct acpi_table_header header; /* Common ACPI table header */
u32 address; /* Physical address of local APIC */
u32 flags;
};
address 字段中,保存着 32 位的 Local APIC 物理基地址。
3.2.3 子表头 -- acpi_subtable_header
acpi_subtable_header 表示 MADT 表项的表头。
我们在上文介绍过,对于 MADT 表的每一个表项,都包含 2 个字段的通用子表头,每个字段的大小为 1 个字节。第 1 个字段表示表项类型;第 2 个字段表示表项大小。
// file: include/acpi/actbl1.h
struct acpi_subtable_header {
u8 type;
u8 length;
};
3.2.4 acpi_madt_local_apic_override
acpi_madt_local_apic_override 描述了 “Local APIC Address Override” 表项的结构。结构详情请参考 “2.6.5 Type 5: Local APIC Address Override” 节。
// file: include/acpi/actbl1.h
struct acpi_madt_local_apic_override {
struct acpi_subtable_header header;
u16 reserved; /* Reserved, must be zero */
u64 address; /* APIC physical address */
};
该表项的 address 字段,保存着 64 位的 Local APIC 物理基地址。
3.2.5 acpi_madt_local_x2apic
acpi_madt_local_x2apic 描述了"Processor Local x2APIC" 表项的结构。结构详情请参考 “2.6.6 Type 9: Processor Local x2APIC” 节。
// file: include/acpi/actbl1.h
struct acpi_madt_local_x2apic {
struct acpi_subtable_header header;
u16 reserved; /* reserved - must be zero */
u32 local_apic_id; /* Processor x2APIC ID */
u32 lapic_flags;
u32 uid; /* ACPI processor UID */
};
我们主要关注 2 个字段:
- local_apic_id:32 位的 x2APIC ID;
- lapic_flags:处理器是否可用的标志。
3.2.6 acpi_madt_local_apic
acpi_madt_local_apic 描述了"Processor Local APIC" 表项的结构。结构详情请参考 “2.6.4 Type 0: Processor Local APIC” 节。
struct acpi_madt_local_apic {
struct acpi_subtable_header header;
u8 processor_id; /* ACPI processor id */
u8 id; /* Processor's local APIC id */
u32 lapic_flags;
};
我们主要关注 2 个字段:
- id:8 位的 APIC ID;
- lapic_flags:处理器是否可用的标志。
3.2.7 ACPI 表签名
不同的 ACPI 表,有着不同的签名。内核为不同表的签名定义了宏:
// file: include/acpi/actbl1.h
#define ACPI_SIG_BERT "BERT" /* Boot Error Record Table */
#define ACPI_SIG_CPEP "CPEP" /* Corrected Platform Error Polling table */
#define ACPI_SIG_ECDT "ECDT" /* Embedded Controller Boot Resources Table */
#define ACPI_SIG_EINJ "EINJ" /* Error Injection table */
#define ACPI_SIG_ERST "ERST" /* Error Record Serialization Table */
#define ACPI_SIG_HEST "HEST" /* Hardware Error Source Table */
#define ACPI_SIG_MADT "APIC" /* Multiple APIC Description Table */
#define ACPI_SIG_MSCT "MSCT" /* Maximum System Characteristics Table */
#define ACPI_SIG_SBST "SBST" /* Smart Battery Specification Table */
#define ACPI_SIG_SLIT "SLIT" /* System Locality Distance Information Table */
#define ACPI_SIG_SRAT "SRAT" /* System Resource Affinity Table */
在本文中,我们使用到 MADT 表的签名,即宏 ACPI_SIG_MADT。
3.2.8 MADT 表项类型
内核使用枚举类型 acpi_madt_type 为 MADT 中的每一种表项定义了不同的枚举值。
// file: include/acpi/actbl1.h
enum acpi_madt_type {
ACPI_MADT_TYPE_LOCAL_APIC = 0,
ACPI_MADT_TYPE_IO_APIC = 1,
ACPI_MADT_TYPE_INTERRUPT_OVERRIDE = 2,
ACPI_MADT_TYPE_NMI_SOURCE = 3,
ACPI_MADT_TYPE_LOCAL_APIC_NMI = 4,
ACPI_MADT_TYPE_LOCAL_APIC_OVERRIDE = 5,
ACPI_MADT_TYPE_IO_SAPIC = 6,
ACPI_MADT_TYPE_LOCAL_SAPIC = 7,
ACPI_MADT_TYPE_INTERRUPT_SOURCE = 8,
ACPI_MADT_TYPE_LOCAL_X2APIC = 9,
ACPI_MADT_TYPE_LOCAL_X2APIC_NMI = 10,
ACPI_MADT_TYPE_GENERIC_INTERRUPT = 11,
ACPI_MADT_TYPE_GENERIC_DISTRIBUTOR = 12,
ACPI_MADT_TYPE_RESERVED = 13 /* 13 and greater are reserved */
};
我们在 ”2.6 MADT 格式“ 中介绍的 Type 0、Type 5 和 Type 9 的表项,对应着枚举值:ACPI_MADT_TYPE_LOCAL_APIC、ACPI_MADT_TYPE_LOCAL_APIC_OVERRIDE、ACPI_MADT_TYPE_LOCAL_X2APIC。
3.3 其它变量
在 CPU 拓扑探测过程中,还需要用到以下变量:
- num_processors:可用的处理器数量;
- disabled_cpus:被禁用的处理器数量;
- boot_cpu_physical_apicid:BSP 的 APIC ID,初始化为 -1;
- max_physical_apicid:用于保存探测到的最大 APIC ID。随着探测的进行,该值是动态变化的;
- phys_cpu_present_map:用来指示 cpu 是否存在的位图,每个比特位对应着一个 cpu;
- x86_cpu_to_apicid:per-cpu 变量,表示 cpu 和 APIC ID 的对应关系,初始化为
BAD_APICID; - x86_bios_cpu_apicid:per-cpu 变量,表示 cpu 和 APIC ID 的对应关系,初始化为
BAD_APICID。
// file: arch/x86/kernel/apic/apic.c
unsigned int num_processors;
unsigned disabled_cpus __cpuinitdata;
unsigned int boot_cpu_physical_apicid = -1U;
unsigned int max_physical_apicid;
physid_mask_t phys_cpu_present_map;
DEFINE_EARLY_PER_CPU_READ_MOSTLY(u16, x86_cpu_to_apicid, BAD_APICID);
DEFINE_EARLY_PER_CPU_READ_MOSTLY(u16, x86_bios_cpu_apicid, BAD_APICID);
3.3.1 BAD_APICID
#ifdef CONFIG_X86_32
#define BAD_APICID 0xFFu
#else
#define BAD_APICID 0xFFFFu
#endif
对于 x86-64 系统,该宏扩展为 0xFFFF。
四、APIs
4.1 基础接口
4.1.1 have_cpuid_p
have_cpuid_p 函数,检查处理器是否支持 cpuid 指令。
// file: arch/x86/include/asm/processor.h
#ifdef CONFIG_X86_32
extern int have_cpuid_p(void);
#else
static inline int have_cpuid_p(void)
{
return 1;
}
从函数定义中可以看到,如果不是 x86-32 架构,该函数直接返回 1,表示支持 cpuid 指令。也就是说,在x86-64 架构下的处理器,肯定是支持 cpuid 指令的。
4.1.2 cpuid
该函数是对汇编指令 cpuid 的封装,其接收 5 个参数:
- @op:指定 cpuid 的查询向量
- @eax:用于保存 EAX 寄存器中的输出结果
- @ebx:用于保存 EBX 寄存器中的输出结果
- @ecx:用于保存 ECX 寄存器中的输出结果
- @edx:用于保存 EDX 寄存器中的输出结果
// file: arch/x86/include/asm/processor.h
static inline void cpuid(unsigned int op,
unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
*eax = op;
*ecx = 0;
__cpuid(eax, ebx, ecx, edx);
}
根据 cpuid 指令格式,主查询向量要在 EAX 寄存器指定,子查询向量在 ECX 寄存器指定。该函数本来不需指定 ECX 寄存器的值,但在某些处理器上如果不清除 ECX 的值,会返回陈旧的内容。为了防止此类问题的发生,将 ECX 寄存器的值设置为 0。
最后,调用 __cpuid 函数完成查询功能。
4.1.3 __cpuid
__cpuid 宏 扩展为 native_cpuid 函数。
// file: arch/x86/include/asm/processor.h
#define __cpuid native_cpuid
native_cpuid 函数定义如下:
// file: arch/x86/include/asm/processor.h
static inline void native_cpuid(unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
/* ecx is often an input as well as an output. */
asm volatile("cpuid"
: "=a" (*eax),
"=b" (*ebx),
"=c" (*ecx),
"=d" (*edx)
: "0" (*eax), "2" (*ecx)
: "memory");
}
该函数使用内联汇编,直接执行 cpuid 汇编指令,其中 eax 和 ecx 作为输入参数,输出结果保存到 eax、ebx、ecx 以及 edx 中。
4.1.4 cpuid_count
// file: arch/x86/include/asm/processor.h
static inline void cpuid_count(unsigned int op, int count,
unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
*eax = op;
*ecx = count;
__cpuid(eax, ebx, ecx, edx);
}
有些 cpuid 查询,除了需要在 EAX 寄存器指定主查询向量外,还需要在 ECX 寄存器中指定子查询向量。内核专门为此场景定义了 cpuid_count 函数。相比 cpuid 函数,该函数多出了 count 参数,即子查询向量,该参数需要放置到 ECX 寄存器中。
同 cpuid 函数一样,该函数也将查询委托给 __cpuid 函数执行。
4.1.5 cpuid_eax
cpuid_eax 函数是对 cpuid 函数的封装,只返回 EAX 寄存器的值。
// file: arch/x86/include/asm/processor.h
static inline unsigned int cpuid_eax(unsigned int op)
{
unsigned int eax, ebx, ecx, edx;
cpuid(op, &eax, &ebx, &ecx, &edx);
return eax;
}
4.1.6 cpuid_ebx
cpuid_ebx 函数是对 cpuid 函数的封装,只返回 EBX 寄存器的值。
// file: arch/x86/include/asm/processor.h
static inline unsigned int cpuid_ebx(unsigned int op)
{
unsigned int eax, ebx, ecx, edx;
cpuid(op, &eax, &ebx, &ecx, &edx);
return ebx;
}
4.1.7 set_cpu_cap && clear_cpu_cap && test_cpu_cap
// file: arch/x86/include/asm/cpufeature.h
#define set_cpu_cap(c, bit) set_bit(bit, (unsigned long *)((c)->x86_capability))
#define clear_cpu_cap(c, bit) clear_bit(bit, (unsigned long *)((c)->x86_capability))
#define test_cpu_cap(c, bit) \
test_bit(bit, (unsigned long *)((c)->x86_capability))
cpuinfo_x86 结构体中的 x86_capability 字段本质上是一个位图,每个比特位对应着处理器的一个能力。set_cpu_cap 和 clear_cpu_cap 宏,分别调用位图接口 set_bit 和 clear_bit 将 x86_capability 中能力对应的比特位置位或者清除。
test_cpu_cap 宏测试位图中指定能力的比特位是否置位,如果置位,返回 1,否则,返回 0。
4.1.8 cpu_has
cpu_has 宏检查处理器是否具有某种能力,该宏接收 2 个参数:
- @c:处理器对应的
cpuinfo_x86结构体实例; - @bit:能力对应的比特位
// file: arch/x86/include/asm/cpufeature.h
#define cpu_has(c, bit) \
(__builtin_constant_p(bit) && REQUIRED_MASK_BIT_SET(bit) ? 1 : \
test_cpu_cap(c, bit))
__builtin_constant_p 是 gcc 内建函数,用来检查参数是否是编译时常量;REQUIRED_MASK_BIT_SET 宏用来检查指定的能力是否处理器的必备能力,比如在 x86-64 架构下的处理器都具体 MSR、XMM 能力等等。
如果两者都满足,说明处理器具有该能力,返回 1;否则,使用 test_cpu_cap 宏检查 cpuinfo_x86 实例的 x86_capability 字段中,该能力是否置位。
4.1.9 boot_cpu_has
// file: arch/x86/include/asm/cpufeature.h
#define boot_cpu_has(bit) cpu_has(&boot_cpu_data, bit)
boot_cpu_has 宏用来检查 BSP 是否具有某种能力,其内部调用了 cpu_has 宏完成检查功能。
其中,入参 bit 表示能力对应的比特位;boot_cpu_data 是 BSP 对应的 cpuinfo_x86 实例。
4.1.10 cpu_has_apic
// file: arch/x86/include/asm/cpufeature.h
#define cpu_has_apic boot_cpu_has(X86_FEATURE_APIC)
宏 cpu_has_apic 用来检测 BSP 是否支持 APIC。
4.1.11 apic_read
apic_read 函数会读取指定的 APIC 寄存器的内容,入参 reg 是寄存器是相对于物理基地址的偏移量。
// file: arch/x86/include/asm/apic.h
static inline u32 apic_read(u32 reg)
{
return apic->read(reg);
}
不同的 APIC 模式,其实现不同。对于 xAPIC 模式,由于寄存器会映射到虚拟内存,所以直接读取内存地址即可;对于 x2APIC 模式,则需要使用 rdmsr 指令。
4.1.11.1 native_apic_mem_read
在 xAPIC 模式下,实际会调用 native_apic_mem_read 函数:
static struct apic apic_physflat = {
......
.read = native_apic_mem_read,
......
}
native_apic_mem_read 函数直接读取寄存器的映射地址中的值。
// file: arch/x86/include/asm/apic.h
static inline u32 native_apic_mem_read(u32 reg)
{
return *((volatile u32 *)(APIC_BASE + reg));
}
xAPIC 模式下,需要将寄存器映射到虚拟地址空间。具体来说,就是将从物理基地址开始的 4KB 物理内存映射到固定映射区 FIX_APIC_BASE 对应的虚拟地址空间。
APIC_BASE 是映射后的虚拟基地址:
#define APIC_BASE (fix_to_virt(FIX_APIC_BASE))
其中,FIX_APIC_BASE 是固定映射区的索引,fix_to_virt 函数用于将索引转换成虚拟地址。
固定映射相关内容请参考: Linux Kernel:内存管理之固定映射 (Fixmap)。
由于参数 reg 是寄存器相对于物理基地址的偏移量,所以APIC_BASE + reg 就得到映射后的虚拟地址。
获取到寄存器对应的虚拟地址后,返回虚拟地址中的内存值。
4.1.11.2 native_apic_msr_read
对于 x2APIC 模式,实际会调用 native_apic_msr_read 函数:
// file: arch/x86/kernel/apic/x2apic_phys.c
static struct apic apic_x2apic_phys = {
......
.read = native_apic_msr_read,
......
}
native_apic_msr_read 函数会使用 rdmsr 指令读取指定 MSR 中的值。
static inline u32 native_apic_msr_read(u32 reg)
{
u64 msr;
if (reg == APIC_DFR)
return -1;
rdmsrl(APIC_BASE_MSR + (reg >> 4), msr);
return (u32)msr;
}
在 x2APIC 模式下,废弃了 Destination Format Register (DFR) 寄存器。所以,如果要去读取的是 DFR 寄存器,直接返回 -1。
由于入参是寄存器相对于物理基地址的偏移,所以需要将偏移量转换成 MSR 寄存器地址。
《Intel SDM: Volume 3A,Table 11-6. Local APIC Register Address Map Supported by x2APIC》描述了这种映射关系,前 2 项示意如下:
其算法就是将偏移地址右移 4 位后,再加上 MSR 的基地址 0x800。内核将 MSR 基地址定义成宏 APIC_BASE_MSR:
// file: arch/x86/include/asm/apicdef.h
#define APIC_BASE_MSR 0x800
然后,使用 rdmsr 指令读取对应 MSR 寄存器的值并返回。
4.1.12 read_apic_id
read_apic_id 函数是 apic_read 函数的简化用法,用来获取 ”Local APIC ID register“ 中的 APIC ID 的值。
// file: arch/x86/include/asm/apic.h
static inline unsigned int read_apic_id(void)
{
unsigned int reg;
reg = apic_read(APIC_ID);
return apic->get_apic_id(reg);
}
其中,宏 APIC_ID 表示 ”Local APIC ID register“ 相对于物理基地址的偏移,该宏扩展为 0x20(见上图):
// file: arch/x86/include/asm/apicdef.h
#define APIC_ID 0x20
最后,调用不同 APIC 驱动实例的 get_apic_id 函数来获取该值。
不同模式下,”Local APIC ID register“ 的格式以及 APIC ID 的位数也是不同的,详见 ”2.3 xAPIC 模式 vs x2APIC 模式“。
4.1.13 enable_x2apic
enable_x2apic 函数用于开启 x2APIC,该函数定义如下:
// file: arch/x86/kernel/apic/apic.c
void enable_x2apic(void)
{
u64 msr;
rdmsrl(MSR_IA32_APICBASE, msr);
if (x2apic_disabled) {
__disable_x2apic(msr);
return;
}
if (!x2apic_mode)
return;
if (!(msr & X2APIC_ENABLE)) {
printk_once(KERN_INFO "Enabling x2apic\n");
wrmsrl(MSR_IA32_APICBASE, msr | X2APIC_ENABLE);
}
}
enable_x2apic 函数中,涉及到 IA32_APIC_BASE MSR,该寄存器的地址是 0x1b,其格式如下图所示:
其中位 8 指示处理器是否 BSP;位 10 指示 x2APIC 模式是否开启;位 11 指示 APIC 是否启用;位 35-12 是右移 12 位后的 Local APIC 物理基地址。
内核为 IA32_APIC_BASE MSR 的地址及各标志位的掩码定义了对应的宏:
// file: arch/x86/include/uapi/asm/msr-index.h
#define MSR_IA32_APICBASE 0x0000001b
#define MSR_IA32_APICBASE_BSP (1<<8)
#define MSR_IA32_APICBASE_ENABLE (1<<11)
#define MSR_IA32_APICBASE_BASE (0xfffff<<12)
// file: arch/x86/include/asm/apicdef.h
#define X2APIC_ENABLE (1UL << 10)
首先,调用 rdmsrl 函数从指定的 MSR 中读取数据。rdmsrl 函数是对汇编指令 rdmsr 的封装。在本例中,读取 IA32_APIC_BASE 寄存器的值保存到变量 msr 中。
4.1.14 x2apic_enabled
x2apic_enabled 函数检查处理器是否支持 x2APIC。如果支持,返回 1;否则,返回 0。
// file: arch/x86/include/asm/apic.h
static inline int x2apic_enabled(void)
{
u64 msr;
if (!cpu_has_x2apic)
return 0;
rdmsrl(MSR_IA32_APICBASE, msr);
if (msr & X2APIC_ENABLE)
return 1;
return 0;
}
MSR_IA32_APICBASE 寄存器格式见 ”4.1.13 enable_x2apic“ 小节。
4.2 拓扑探测接口
4.2.1 基本信息探测 -- cpu_detect
cpu_detect 函数用于获取处理器基本信息,获取到的信息会保存到对应的 cpuinfo_x86 结构体实例中。
// file: arch/x86/kernel/cpu/common.c
void __cpuinit cpu_detect(struct cpuinfo_x86 *c)
{
/* Get vendor name */
cpuid(0x00000000, (unsigned int *)&c->cpuid_level,
(unsigned int *)&c->x86_vendor_id[0],
(unsigned int *)&c->x86_vendor_id[8],
(unsigned int *)&c->x86_vendor_id[4]);
c->x86 = 4;
/* Intel-defined flags: level 0x00000001 */
if (c->cpuid_level >= 0x00000001) {
u32 junk, tfms, cap0, misc;
cpuid(0x00000001, &tfms, &misc, &junk, &cap0);
c->x86 = (tfms >> 8) & 0xf;
c->x86_model = (tfms >> 4) & 0xf;
c->x86_mask = tfms & 0xf;
if (c->x86 == 0xf)
c->x86 += (tfms >> 20) & 0xff;
if (c->x86 >= 0x6)
c->x86_model += ((tfms >> 16) & 0xf) << 4;
if (cap0 & (1<<19)) {
c->x86_clflush_size = ((misc >> 8) & 0xff) * 8;
c->x86_cache_alignment = c->x86_clflush_size;
}
}
}
在函数一开始,使用 ”CPUID EAX=0x0“ 来查询处理器基本信息。该类型查询的返回结果中(返回数据格式详见 "2.4.1 CPUID EAX=0x00000000"),EAX 寄存中保存的是 cpuid 指令支持的基本信息查询的最大向量值。EBX、EDX、ECX 联合起来,保存着 Intel 的处理器标识 “GenuineIntel”,与 intel_cpu_dev 中的 c_ident 字段一致。这些数据, 分别保存到 cpuinfo_x86 结构体的 cpuid_level 和 x86_vendor_id 字段中。
接下来,如果处理器支持 ”CPUID EAX=0x1“ 类型查询,那么执行该查询,获取对应的处理器信息。
返回的结果中 EAX、EBX、ECX、EDX 寄存器的值分别存入 tfms、 misc、junk、 cap0 中。
tfms 中保存着处理器的版本信息,包括 Type、 Family、Model 以及 Stepping ID。可以看到,变量名就是这些信息的首字母组合。这些数据被解析后,分别存入 cpuinfo_x86 结构体的 x86、x86_model 和 x86_mask 字段中。这些字段与本文关系不大,所以就不深入介绍了。
misc 的位 15-8( EBX[15:8] )中,保存着 CLFLUSH 指令刷新的缓存行大小(以 8 字节为单位),该值乘以 8 就得到以字节为单位的缓存行大小。
junk 和 cap0 中,保存着处理器支持的能力,每个比特位对应一个能力。其中 cap0 的位 19 ( EDX[19],CLFSH 位),指示是否支持 CLFLUSH 指令。如果支持,将 cpuinfo_x86 结构体的 x86_clflush_size 以及 x86_cache_alignment 字段设置为以字节为单位的缓存行大小。
4.2.2 get_count_order
我们在 ”2.4.3 CPUID EAX=0x00000004“ 小节中介绍过,当使用 ”CPUID.0x4“ 查询时,需要一个函数将处理器数量转换成占用的比特位位数。内核定义了 get_count_order 函数来实现该功能。
get_count_order 函数定义如下:
// file: include/linux/bitops.h
static __inline__ int get_count_order(unsigned int count)
{
int order;
order = fls(count) - 1;
if (count & (count - 1))
order++;
return order;
}
该函数会计算参数 count 向上圆整到 2 的整次幂后的阶。比如,整数 5 向上圆整后的数是 8,此时的阶为 3。
函数内部调用了位操作接口 fls,计算最高权重的为 1 的比特位。由于 fls 返回的值会在实际位数上加 1,所以减 1 后就得到实际比特位。
当 count 不是 2 的整次幂时,需要向上圆整,所以要将 order 加 1。
最后,将 order 返回。
4.2.2.1 fls (find last set bit)
fls 函数用于获取指定整数中权重最高的为 1 的比特位。当输入值为 0 时,返回 0;否则,返回实际权重最高的为 1 的比特位。
该函数只支持 32 位的整数,所以返回值范围为 0 - 32。
注意,该函数的返回值比实际探测到的索引大 1。比如当位 0 是权重最高位时,该函数返回 1。
// file: arch/x86/include/asm/bitops.h
static inline int fls(int x)
{
int r;
#ifdef CONFIG_X86_64
/*
* AMD64 says BSRL won't clobber the dest reg if x==0; Intel64 says the
* dest reg is undefined if x==0, but their CPU architect says its
* value is written to set it to the same as before, except that the
* top 32 bits will be cleared.
*
* We cannot do this on 32 bits because at the very least some
* 486 CPUs did not behave this way.
*/
asm("bsrl %1,%0"
: "=r" (r)
: "rm" (x), "0" (-1));
#elif defined(CONFIG_X86_CMOV)
......
#else
......
#endif
return r + 1;
}
函数使用内联汇编,通过执行 bsrl 指令来获取最高位。由于指令后缀为 l,指示操作数为 32 位,所以只能计算 32 位的整数。
bsr (Bit Scan Reverse)指令会从后往前扫描操作数,获取权重最高的为 1 的比特位索引。
函数最后,将获取到的比特位索引加 1 后返回。
4.2.3 探测扩展拓扑 -- detect_extended_topology
detect_extended_topology 函数用来获取处理器的扩展拓扑信息,并将其保存到 cpuinfo_x86 结构体实例中。
函数内部使用了 ”CPUID EAX=0xB“ 查询,该查询返回的数据格式详见 2.4.3 CPUID EAX=0x0000000B 小节。
该函数是用来获取处理器拓扑的主要函数。
// file: arch/x86/kernel/cpu/topology.c
void __cpuinit detect_extended_topology(struct cpuinfo_x86 *c)
{
#ifdef CONFIG_SMP
unsigned int eax, ebx, ecx, edx, sub_index;
unsigned int ht_mask_width, core_plus_mask_width;
unsigned int core_select_mask, core_level_siblings;
static bool printed;
if (c->cpuid_level < 0xb)
return;
cpuid_count(0xb, SMT_LEVEL, &eax, &ebx, &ecx, &edx);
/*
* check if the cpuid leaf 0xb is actually implemented.
*/
if (ebx == 0 || (LEAFB_SUBTYPE(ecx) != SMT_TYPE))
return;
......
#endif
}
cpuinfo_x86 的 cpuid_level 字段指示处理器支持的基本信息查询最大的向量值,该值是在 cpu_detect 函数中获取到的。如果处理器不支持 0xB 查询,直接返回。
......
cpuid_count(0xb, SMT_LEVEL, &eax, &ebx, &ecx, &edx);
/*
* check if the cpuid leaf 0xb is actually implemented.
*/
if (ebx == 0 || (LEAFB_SUBTYPE(ecx) != SMT_TYPE))
return;
......
接下来,使用 ”CPUID.0xB“ 查询获取处理器扩展拓扑信息。执行 0xB 查询时,需要在 ECX 寄存器指定子查询向量,所以使用了 cpuid_count 函数。此处将子查询向量指定为 SMT_LEVEL(扩展为 0),即查询逻辑处理器层级的信息。
// file: arch/x86/kernel/cpu/topology.c
#define SMT_LEVEL 0
我们在 2.4.3 CPUID EAX=0x0000000B 小节中介绍过,判定 0xB 查询可用,需要 2 个条件:
- ”CPUID.0x0“ 的返回结果中 EAX 寄存器值必须大于等于 0xB;
- ”CPUID.0xB.0x0“ 的返回结果中 EBX 寄存器的值必须非 0。
此处还要检查返回的层级类型是否为 SMT_TYPE,即逻辑处理器层级。ECX[15:8] 中保存着返回的层级类型,LEAFB_SUBTYPE 宏用来获取该值:
// file: arch/x86/kernel/cpu/topology.c
#define LEAFB_SUBTYPE(ecx) (((ecx) >> 8) & 0xff)
内核定义了 3 个宏,对应着返回结果中的不同层级类型( level type):
// file: arch/x86/kernel/cpu/topology.c
/* leaf 0xb sub-leaf types */
#define INVALID_TYPE 0
#define SMT_TYPE 1
#define CORE_TYPE 2
其中,SMT_TYPE 指示查询的是逻辑处理器。
如果检查失败,说明实际不支持 0xB 查询,直接返回。
......
set_cpu_cap(c, X86_FEATURE_XTOPOLOGY);
......
检查通过,说明处理器支持扩展拓扑能力,将该能力对应的比特位置位。
......
/*
* initial apic id, which also represents 32-bit extended x2apic id.
*/
c->initial_apicid = edx;
......
EDX 寄存器中保存着 32 位的 x2APIC ID,将其保存到 cpuinfo_x86 的 initial_apicid 字段中。
......
/*
* Populate HT related information from sub-leaf level 0.
*/
core_level_siblings = smp_num_siblings = LEVEL_MAX_SIBLINGS(ebx);
core_plus_mask_width = ht_mask_width = BITS_SHIFT_NEXT_LEVEL(eax);
......
接下来,解析逻辑处理器的数量及位宽。BITS_SHIFT_NEXT_LEVEL 以及 LEVEL_MAX_SIBLINGS 宏扩展如下:
// file: arch/x86/kernel/cpu/topology.c
#define BITS_SHIFT_NEXT_LEVEL(eax) ((eax) & 0x1f)
#define LEVEL_MAX_SIBLINGS(ebx) ((ebx) & 0xffff)
宏 BITS_SHIFT_NEXT_LEVEL 用来获取 EAX[4:0] 的值,该值指示在 x2APIC ID 中获取下一层级的拓扑 ID 所要右移的位数。
当子查询为 0 时,该值指示逻辑处理器 ID 的位宽。用该值初始化变量 core_plus_mask_width 以及 ht_mask_width。
LEVEL_MAX_SIBLINGS 宏用来获取 EBX[15:0] 中的值,该值指示当前层级的逻辑处理器数量。
当子查询为 0 时,该值指示每个核中的逻辑处理器数量。 用该值初始化变量 core_level_siblings 和 smp_num_siblings。
注:
core_plus_mask_width表示 Core ID 加上 SMT ID 的总位数,此处先用 SMT ID 的位数占位,后续计算会更新该值。core_level_siblings表示 package 中的 core 的数量,此处先用逻辑处理器数量占位,后续计算会更新该值。
......
sub_index = 1;
do {
cpuid_count(0xb, sub_index, &eax, &ebx, &ecx, &edx);
/*
* Check for the Core type in the implemented sub leaves.
*/
if (LEAFB_SUBTYPE(ecx) == CORE_TYPE) {
core_level_siblings = LEVEL_MAX_SIBLINGS(ebx);
core_plus_mask_width = BITS_SHIFT_NEXT_LEVEL(eax);
break;
}
sub_index++;
} while (LEAFB_SUBTYPE(ecx) != INVALID_TYPE);
......
接下来是一个 do-while 循环,子查询从 1 开始递增,直到获取到 Core 层级的数据或者 ECX[15:8] 中返回无效的层级类型。该循环的主要目的就是获取 Core 层级的相关数据。当 LEAFB_SUBTYPE(ecx) == CORE_TYPE 时,说明当前层级为 Core 层,EBX[15:0] 中保存着当前层级的逻辑处理器数量,即所有 core 的逻辑处理器的总数量。通过宏 LEVEL_MAX_SIBLINGS 获取到该值并更新core_level_siblings 的值;EAX[4:0] 中保存着 core 的下一层偏移位数,即 package 层的位偏移,同时也是 core_id + smt_id 的总位宽。使用宏 BITS_SHIFT_NEXT_LEVEL 获取该值并更新变量 core_plus_mask_width 的值。
此时,core_plus_mask_width 和 ht_mask_width 如下图所示:
计算过程可参考 2.4.3 CPUID EAX=0x0000000B 小节中的示意图。
......
core_select_mask = (~(-1 << core_plus_mask_width)) >> ht_mask_width;
......
core_select_mask 表示 core_id 的掩码。
......
c->cpu_core_id = apic->phys_pkg_id(c->initial_apicid, ht_mask_width)
& core_select_mask;
c->phys_proc_id = apic->phys_pkg_id(c->initial_apicid, core_plus_mask_width);
......
apic->phys_pkg_id 是特定 APIC 实现的驱动函数,不论是哪种 APIC 实现,该函数的功能都是将 APIC ID 右移一定的位数。
在 x2APIC 非集群模式下,phys_pkg_id 实现为 x2apic_phys_pkg_id 函数:
// file: arch/x86/kernel/apic/x2apic_phys.c
static struct apic apic_x2apic_phys = {
......
.phys_pkg_id = x2apic_phys_pkg_id,
......
}
x2apic_phys_pkg_id 函数的功能就是将 APIC ID 右移一定的位数:
// file: arch/x86/include/asm/x2apic.h
static int x2apic_phys_pkg_id(int initial_apicid, int index_msb)
{
return initial_apicid >> index_msb;
}
所以,c->cpu_core_id 中保存的是 core_id;c->phys_proc_id 中保存的是 package_id。
......
c->apicid = apic->phys_pkg_id(c->initial_apicid, 0);
c->x86_max_cores = (core_level_siblings / smp_num_siblings);
......
接下来,用 c->initial_apicid 填充 c->apicid,这样两者都是 32 位的 x2APIC ID。
core_level_siblings 表示 package 中所有核的逻辑处理器总数量,smp_num_siblings 表示 package 中每个核的逻辑处理器数量,两者相除就得到核 ( core )的数量,并填充到 c->x86_max_cores 中。
if (!printed) {
printk(KERN_INFO "CPU: Physical Processor ID: %d\n",
c->phys_proc_id);
if (c->x86_max_cores > 1)
printk(KERN_INFO "CPU: Processor Core ID: %d\n",
c->cpu_core_id);
printed = 1;
}
return;
最后,打印出处理器相关信息并返回。
在我的虚拟机上,打印出如下信息:
# dmesg|grep "CPU:"
......
[ 0.100976] CPU: Physical Processor ID: 0
[ 0.100976] CPU: Processor Core ID: 0
4.2.4 intel_num_cpu_cores
如果处理器不支持 ”CPUID.0xB“ 扩展拓扑查询,那么为了获取处理器拓扑,就需使用 ”CPUID.0x4“ 查询。 该查询的返回结果中,EAX[31:26] 中保存着 package 支持的最大的 core 数量。注意,实际的 core 数量是 EAX[31:26] + 1。
intel_num_cpu_cores 函数用来在 ”CPUID.0x4“ 查询中,获取 package 支持的最大处理器核(core)数。
// file: arch/x86/kernel/cpu/intel.c
static int __cpuinit intel_num_cpu_cores(struct cpuinfo_x86 *c)
{
unsigned int eax, ebx, ecx, edx;
if (c->cpuid_level < 4)
return 1;
/* Intel has a non-standard dependency on %ecx for this CPUID level. */
cpuid_count(4, 0, &eax, &ebx, &ecx, &edx);
if (eax & 0x1f)
return (eax >> 26) + 1;
else
return 1;
}
cpuid_level 字段表示 cpuid 指令支持的基本查询的最大向量值,如果该值小于 4,说明是单核处理器,直接返回 1。
接下来调用 cpuid_count 函数探测处理器信息,其中主查询向量为 4,子查询向量为 0。
在返回数据中,EAX[4:0](共 5 位)指示缓存类型:
- 0 = Null - No more caches.
- 1 = Data Cache.
- 2 = Instruction Cache.
- 3 = Unified Cache.
- 4-31 = Reserved.
EAX[31:26] 指示 package 支持的最大处理器核数。根据 Intel 开发文档,该值要加上 1 才能得到实际结果。
执行 cpuid 指令后,先检测缓存类型。
如果缓存类型为 0,说明是单核处理器,直接返回 1;否则, 返回 EAX[31:26] + 1。
4.2.5 detect_ht
当处理器不支持 ”CPUID.0xB“ 扩展拓扑查询时,为了获取拓扑信息,就需要执行 ”CPUID.0x4“ 和 ”CPUID.0x1“ 查询。在 intel_num_cpu_cores 函数中,使用 ”CPUID.0x4“ 查询获取了 package 中最大的 core 数量。本函数使用 ”CPUID.0x1“ 查询,来获取拓扑的其它信息。
// file: arch/x86/kernel/cpu/common.c
void __cpuinit detect_ht(struct cpuinfo_x86 *c)
{
#ifdef CONFIG_X86_HT
u32 eax, ebx, ecx, edx;
int index_msb, core_bits;
static bool printed;
if (!cpu_has(c, X86_FEATURE_HT))
return;
if (cpu_has(c, X86_FEATURE_CMP_LEGACY))
goto out;
if (cpu_has(c, X86_FEATURE_XTOPOLOGY))
return;
......
#endif
}
函数一开始,判断是否需要使用 ”CPUID.0x1“ 探测:
- 当处理器不支持超线程时,说明是单核处理器,所以无需探测,直接返回。
- 当处理器支持扩展拓扑时,会优先使用扩展拓扑进行探测,根本就用不到 ”CPUID.0x1“ 类型探测,直接返回。
- 如果处理器具有 X86_FEATURE_CMP_LEGACY 能力,直接跳转到 out 标签处打印处理器信息。
// file: arch/x86/include/asm/cpufeature.h
#define X86_FEATURE_CMP_LEGACY (6*32+ 1) /* If yes HyperThreading not valid */
X86_FEATURE_CMP_LEGACY 是 AMD 处理器的能力,如果该能力有效,说明不支持超线程,
......
cpuid(1, &eax, &ebx, &ecx, &edx);
smp_num_siblings = (ebx & 0xff0000) >> 16;
if (smp_num_siblings == 1) {
printk_once(KERN_INFO "CPU0: Hyper-Threading is disabled\n");
goto out;
}
if (smp_num_siblings <= 1)
goto out;
......
接下来执行 ”CPUID.0x1“ 查询,查询结果保存在 eax、 ebx、 ecx 以及 edx 中。
EBX[23:16] 中保存着 package 支持的最大逻辑处理器数量,将其取出并保存在变量 smp_num_siblings 中。
如果 smp_num_siblings 等于 1,说明超线程被禁用,直接跳转到 out 标签处执行。
如果 smp_num_siblings 等于 0,说明处理器不支持超线程,直接跳转到 out 标签处执行。
......
index_msb = get_count_order(smp_num_siblings);
c->phys_proc_id = apic->phys_pkg_id(c->initial_apicid, index_msb);
smp_num_siblings = smp_num_siblings / c->x86_max_cores;
index_msb = get_count_order(smp_num_siblings);
core_bits = get_count_order(c->x86_max_cores);
c->cpu_core_id = apic->phys_pkg_id(c->initial_apicid, index_msb) &
((1 << core_bits) - 1);
......
Intel SDM 要求将 smp_num_siblings 向上圆整到 2 的整次幂,所以调用 get_count_order 函数计算向上圆整后的阶,即位宽,并赋值给 index_msb。详细计算过程见 ”4.2.2 get_count_order“ 小节。
接下来计算 package_id。APIC 驱动的 phys_pkg_id 函数的功能是将 APIC ID 右移一定的位数,我们在 detect_extended_topology 函数中已经遇到过。 此时,index_msb 表示 core_id + smt_id 的总位数,所以将 initial_apicid 右移 index_msb 位,就得到 package_id,并赋值给 phys_proc_id 字段。
此时,smp_num_siblings 表示 package 中的最大逻辑处理器数量,c->x86_max_cores 表示 package 中的最大 core 数量(在 intel_num_cpu_cores 函数中计算得到),两者相除就得到每个 core 中的逻辑处理器数量,使用此值更新 smp_num_siblings。
然后,分别计算 smt_id 和 core_id 的位宽,并保存在 index_msb 和 core_bits 中。
最终,计算出处理器的 core_id 并保存到 cpu_core_id 字段中。
......
out:
if (!printed && (c->x86_max_cores * smp_num_siblings) > 1) {
printk(KERN_INFO "CPU: Physical Processor ID: %d\n",
c->phys_proc_id);
printk(KERN_INFO "CPU: Processor Core ID: %d\n",
c->cpu_core_id);
printed = 1;
}
......
最后,打印出 cpu 相关信息。
4.2.6 identify_cpu
做了种种铺垫之后,我们终于来到了 identify_cpu 函数。该函数实现了拓扑探测的完整逻辑,即如果 ”CPUID.0xB“ 扩展拓扑探测可用,则使用该查询探测处理器拓扑信息;否则,需要使用 ”CPUID.0x4“ 和 ”CPUID.0x1“ 的组合查询。
identify_cpu 函数定义如下:
// file: arch/x86/kernel/cpu/common.c
static void __cpuinit identify_cpu(struct cpuinfo_x86 *c)
{
int i;
c->loops_per_jiffy = loops_per_jiffy;
c->x86_cache_size = -1;
c->x86_vendor = X86_VENDOR_UNKNOWN;
c->x86_model = c->x86_mask = 0; /* So far unknown... */
c->x86_vendor_id[0] = '\0'; /* Unset */
c->x86_model_id[0] = '\0'; /* Unset */
c->x86_max_cores = 1;
c->x86_coreid_bits = 0;
#ifdef CONFIG_X86_64
c->x86_clflush_size = 64;
c->x86_phys_bits = 36;
c->x86_virt_bits = 48;
#else
c->cpuid_level = -1; /* CPUID not detected */
c->x86_clflush_size = 32;
c->x86_phys_bits = 32;
c->x86_virt_bits = 32;
#endif
c->x86_cache_alignment = c->x86_clflush_size;
memset(&c->x86_capability, 0, sizeof c->x86_capability);
generic_identify(c);
if (this_cpu->c_identify)
this_cpu->c_identify(c);
......
#ifdef CONFIG_X86_64
c->apicid = apic->phys_pkg_id(c->initial_apicid, 0);
#endif
/*
* Vendor-specific initialization. In this section we
* canonicalize the feature flags, meaning if there are
* features a certain CPU supports which CPUID doesn't
* tell us, CPUID claiming incorrect flags, or other bugs,
* we handle them here.
*
* At the end of this section, c->x86_capability better
* indicate the features this CPU genuinely supports!
*/
if (this_cpu->c_init)
this_cpu->c_init(c);
.......
#ifdef CONFIG_X86_64
detect_ht(c);
#endif
......
#ifdef CONFIG_NUMA
numa_add_cpu(smp_processor_id());
#endif
}
identify_cpu 函数基本执行流程:
在函数内部,首先对 cpuinfo_x86 结构体的一些字段进行初始化。接下来调用 generic_identify 函数来生成处理器标识并将标识信息保存到 cpuinfo_x86 结构体中。 generic_identify 函数实现请参考 ”4.2.6.1 generic_identify“ 小节。
this_cpu 指向当前处理器的 cpu_dev 实例,对于 Intel 处理器来说,就是 intel_cpu_dev。由于 intel_cpu_dev 中并未定义 c_identify 函数,所以直接跳过。
然后,将 cpuinfo_x86 中的 apicid 字段更新为 initial_apicid。在 generic_identify 函数中,从 ”CPUID.0x1:EBX[31:24]“ 获取到 8 位的初始 APIC ID,并保存到 initial_apicid字段中。APIC 驱动的 phys_pkg_id 函数,将指定的 apicid 右移一定的位数。在本例中,右移位数为 0,所以 apicid 字段与 initial_apicid 一致。此时,这 2 者均为 8 位的 APIC ID。
接下来,将执行处理器厂商特定的初始化函数 c_init。对于 Intel 处理器来说,就是 init_intel 函数。
static const struct cpu_dev __cpuinitconst intel_cpu_dev = {
......
.c_init = init_intel,
......
}
init_intel 函数的大部分功能是围绕处理器能力展开的,我们只关注拓扑相关的部分。该函数内部会首先尝试调用 detect_extended_topology 函数执行扩展拓扑的探测。如果处理器不支持 ”CPUID.0xB“ 扩展拓扑查询,则会调用 intel_num_cpu_cores 函数执行 ”CPUID.0x4“ 查询。init_intel 函数实现请参考 ”4.2.6.2 init_intel“ 小节。
然后调用 detect_ht 函数执行 ”CPUID.0x1“ 查询。在该函中,会判断处理器是否支持 ”CPUID.0xB“ 扩展拓扑查询,如果支持,会直接返回;否则,才会真正执行查询动作。detect_ht 函数请参考 ”4.2.5 detect_ht“ 小节。
最后,调用 numa_add_cpu 函数将 cpu 添加到对应的节点。
// file: arch/x86/mm/numa.c
void __cpuinit numa_add_cpu(int cpu)
{
cpumask_set_cpu(cpu, node_to_cpumask_map[early_cpu_to_node(cpu)]);
}
其中,node_to_cpumask_map 是一个位图数组,成员数量为最大节点数 MAX_NUMNODES(扩展为 1024)。
// file: arch/x86/mm/numa.c
cpumask_var_t node_to_cpumask_map[MAX_NUMNODES];
由于一个节点下可能会有多个 cpu,所以每个成员都是一个单独的 cpu 位图。
4.2.6.1 generic_identify
// file: arch/x86/kernel/cpu/common.c
static void __cpuinit generic_identify(struct cpuinfo_x86 *c)
{
c->extended_cpuid_level = 0;
if (!have_cpuid_p())
identify_cpu_without_cpuid(c);
/* cyrix could have cpuid enabled via c_identify()*/
if (!have_cpuid_p())
return;
cpu_detect(c);
......
if (c->cpuid_level >= 0x00000001) {
c->initial_apicid = (cpuid_ebx(1) >> 24) & 0xFF;
#ifdef CONFIG_X86_32
......
#endif
c->phys_proc_id = c->initial_apicid;
}
......
}
函数内部,先将 cpuinfo_x86 实例的 extended_cpuid_level 字段初始化为 0。
接着调用 have_cpuid_p 函数,检查处理器是否支持 cpuid 指令。对于 x86-64 架构来说,cpuid 指令总是支持的;对于 x86-32 架构来说,需要额外的操作来判断。
如果不支持 cpuid 指令,那么需要调用 identify_cpu_without_cpuid 函数来识别处理器。我们只关心 x86-64架构,所以是支持 cpuid 的,identify_cpu_without_cpuid 函数我们就不深入查看了。
由于 cyrix 的 32 位处理器,会在 identify_cpu_without_cpuid 函数中调用自身 cpu_dev 结构体中的 c_identify 函数将 cpuid 指令设置成可用,而不会直接探测处理器信息,所以还要再次判断 cpuid 是否可用。如果仍然不可用,直接返回。
接下来,调用 cpu_detect 函数探测处理器的基本信息,并将其保存到 cpuinfo_x86 结构体中。详见 4.2.1 cpu_detect 小节。
再接着,如果处理器支持 “CPUID.0x1” 查询,则通过此查询获取到 EBX[31:24] 中的原始 APIC ID 的值,并保存到 initial_apicid 以及 phys_proc_id 字段中。
4.2.6.2 init_intel
// file: arch/x86/kernel/cpu/intel.c
static void __cpuinit init_intel(struct cpuinfo_x86 *c)
{
......
detect_extended_topology(c);
......
if (!cpu_has(c, X86_FEATURE_XTOPOLOGY)) {
/*
* let's use the legacy cpuid vector 0x1 and 0x4 for topology
* detection.
*/
c->x86_max_cores = intel_num_cpu_cores(c);
#ifdef CONFIG_X86_32
......
#endif
}
......
}
init_intel 函数的大部分功能是围绕处理器能力展开的。在此,我们只保留了与处理器拓扑探测相关的代码。
首先是调用 detect_extended_topology 函数执行处理器扩展拓扑探测。
如果处理器不支持扩展拓扑,则需要通过 "CPUID.0x1" 和 "CPUID.0x4" 进行探测。在本函数中, 会调用intel_num_cpu_cores 函数会执行 "CPUID.0x4" 探测,获取 package 支持的最大 core 数量。
在另外的 detect_ht 函数中,会执行 "CPUID.0x1" 探测。
4.3 MADT 解析接口
4.3.1 register_lapic_address
// file: arch/x86/kernel/acpi/boot.c
void __init register_lapic_address(unsigned long address)
{
mp_lapic_addr = address;
if (!x2apic_mode) {
set_fixmap_nocache(FIX_APIC_BASE, address);
apic_printk(APIC_VERBOSE, "mapped APIC to %16lx (%16lx)\n",
APIC_BASE, mp_lapic_addr);
}
if (boot_cpu_physical_apicid == -1U) {
boot_cpu_physical_apicid = read_apic_id();
apic_version[boot_cpu_physical_apicid] =
GET_APIC_VERSION(apic_read(APIC_LVR));
}
}
我们在 “2.3 xAPIC模式 vs x2APIC模式” 小节中介绍过,不同模式下对 APIC 寄存器的访问方式是不同的。在 xAPIC 模式下,需要使用 MMIO (将 APIC 寄存器映射到虚拟地址空间)的方式访问;而对于 x2APIC 模式,不能使用内存映射的方式,只能使用 wrmsr 或 rdmsr 指令。
首先检查系统是否处于 x2APIC 模式。如果不是,说明需要执行 MMIO,则调用 set_fixmap_nocache 函数将该 APIC 的物理基地址映射到固定映射区索引 FIX_APIC_BASE 对应的虚拟地址处。
固定映射相关内容请参考: Linux Kernel:内存管理之固定映射 (Fixmap)。
接下来,检查 BSP 的 APIC ID 是否设置(默认值为 -1)。如果未设置,则通过 read_apic_id() 函数获取到 BSP 的 APIC ID,并赋值给 boot_cpu_physical_apicid;然后将 BSP 对应的 APIC 版本,保存到 apic_version 数组中,下标就是 boot_cpu_physical_apicid。
apic_version 是一个拥有 MAX_LOCAL_APIC(扩展为 32768) 个成员的数组:
int apic_version[MAX_LOCAL_APIC];
数组下标为 APIC ID,值为 APIC 版本。
apic_read 函数会读取指定的 APIC 寄存器的内容。
APIC 的版本信息,保存在 “Local APIC Version register” 寄存器中。该寄存器的格式如下图所示:
宏 APIC_LVR 扩展为 0x30,对应着该寄存器在 xAPIC 模式下的偏移:
// file: arch/x86/include/asm/apicdef.h
#define APIC_LVR 0x30
4.3.1.1 GET_APIC_VERSION
版本寄存器中的最低 8 位(位 7-0)指示 APIC 的版本, 宏 GET_APIC_VERSION 用来获取版本值。
#define GET_APIC_VERSION(x) ((x) & 0xFFu)
注:register_lapic_address 函数只会在 BSP 中调用。
4.3.2 acpi_parse_lapic_addr_ovr
acpi_parse_lapic_addr_ovr 函数用于解析 MADT 中的 ”Local APIC Address Override“ 表项,该函数接收 2 个参数:
- @header:表项起始地址
- @end:表项的结束地址
// file: arch/x86/kernel/acpi/boot.c
static int __init
acpi_parse_lapic_addr_ovr(struct acpi_subtable_header * header,
const unsigned long end)
{
struct acpi_madt_local_apic_override *lapic_addr_ovr = NULL;
lapic_addr_ovr = (struct acpi_madt_local_apic_override *)header;
if (BAD_MADT_ENTRY(lapic_addr_ovr, end))
return -EINVAL;
acpi_lapic_addr = lapic_addr_ovr->address;
return 0;
}
acpi_subtable_header 结构体表示 MADT 中各表项的通用表头,acpi_madt_local_apic_override 结构体表示完整的 ”Local APIC Address Override“ 结构。这 2 种结构体的详细字段,请参考 ”3.2 MADT 相关数据结构“ 中的内容。
首先,使用 BAD_MADT_ENTRY 宏检查该表项是否有效。当表项地址为空或者表项的实际结束地址与 end 不符时,说明表项无效,直接返回错误码 -EINVAL。
验证无误后,将 ”Local APIC Address Override“ 表项中的 64 位物理基地址赋值给 acpi_lapic_addr。
最后,返回成功码 0。
4.3.2.1 BAD_MADT_ENTRY
BAD_MADT_ENTRY 用于验证表项是否有效,该宏接收 2 个参数:
- @entry:表项起始地址;
- @end:表项结束地址;
// file: arch/x86/kernel/acpi/boot.c
#define BAD_MADT_ENTRY(entry, end) ( \
(!entry) || (unsigned long)entry + sizeof(*entry) > end || \
((struct acpi_subtable_header *)entry)->length < sizeof(*entry))
如果表项起始地址为空,或者经过计算后的结束地址与入参 end 不一致,或者表项实际大小与 length 字段不符,都说明是无效表项,返回 1;否则,返回 0。
4.4 APIC 驱动探测
内核支持多种 APIC 驱动,但是系统中实际可用的是哪种 APIC,需要进行 APIC 探测。
首先,通过 apic_drivers 或 apic_driver 宏,将多个 APIC 的驱动注册到内核中。
apic_driver 宏注册单个 APIC 驱动:
// file: arch/x86/kernel/apic/x2apic_cluster.c
apic_driver(apic_x2apic_cluster);
// file: arch/x86/kernel/apic/x2apic_phys.c
apic_driver(apic_x2apic_phys);
apic_drivers 宏会注册多个 APIC 驱动:
// file: arch/x86/kernel/apic/apic_flat_64.c
apic_drivers(apic_physflat, apic_flat);
由于位置靠前的驱动会先被检测到,所以驱动的顺序很重要。在上述代码片段中,apic_physflat 会比 apic_flat 先被检测到。
apic_drivers 或 apic_driver 宏定义如下:
// file: arch/x86/include/asm/apic.h
#define apic_driver(sym) \
static const struct apic *__apicdrivers_##sym __used \
__aligned(sizeof(struct apic *)) \
__section(.apicdrivers) = { &sym }
#define apic_drivers(sym1, sym2) \
static struct apic *__apicdrivers_##sym1##sym2[2] __used \
__aligned(sizeof(struct apic *)) \
__section(.apicdrivers) = { &sym1, &sym2 }
这 2 个宏,会将驱动的地址注册到 .apicdrivers 节。
// file: arch/x86/kernel/vmlinux.lds.S
.apicdrivers : AT(ADDR(.apicdrivers) - LOAD_OFFSET) {
__apicdrivers = .;
*(.apicdrivers);
__apicdrivers_end = .;
}
这些驱动地址会被放置在符号 __apicdrivers 和 __apicdrivers_end 之间。
// file: arch/x86/include/asm/apic.h
extern struct apic *__apicdrivers[], *__apicdrivers_end[];
这 2 个符号被声明为 struct apic 指针数组。
由于 APIC 驱动探索顺序,基于驱动在 .apicdrivers 节中的位置,所以,驱动的位置顺序很重要。为此,在 Makefile 文件中,指定了驱动的先后顺序。
// file: arch/x86/kernel/apic/Makefile
......
ifeq ($(CONFIG_X86_64),y)
# APIC probe will depend on the listing order here
obj-$(CONFIG_X86_NUMACHIP) += apic_numachip.o
obj-$(CONFIG_X86_UV) += x2apic_uv_x.o
obj-$(CONFIG_X86_X2APIC) += x2apic_phys.o
obj-$(CONFIG_X86_X2APIC) += x2apic_cluster.o
obj-y += apic_flat_64.o
endif
# APIC probe will depend on the listing order here
obj-$(CONFIG_X86_NUMAQ) += numaq_32.o
obj-$(CONFIG_X86_SUMMIT) += summit_32.o
obj-$(CONFIG_X86_BIGSMP) += bigsmp_32.o
obj-$(CONFIG_X86_ES7000) += es7000_32.o
# For 32bit, probe_32 need to be listed last
obj-$(CONFIG_X86_LOCAL_APIC) += probe_$(BITS).o
可以看到,在 x86-64 系统下,驱动的搜索顺序为 apic_numachip、x2apic_uv_x、x2apic_phys、x2apic_cluster、apic_physflat、apic_flat。其中 apic_flat_64.c 文件中使用 apic_drivers 宏注册了 2 个驱动:apic_physflat 以及 apic_flat。
这些驱动的类型都是 struct apic,该结构体定义了 probe 函数检查系统是否支持该类型的 APIC:
// file: arch/x86/include/asm/apic.h
struct apic {
......
int (*probe)(void);
......
}
不同的驱动定义了不同 probe 函数实现,以 x2apic_phys 为例:
// file: arch/x86/kernel/apic/x2apic_phys.c
static struct apic apic_x2apic_phys = {
......
.probe = x2apic_phys_probe,
......
}
在 default_setup_apic_routing 函数中,会遍历内核支持的所有驱动,使用驱动特定的 probe 函数检查系统是否支持该驱动,如果支持,将该驱动赋值给 apic,作为系统驱动并跳出循环。从这点也能看出,驱动在 .apicdrivers 节中的顺序很重要。
void __init default_setup_apic_routing(void)
{
struct apic **drv;
enable_IR_x2apic();
for (drv = __apicdrivers; drv < __apicdrivers_end; drv++) {
if ((*drv)->probe && (*drv)->probe()) {
if (apic != *drv) {
apic = *drv;
pr_info("Switched APIC routing to %s.\n",
apic->name);
}
break;
}
}
......
}
default_setup_apic_routing 函数的调用链:
另外,在探测驱动之前,还调用了 enable_IR_x2apic 函数,尝试开启 x2APIC 模式。
// file: arch/x86/kernel/apic/apic.c
void __init enable_IR_x2apic(void)
{
......
if (x2apic_supported() && !x2apic_mode) {
x2apic_mode = 1;
enable_x2apic();
pr_info("Enabled x2apic\n");
}
......
}
其中,x2apic_mode 是一个全局变量,其初始值为 0,指示处理器是否支持 x2APIC 模式。
// file: arch/x86/kernel/apic/apic.c
int x2apic_mode;
宏 x2apic_supported 扩展为 cpu_has_x2apic:
#define x2apic_supported() (cpu_has_x2apic)
cpu_has_x2apic (见 ”4.20 cpu_has_x2apic“)会检测 BSP 是否支持 x2APIC。
如果处理器支持 x2APIC 且 x2apic_mode 没有被设置过,则将 x2apic_mode 设置为 1,并调用 enable_x2apic 函数启用 x2APIC。