Linux Kernel:CPU 拓扑结构探测(一)

1,160 阅读1小时+
CPU_topolopy_detect_xmind.png

本文采用 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)给出的处理器层级关系如下图所示:

x86_topology_1.png

在上图中,使用 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 interruptIPI)、APIC计时器中断(APIC timer generated interrupt) 等多种中断。

在支持超线程的多处理器系统下,其中断系统架构如下图所示:

APIC_in_SMP.png

在通电启动时,系统硬件会为每一个 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_Register.png

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(见下图)。

Local_APIC_Version_Register.png

xAPIC 模式下,Local APIC 的各种寄存器会映射到从物理地址 0xFEE00000 (该值可配置)开始的 4KB 内存区域。使用时,需要将该物理内存区域映射到系统的虚拟内存空间,即通过**内存映射(MMIO)**的方式访问。

x2APIC 模式下,这些寄存器被设计成了 MSR(Model Specific Register),不能通过内存映射(MMIO)的方式访问了,必须使用 rdmsr 和 wrmsr 指令访问

不同模式下,寄存器地址的映射关系见下表:

Local_APIC_Version_Register_Offset.png

比如,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" 查询,输出结果如下:

寄存器信息描述
EAXcpuid 指令支持的基础信息查询的最大输入值
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)
ECXcpu 支持的能力信息
EDXcpu 支持的能力信息

提示:

  • 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。

Core_ID_shift.png

第二次,EAX=0xB 且 ECX=1,返回结果中的 EAX[4:0] 就是 Package 层级的右移位数 package_id_shift。

Package_ID_shift.png

尽管 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,那实际值会向上圆整为 23=82^{3}=8,此时 core_id 占用的比特位数为 3,即 core_id_bits = 3。

我们在 CPUID.0x1:EBX[23:16] 中,获取到了package 中逻辑处理器的最大数量,该值同样需要向上圆整到 2 的整次幂。比如 EBX[23:16] = 10,那么要向上圆整到 24=162^{4} = 16 ,说明实际可用的最大逻辑处理器数量为 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_plus_smt_bits.png

core_id_bits:

core_id_bits.png

smt_id_bits:

smt_id_bits.png

2.5 三级拓扑算法

结合 2.4 小节,我们能够得到三级拓扑结构的算法:

  1. 执行 “CPUID.0x0” 查询,获取处理器基本信息。

    • EAX 寄存器中保存着 cpuid 指令所支持的基本信息查询的最大向量
    • EBX、ECX、EDX 寄存器中保存处理器标识
  2. 执行 “CPUID.0x1” 查询,检查处理器是否支持超线程。

    • 如果不支持 HTT 功能( EDX[28] 被清除),说明是不支持超线程的单核处理器。在这种情况下,smt_id_bits 和 core_id_bits 都为 0。拓扑探测完毕。

    • 如果支持 HTT 功能,但 “CPUID.0x0” 的返回结果中 EAX 寄存器的值小于 0xB,说明不支持扩展拓扑,则跳到第 4 步执行。

  3. 执行 “CPUID.0xB” 查询,获取扩展拓扑信息。

    • 如果 “CPUID.0xB.0x0” 的返回结果中,EBX 寄存器的值为 0,说明不支持扩展拓扑,则跳到第 4 步执行;

    • 否则,说明支持扩展拓扑。通过改变 ECX 的输入值,可以获取到不同层级的位偏移 core_id_shift 以及 package_id_shift;

    • 从 EDX[31:0] 中获取 32 位 x2APIC ID 的值。

    • 拓扑探测完毕。

  4. 通过 “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 通用表头开始,通用表头格式如下:

FieldByte LengthByte OffsetDescription
Signature40表的签名,用来区分不同的表
Length44表的完整大小(包括表头)
Revision18Revision
Checksum19Entire table must sum to zero.
OEMID610OEM ID
OEM Table ID816OEM Table ID
OEM Revision424OEM Revision
Creator ID428Creator ID
Creator Revision432Creator Revision

在通用表头中,我们只关注签名(Signature)和 长度(Length) 字段。签名字段用于区分不同的表,对于 MADT 表来说,该字段的值是 “APIC”。长度字段指示该表的总大小(包括表头)。

2.6.2 MADT 表头专用字段

在通用表头之后,是 MADT 表头的专用字段:

FieldByte LengthByte OffsetDescription
Local Interrupt Controller Address436The 32-bit physical address at which each processor can access its local interrupt controller.
Flags440Multiple 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 处理器相关的表项主要有以下几种:

ValueDescription**For Processor **For an I/O APIC
0Processor Local APICyesno
1I/O APICnoyes
2Interrupt Source Overridenoyes
3Non-maskable Interrupt (NMI) Sourcenoyes
4Local APIC NMIyesno
5Local APIC Address Overridenono
6I/O SAPICnoyes
7Local SAPICyesno
8Platform Interrupt Sourcesnoyes
9Processor Local x2APICyesno
0xALocal x2APIC NMIyesno
........................

在不同类型的表项中,我们主要关注与 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 个字节

OffsetLengthDescription
01Entry Type
11Record Length

2.6.4 Type 0: Processor Local APIC

FieldOffsetLengthDescription
Type010
Length118
ACPI Processor UID21ACPI Processor UID
APIC ID31The processor’s local APIC ID.
Flags44Flags (bit 0 = Processor Enabled) (bit 1 = Online Capable)

该表项的 APIC ID 字段中,保存着 8 位的 APIC ID。

Flags 字段指示处理器是否可用。

Local APIC FlagsBit LengthBit OffsetDescription
Enabled10
Online Capable11
Reserved302Must be zero.

如果 Flags 中的位 0 被置位,说明处理器可被系统使用。如果位 0 没有置位,就需要检查位 1。如果位 1 被置位,说明系统硬件支持操作系统在运行时启用该处理器;否则说明该处理器不能使用。

2.6.5 Type 5: Local APIC Address Override

该表项是可选的,如果定义了该表项,就需要用表项中的 64 位物理地址替换掉 MADT 表头中的 32 位地址。

FieldOffsetLengthDescription
Type115
Length1112
Reserved22Reserved
Local APIC Address4864-bit physical address of Local APIC

2.6.6 Type 9: Processor Local x2APIC

该表项提供 x2APIC ID。

FieldOffsetLengthDescription
Type010
Length1116
Reserved22Reserved - Must be zero
X2APIC ID44Processor's local x2APIC ID
Flags84Flags (same as the Local APIC flags)
ACPI Processor UID124ACPI 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_infostruct 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_datastruct 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_APICACPI_MADT_TYPE_LOCAL_APIC_OVERRIDEACPI_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_capclear_cpu_cap 宏,分别调用位图接口 set_bitclear_bitx86_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 项示意如下:

Local_APIC_Version_Register_Offset.png

其算法就是将偏移地址右移 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,其格式如下图所示:

IA32_APIC_BASE_MSR_x2APIC.png

其中位 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_levelx86_vendor_id 字段中。

接下来,如果处理器支持 ”CPUID EAX=0x1“ 类型查询,那么执行该查询,获取对应的处理器信息。

返回的结果中 EAX、EBX、ECX、EDX 寄存器的值分别存入 tfmsmiscjunkcap0 中。

tfms 中保存着处理器的版本信息,包括 Type、 Family、Model 以及 Stepping ID。可以看到,变量名就是这些信息的首字母组合。这些数据被解析后,分别存入 cpuinfo_x86 结构体的 x86x86_modelx86_mask 字段中。这些字段与本文关系不大,所以就不深入介绍了。

misc 的位 15-8( EBX[15:8] )中,保存着 CLFLUSH 指令刷新的缓存行大小(以 8 字节为单位),该值乘以 8 就得到以字节为单位的缓存行大小。

junkcap0 中,保存着处理器支持的能力,每个比特位对应一个能力。其中 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_x86cpuid_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_x86initial_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_siblingssmp_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_widthht_mask_width 如下图所示:

core_plus_mask_width.png

计算过程可参考 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_msbcore_bits 中。

detect_ht.png

最终,计算出处理器的 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 函数基本执行流程:

identify_cpu.png

在函数内部,首先对 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 模式,不能使用内存映射的方式,只能使用 wrmsrrdmsr 指令。

首先检查系统是否处于 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” 寄存器中。该寄存器的格式如下图所示:

Local_APIC_Version_Register.png

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_driversapic_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_driversapic_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_numachipx2apic_uv_xx2apic_physx2apic_clusterapic_physflatapic_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 函数的调用链:

apic_driver_detect.png

另外,在探测驱动之前,还调用了 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。