五、CPU 拓扑探测的代码实现
5.1 BSP 拓扑探测
BSP 拓扑探测主要分为三步:
- BSP 基本信息查询,包括生产商、处理器能力等。这部分功能在
early_cpu_init函数中实现。 - 早期的 APIC 驱动探测。在这一步中,会探测系统支持的 APIC,并将其驱动赋值给全局变量 apic。这是在
default_acpi_madt_oem_check函数中进行的。 - BSP 拓扑信息探测。这部分功能是在
identify_boot_cpu函数中完成的。
5.1.1 BSP 基本信息查询
5.1.1.1 early_cpu_init
early_cpu_init 函数的主要功能是对 BSP 进行初始化,即获取 BSP 基本信息并将其保存到 cpuinfo_x86 结构体实例 boot_cpu_data 中。
// file: arch/x86/kernel/cpu/common.c
void __init early_cpu_init(void)
{
const struct cpu_dev *const *cdev;
int count = 0;
#ifdef CONFIG_PROCESSOR_SELECT
printk(KERN_INFO "KERNEL supported cpus:\n");
#endif
for (cdev = __x86_cpu_dev_start; cdev < __x86_cpu_dev_end; cdev++) {
const struct cpu_dev *cpudev = *cdev;
if (count >= X86_VENDOR_NUM)
break;
cpu_devs[count] = cpudev;
count++;
#ifdef CONFIG_PROCESSOR_SELECT
{
unsigned int j;
for (j = 0; j < 2; j++) {
if (!cpudev->c_ident[j])
continue;
printk(KERN_INFO " %s %s\n", cpudev->c_vendor,
cpudev->c_ident[j]);
}
}
#endif
}
early_identify_cpu(&boot_cpu_data);
}
函数内部,首先遍历内核支持的所有 x86 处理器厂商的设备驱动,将它们保存到 cpu_devs 数组中,以备后用。
cpu_devs 是一个包含 X86_VENDOR_NUM 个元素的指针数组,其定义如下:
static const struct cpu_dev *__cpuinitdata cpu_devs[X86_VENDOR_NUM] = {};
__x86_cpu_dev_start 和 __x86_cpu_dev_end 符号之间保存着不同厂商的 cpu_dev 实例地址。详见 “3.1.6 __x86_cpu_dev_start && __x86_cpu_dev_end” 小节。
如果配置了内核选项 CONFIG_PROCESSOR_SELECT,还会打印出这些设备的厂商以及标识信息。
# dmesg|grep "KERNEL supported cpus" -A 5
[ 0.000000] KERNEL supported cpus:
[ 0.000000] Intel GenuineIntel
[ 0.000000] AMD AuthenticAMD
[ 0.000000] Centaur CentaurHauls
......
最后,将初始化工作委托给 early_identify_cpu 函数。其中,参数 boot_cpu_data 是 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
5.1.1.2 early_identify_cpu
early_identify_cpu 函数用来探测 BSP 基本信息并保存到 cpuinfo_x86 结构体。
// file: arch/x86/kernel/cpu/common.c
static void __init early_identify_cpu(struct cpuinfo_x86 *c)
{
#ifdef CONFIG_X86_64
c->x86_clflush_size = 64;
c->x86_phys_bits = 36;
c->x86_virt_bits = 48;
#else
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);
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);
get_cpu_vendor(c);
get_cpu_cap(c);
if (this_cpu->c_early_init)
this_cpu->c_early_init(c);
c->cpu_index = 0;
filter_cpuid_features(c, false);
if (this_cpu->c_bsp_init)
this_cpu->c_bsp_init(c);
}
在函数一开始,对 cpuinfo_x86 结构体中的一些字段设置默认值。
接着调用 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 小节。
再接着,调用 get_cpu_vendor 函数,填充 cpuinfo_x86 中的 x86_vendor 字段。详见 5.1.1.3 get_cpu_vendor 小节。
get_cpu_cap 函数,主要是使用 cpuid 指令探测处理器的能力,并保存到 cpuinfo_x86 结构体的 x86_capability 字段中。该函数与本文关系不大,就不深入查看了。
接下来,如果当前处理器提供了 c_early_init 函数,则调用对应的函数。对于 Intel 处理器,其对应的函数为 early_init_intel。
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,
};
early_init_intel 函数中,主要是探测处理器的各种能力,并设置或清除 cpuinfo_x86.x86_capability 中对应的比特位,与本文主题关系不大,我们也不深入查看了。
再接着,将 cpu_index 字段设置为 0,该字段指示处理器的编号。
filter_cpuid_features 函数用来过滤掉一些处理器不支持的特征,与本文主题关系不大,我们也不深入查看了。
最后,由于 Intel 的处理器没有提供 c_bsp_init 函数,所以也不需要执行。
5.1.1.3 get_cpu_vendor
get_cpu_vendor 函数获取处理器的厂商信息,并保存到 cpuinfo_x86 结构体的 x86_vendor 字段中。
// file: arch/x86/kernel/cpu/common.c
static void __cpuinit get_cpu_vendor(struct cpuinfo_x86 *c)
{
char *v = c->x86_vendor_id;
int i;
for (i = 0; i < X86_VENDOR_NUM; i++) {
if (!cpu_devs[i])
break;
if (!strcmp(v, cpu_devs[i]->c_ident[0]) ||
(cpu_devs[i]->c_ident[1] &&
!strcmp(v, cpu_devs[i]->c_ident[1]))) {
this_cpu = cpu_devs[i];
c->x86_vendor = this_cpu->c_x86_vendor;
return;
}
}
printk_once(KERN_ERR
"CPU: vendor_id '%s' unknown, using generic init.\n" \
"CPU: Your system may be unstable.\n", v);
c->x86_vendor = X86_VENDOR_UNKNOWN;
this_cpu = &default_cpu;
}
首先,从 x86_vendor_id 字段中获取处理器的标识,比如 Intel 的 "GenuineIntel"。该信息是我们在 cpu_detect 函数中存入的。然后遍历 cpu_devs 数组,找到与 x86_vendor_id 一致的处理器设备信息。并将其中的 c_x86_vendor 字段保存到 cpuinfo_x86 结构的 x86_vendor 字段中。对于 Intel 处理器来,该值是 0。
如果遍历完后也没找到匹配的生产商,则打印出错误信息,并将 x86_vendor 设置为 X86_VENDOR_UNKNOWN,表示未知生产商。同时,将当前处理器设置成 default_cpu。
// file: arch/x86/kernel/cpu/common.c
static const struct cpu_dev __cpuinitconst default_cpu = {
.c_init = default_init,
.c_vendor = "Unknown",
.c_x86_vendor = X86_VENDOR_UNKNOWN,
};
5.1.2 早期 MADT 处理
5.1.2.1 early_acpi_boot_init
early_acpi_boot_init 函数主要是进行早期的 MADT 表处理。
int __init early_acpi_boot_init(void)
{
/*
* If acpi_disabled, bail out
*/
if (acpi_disabled)
return 1;
/*
* Process the Multiple APIC Description Table (MADT), if present
*/
early_acpi_process_madt();
return 0;
}
如果 ACPI 被禁用,直接返回 1。
变量 acpi_disabled 指示 ACPI 是否被禁用。由于是一个全局变量,所以其默认值为 0,表示不禁用。
// file: arch/x86/kernel/acpi/boot.c
int acpi_disabled;
在 disable_acpi 函数中,acpi_disabled 被设置为 1。
static inline void disable_acpi(void)
{
acpi_disabled = 1;
acpi_pci_disabled = 1;
acpi_noirq = 1;
}
如果 ACPI 可用,则执行 early_acpi_process_madt 函数,然后返回成功码 0。
5.1.2.2 early_acpi_process_madt
// file: arch/x86/kernel/acpi/boot.c
static void __init early_acpi_process_madt(void)
{
#ifdef CONFIG_X86_LOCAL_APIC
int error;
if (!acpi_table_parse(ACPI_SIG_MADT, acpi_parse_madt)) {
/*
* Parse MADT LAPIC entries
*/
error = early_acpi_parse_madt_lapic_addr_ovr();
if (!error) {
acpi_lapic = 1;
smp_found_config = 1;
}
if (error == -EINVAL) {
/*
* Dell Precision Workstation 410, 610 come here.
*/
printk(KERN_ERR PREFIX
"Invalid BIOS MADT, disabling ACPI\n");
disable_acpi();
}
}
#endif
}
函数开始,调用 acpi_table_parse 函数检查 MADT 表是否存在。如果存在,则调用 acpi_parse_madt 函数进行解析并设置可用的 APIC 驱动,并返回 0;否则,返回 1。
其中,ACPI_SIG_MADT 宏扩展为 "APIC",即 MADT 表的签名:
// file: include/acpi/actbl1.h
#define ACPI_SIG_MADT "APIC" /* Multiple APIC Description Table */
当确认 MADT 表存在时,调用 early_acpi_parse_madt_lapic_addr_ovr() 函数对 ”Processor Local APIC“ 结构进行解析。
如果解析成功,则将 acpi_lapic 和 smp_found_config 都设置为 1,表示系统支持 Local APIC。
如果返回错误码 -EINVAL,说明 MADT 不可用,打印错误信息并调用 disable_acpi 函数禁用 ACPI。
5.1.2.3 acpi_table_parse
acpi_table_parse 函数接收 2 个参数:
- @id:要搜索的表的 id,也就是表的类型;
- @handler:处理函数。
acpi_table_parse 函数会扫描所有 ACPI 系统描述表(System Description Table,STD),查找与 @id 匹配的表。如果找到,就调用 handler 函数对其进行处理,并返回 0;否则,返回 1。
// file: drivers/acpi/tables.c
int __init acpi_table_parse(char *id, acpi_tbl_table_handler handler)
{
struct acpi_table_header *table = NULL;
acpi_size tbl_size;
if (acpi_disabled)
return -ENODEV;
if (!handler)
return -EINVAL;
if (strncmp(id, ACPI_SIG_MADT, 4) == 0)
acpi_get_table_with_size(id, acpi_apic_instance, &table, &tbl_size);
else
acpi_get_table_with_size(id, 0, &table, &tbl_size);
if (table) {
handler(table);
early_acpi_os_unmap_memory(table, tbl_size);
return 0;
} else
return 1;
}
在函数内部,首先进行条件检测。如果系统不支持 ACPI 或 ACPI 被禁止,返回错误码 -ENODEV。如果处理函数 handler 不存在,返回错误码 -EINVAL。
// file: include/uapi/asm-generic/errno-base.h
#define ENODEV 19 /* No such device */
#define EINVAL 22 /* Invalid argument */
接下来,调用 acpi_get_table_with_size 函数在 ACPI 系统描述表中查找与 id 相匹配的表。由于 MADT 表可能有 2 个,其它表只有一个,所以当要查找的是 MADT 表时,需要在 acpi_get_table_with_size 函数中通过参数 acpi_apic_instance 指定要用的是第几个 MADT 表。
acpi_apic_instance 是静态变量,默认值为 0,表示使用第一个 MADT 表。
// file: drivers/acpi/tables.c
static int acpi_apic_instance __initdata;
可使用命令行参数 acpi_apic_instance 来指定该值,可选的值为 0、1、2。当命令行参数为 0 或者 1 时,使用第一个 MADT 表;为 2 时,使用第 2 个 MADT 表。
// file: Documentation/kernel-parameters.txt
acpi_apic_instance= [ACPI, IOAPIC]
Format: <int>
2: use 2nd APIC table, if available
1,0: use 1st APIC table
default: 0
该参数通过 acpi_parse_apic_instance 函数解析:
// file: drivers/acpi/tables.c
static int __init acpi_parse_apic_instance(char *str)
{
if (!str)
return -EINVAL;
acpi_apic_instance = simple_strtoul(str, NULL, 0);
printk(KERN_NOTICE PREFIX "Shall use APIC/MADT table %d\n",
acpi_apic_instance);
return 0;
}
early_param("acpi_apic_instance", acpi_parse_apic_instance);
如果找到匹配的表,acpi_get_table_with_size 函数会将表的地址及大小分别保存到入参 table 及 size 中,供后续使用。
如果 table 为真,说明表存在,调用 handler 函数进行处理,并返回 0;否则,返回 1。
5.1.2.4 acpi_parse_madt
该函数用于解析 MADT 表,并设置可用的 APIC 驱动。
// file: arch/x86/kernel/acpi/boot.c
static int __init acpi_parse_madt(struct acpi_table_header *table)
{
struct acpi_table_madt *madt = NULL;
if (!cpu_has_apic)
return -EINVAL;
madt = (struct acpi_table_madt *)table;
if (!madt) {
printk(KERN_WARNING PREFIX "Unable to map MADT\n");
return -ENODEV;
}
if (madt->address) {
acpi_lapic_addr = (u64) madt->address;
printk(KERN_DEBUG PREFIX "Local APIC address 0x%08x\n",
madt->address);
}
default_acpi_madt_oem_check(madt->header.oem_id,
madt->header.oem_table_id);
return 0;
}
首先检查处理器是否支持 APIC,如果不支持,返回错误码 -EINVAL。
接下来,将 ACPI 通用表头指针转换成 MADT 表头指针。如果指针为空,说明表不存在,打印错误信息,并返回错误码 -ENODEV。
MADT 表头中的 address 字段,表示 32 位的 Local APIC 的物理地址。如果该地址存在,则把它保存到静态变量 acpi_lapic_addr 中。
// file: arch/x86/kernel/acpi/boot.c
static u64 acpi_lapic_addr __initdata = APIC_DEFAULT_PHYS_BASE;
acpi_lapic_addr 的初始值为宏 APIC_DEFAULT_PHYS_BASE ,扩展为 0xfee00000:
// file: arch/x86/include/asm/apicdef.h
#define APIC_DEFAULT_PHYS_BASE 0xfee00000
我们在 “2.3 xAPIC 模式 vs x2APIC 模式” 小节中介绍过, 在 xAPIC 模式下,Local APIC 的各种寄存器会默认映射到从物理地址 0xFEE00000 (该值可配置)开始的 4KB 内存区域。所以,默认情况下,0xfee00000 就是 Local APIC 的物理基地址。
然后,执行 default_acpi_madt_oem_check 函数并返回 0。
5.1.2.5 default_acpi_madt_oem_check
default_acpi_madt_oem_check 函数功能是探索可用的 APIC 驱动。
// file: arch/x86/kernel/apic/probe_64.c
int __init default_acpi_madt_oem_check(char *oem_id, char *oem_table_id)
{
struct apic **drv;
for (drv = __apicdrivers; drv < __apicdrivers_end; drv++) {
if ((*drv)->acpi_madt_oem_check(oem_id, oem_table_id)) {
if (apic != *drv) {
apic = *drv;
pr_info("Setting APIC routing to %s.\n",
apic->name);
}
return 1;
}
}
return 0;
}
default_acpi_madt_oem_check 函数会遍历所有的 APIC 驱动,执行驱动提供的 acpi_madt_oem_check 函数进行检查。如果检查通过,说明该驱动可用,将系统驱动 apic 指定为当前驱动,并打印相关信息。
以 x2APIC 的集群模式为例,实际执行的是 x2apic_acpi_madt_oem_check 函数:
static struct apic apic_x2apic_cluster = {
......
.acpi_madt_oem_check = x2apic_acpi_madt_oem_check,
......
}
x2apic_acpi_madt_oem_check 函数直接返回 x2apic_enabled 函数的执行结果。
static int x2apic_acpi_madt_oem_check(char *oem_id, char *oem_table_id)
{
return x2apic_enabled();
}
x2apic_enabled 函数请参考 ”4.1.14 x2apic_enabled“ 小节。
5.1.2.6 early_acpi_parse_madt_lapic_addr_ovr
early_acpi_parse_madt_lapic_addr_ovr 函数用于解析 MADT 中的 ”Local APIC Address Override“ 结构。
static int __init early_acpi_parse_madt_lapic_addr_ovr(void)
{
int count;
if (!cpu_has_apic)
return -ENODEV;
/*
* Note that the LAPIC address is obtained from the MADT (32-bit value)
* and (optionally) overriden by a LAPIC_ADDR_OVR entry (64-bit value).
*/
count =
acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_APIC_OVERRIDE,
acpi_parse_lapic_addr_ovr, 0);
if (count < 0) {
printk(KERN_ERR PREFIX
"Error parsing LAPIC address override entry\n");
return count;
}
register_lapic_address(acpi_lapic_addr);
return count;
}
如果 BSP 不支持 APIC,直接返回错误码 -ENODEV。
然后调用 acpi_table_parse_madt 函数,解析 MADT 表中指定类型的表项。在本例中,是解析 ”Local APIC Address Override“ 结构。
如果 count 小于 0,说明未找到对应的表项,打印错误信息并返回 count。
然后调用 register_lapic_address 函数注册 Local APIC 。在 register_lapic_address 函数中,如果检测到系统未处于 x2APIC 模式,则会将 Local APIC 的物理基地址映射到固定映射区的虚拟地址空间。详细执行过程请参考 ”4.3.1 register_lapic_address“。
5.1.2.7 acpi_table_parse_madt
acpi_table_parse_madt 函数用于解析 MADT 表的不同表项,该函数接收 3 个参数:
- @id:要解析的表项类型,类型在
acpi_madt_type中指定。 - @handler:处理函数。不同类型的表项的对应着不同的处理函数。
- @max_entries:表项的最大数量。对于不同的表项,其最大数量也是不同的。比如,对于 ”Local APIC Address Override“ 表项,最多只能有一个;对于 ”Processor Local APIC“ 表项,最大数量依赖于系统。内核使用宏
MAX_LOCAL_APIC(扩展为 32768) 表示系统支持的最大 APIC 数量。
函数定义如下:
// file: drivers/acpi/tables.c
int __init
acpi_table_parse_madt(enum acpi_madt_type id,
acpi_tbl_entry_handler handler, unsigned int max_entries)
{
return acpi_table_parse_entries(ACPI_SIG_MADT,
sizeof(struct acpi_table_madt), id,
handler, max_entries);
}
函数内部,将功能完全委托给 acpi_table_parse_entries 函数。
MADT 表所有的表项类型请参考 ”3.2.8 MADT 表项类型“。
5.1.2.8 acpi_table_parse_entries
acpi_table_parse_entries 函数用来解析 ACPI 的某类表项,该函数接收 5 个参数:
- @id:表的类型,对应于表的签名
- @table_size:表头的大小
- @entry_id:表项类型,对应着
acpi_madt_type中的不同枚举值; - @handler:处理函数,不同表项有不同的处理函数
- @max_entries:表项的最大数量,为 0 时表示只处理一次
该函数定义如下:
// file: drivers/acpi/tables.c
int __init
acpi_table_parse_entries(char *id,
unsigned long table_size,
int entry_id,
acpi_tbl_entry_handler handler,
unsigned int max_entries)
{
struct acpi_table_header *table_header = NULL;
struct acpi_subtable_header *entry;
unsigned int count = 0;
unsigned long table_end;
acpi_size tbl_size;
if (acpi_disabled)
return -ENODEV;
if (!handler)
return -EINVAL;
if (strncmp(id, ACPI_SIG_MADT, 4) == 0)
acpi_get_table_with_size(id, acpi_apic_instance, &table_header, &tbl_size);
else
acpi_get_table_with_size(id, 0, &table_header, &tbl_size);
if (!table_header) {
printk(KERN_WARNING PREFIX "%4.4s not present\n", id);
return -ENODEV;
}
table_end = (unsigned long)table_header + table_header->length;
/* Parse all entries looking for a match. */
entry = (struct acpi_subtable_header *)
((unsigned long)table_header + table_size);
while (((unsigned long)entry) + sizeof(struct acpi_subtable_header) <
table_end) {
if (entry->type == entry_id
&& (!max_entries || count++ < max_entries))
if (handler(entry, table_end))
goto err;
/*
* If entry->length is 0, break from this loop to avoid
* infinite loop.
*/
if (entry->length == 0) {
pr_err(PREFIX "[%4.4s:0x%02x] Invalid zero length\n", id, entry_id);
goto err;
}
entry = (struct acpi_subtable_header *)
((unsigned long)entry + entry->length);
}
if (max_entries && count > max_entries) {
printk(KERN_WARNING PREFIX "[%4.4s:0x%02x] ignored %i entries of "
"%i found\n", id, entry_id, count - max_entries, count);
}
early_acpi_os_unmap_memory((char *)table_header, tbl_size);
return count;
err:
early_acpi_os_unmap_memory((char *)table_header, tbl_size);
return -EINVAL;
}
在函数内部,首先进行条件检测。如果系统不支持 ACPI 或 ACPI 被禁止,返回错误码 -ENODEV。如果处理函数 handler 不存在,返回错误码 -EINVAL。
接下来,调用 acpi_get_table_with_size 函数在 ACPI 系统描述表中查找与 id 相匹配的表。由于 MADT 表可能有 2 个,其它表只有一个,所以当要查找的是 MADT 表时,需要在 acpi_get_table_with_size 函数中通过参数 acpi_apic_instance 指定要用的是第几个 MADT 表。
如果没找到匹配的表,打印错误信息并返回错误码 -ENODEV。
上面这些步骤我们在 acpi_table_parse 函数中已经遇到过了。
......
table_end = (unsigned long)table_header + table_header->length;
/* Parse all entries looking for a match. */
entry = (struct acpi_subtable_header *)
((unsigned long)table_header + table_size);
......
table_header 表示表头的地址,table_header 中的 length 字段,指示该表的总大小。所以,table_end 指示该表的结束地址。
table_size 指示表头的大小,所以 entry 表示表头的结束地址,也是第一个表项的起始地址。
......
while (((unsigned long)entry) + sizeof(struct acpi_subtable_header) <
table_end) {
if (entry->type == entry_id
&& (!max_entries || count++ < max_entries))
if (handler(entry, table_end))
goto err;
/*
* If entry->length is 0, break from this loop to avoid
* infinite loop.
*/
if (entry->length == 0) {
pr_err(PREFIX "[%4.4s:0x%02x] Invalid zero length\n", id, entry_id);
goto err;
}
entry = (struct acpi_subtable_header *)
((unsigned long)entry + entry->length);
}
......
接下来是一个 while 循环。循环条件是表项空间不能越界,即必须小于 table_end。
循环内部,检查表项类型以及最大表项数量。如果表项类型相符并且最大表项数为 0 或者已处理的表项数小于最大数量时,则调用 handler 函数处理表项。如果 handler 函数处理失败,跳转到标签 err 处执行。
如果解析后,表项的大小为 0,说明出现错误,打印错误信息后,跳转到标签 err 处执行。
再接着,计算下一个表项的地址,进行下一轮循环。
......
if (max_entries && count > max_entries) {
printk(KERN_WARNING PREFIX "[%4.4s:0x%02x] ignored %i entries of "
"%i found\n", id, entry_id, count - max_entries, count);
}
......
如果循环结束后,如果发现已处理的表项超出了指定的最大数量,则打印出警告信息。
......
early_acpi_os_unmap_memory((char *)table_header, tbl_size);
return count;
......
early_acpi_os_unmap_memory 函数会调用早期的内存映射接口 early_iounmap,将以 table_header 为起始虚拟地址,大小为 tbl_size 的空间取消映射。
// file: drivers/acpi/osl.c
void __init early_acpi_os_unmap_memory(void __iomem *virt, acpi_size size)
{
if (!acpi_gbl_permanent_mmap)
__acpi_unmap_table(virt, size);
}
// file: arch/x86/kernel/acpi/boot.c
void __init __acpi_unmap_table(char *map, unsigned long size)
{
if (!map || !size)
return;
early_iounmap(map, size);
}
关于早期 I/O 内存映射的相关内容,请参考:Linux Kernel:内存管理之早期 I/O 内存映射(early ioremap)。
取消映射后,返回查找到的表项数量 count。
......
err:
early_acpi_os_unmap_memory((char *)table_header, tbl_size);
return -EINVAL;
}
最后,err 标签处也是调用 early_acpi_os_unmap_memory 函数取消映射,只不过返回的是错误码 -EINVAL。
5.1.3 BSP 拓扑信息查询
5.1.3.1 identify_boot_cpu
BSP 拓扑探测功能是在 identify_boot_cpu 函数中完成的。该函数将拓扑探测全部委托给 identify_cpu 函数。
// file: arch/x86/kernel/cpu/common.c
void __init identify_boot_cpu(void)
{
identify_cpu(&boot_cpu_data);
......
}
identify_cpu 函数实现请参考 ”4.2.6 identify_cpu“ 小节。
5.1.4 流程示意图
5.2 统计处理器数量
5.2.1 acpi_boot_init
// file: arch/x86/kernel/acpi/boot.c
int __init acpi_boot_init(void)
{
......
/*
* Process the Multiple APIC Description Table (MADT), if present
*/
acpi_process_madt();
......
}
上文说过,探测处理器数量需要解析 MADT 表,这是在 acpi_boot_init 函数中完成的。在该函数内部,将解析的工作委托给 acpi_process_madt 函数。
5.2.1.1 acpi_process_madt
// file: arch/x86/kernel/acpi/boot.c
static void __init acpi_process_madt(void)
{
#ifdef CONFIG_X86_LOCAL_APIC
int error;
if (!acpi_table_parse(ACPI_SIG_MADT, acpi_parse_madt)) {
/*
* Parse MADT LAPIC entries
*/
error = acpi_parse_madt_lapic_entries();
......
} else {
......
}
......
#endif
return;
}
函数开始,调用 acpi_table_parse 函数检查 MADT 表是否存在。如果存在,则调用 acpi_parse_madt 函数进行解析,并返回 0;否则,返回 1。
其中,ACPI_SIG_MADT 宏扩展为 "APIC",即 MADT 表的签名:
// file: include/acpi/actbl1.h
#define ACPI_SIG_MADT "APIC" /* Multiple APIC Description Table */
当确认 MADT 表存在时,调用 acpi_parse_madt_lapic_entries() 函数对 Local APIC 数据进行解析。
5.2.1.2 acpi_table_parse
acpi_table_parse 函数实现请参考 ”5.1.2.3 acpi_table_parse“ 小节。
5.2.1.3 acpi_parse_madt
acpi_parse_madt 函数实现请参考 ”5.1.2.4 acpi_parse_madt“ 小节。
5.2.1.4 acpi_parse_madt_lapic_entries
在找到 MADT 表之后,就需要对表里的每一项进行解析,解析工作是在 acpi_parse_madt_lapic_entries 函数中进行的。
// file: arch/x86/kernel/acpi/boot.c
static int __init acpi_parse_madt_lapic_entries(void)
{
int count;
int x2count = 0;
if (!cpu_has_apic)
return -ENODEV;
......
函数一开始,声明了 2 个变量: count 以及 x2count,并将 x2count 初始化为 0。count 用来保存解析的 ”Processor Local APIC “ 表项的数量;x2count 用来保存解析的 ”Processor Local x2APIC “ 表项的数量。
然后判断处理器是否支持 APIC,如果不支持,直接返回错误码 -ENODEV。
......
count =
acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_APIC_OVERRIDE,
acpi_parse_lapic_addr_ovr, 0);
if (count < 0) {
printk(KERN_ERR PREFIX
"Error parsing LAPIC address override entry\n");
return count;
}
......
接下来,调用 acpi_table_parse_madt 函数来解析 MADT 表中的 ”Local APIC Address Override“ 项。acpi_table_parse_madt 是解析 MADT 表的通用函数,它接收3个参数:表项类型,处理函数,最大条目数量。
在 MADT 的表头中,保存着 32 位的 Local APIC 物理基地址, ”Local APIC Address Override“ 表项中保存着 64 位的物理基地址。如果 ”Local APIC Address Override“ 表项存在,就要用其中的 64 位地址替换掉表头中的 32 位地址。”Local APIC Address Override“ 表项的格式详见 “2.6.5 Entry Type 5: Local APIC Address Override” 小节。
acpi_parse_lapic_addr_ovr 函数会从 ”Local APIC Address Override“ 表项中获取 64 位的 Local APIC 物理基地址,并更新变量 acpi_lapic_addr 的值。
acpi_table_parse_madt 函数执行成功时,会返回处理的表项数量;如果执行过程中遇到错误,就会返回负的错误码。此处根据返回值 count,来判断函数是否出错。如果 count 为负数,则打印错误信息,并返回错误码。
......
register_lapic_address(acpi_lapic_addr);
......
接下来,调用 register_lapic_address 函数注册上一步获取到的 Local APIC 的物理地址。我们在 “2.3 xAPIC模式 vs x2APIC模式” 小节中介绍过,这 2 种模式对 APIC 寄存器的访问方式是不同的。在 xAPIC 模式下,需要使用 MMIO (将 APIC 寄存器映射到虚拟地址空间)的方式访问;而对于 x2APIC,不能使用内存映射的方式,只能使用 wrmsr 或 rdmsr 指令。register_lapic_address 函数的主要功能,就是将 Local APIC 的物理基地址映射到虚拟地址空间(xAPIC 模式),该函数详细实现请参考 ”4.3.1 register_lapic_address“ 小节。
......
count = acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_SAPIC,
acpi_parse_sapic, MAX_LOCAL_APIC);
if (!count) {
x2count = acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_X2APIC,
acpi_parse_x2apic, MAX_LOCAL_APIC);
count = acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_APIC,
acpi_parse_lapic, MAX_LOCAL_APIC);
}
......
接下来,调用 acpi_table_parse_madt 函数处理 LOCAL_SAPIC 表项,处理函数为 acpi_parse_sapic。由于 SAPIC 是 IA-64 架构才有的结构,所以在 x86 架构下,该表项不会存在,返回值 count 必定为 0。
接下来就会分别解析 ”Processor Local x2APIC “ 以及 ”Processor Local APIC “ 表项,解析函数分别为 acpi_parse_x2apic (见 ”5.2.1.6“ 节)和 acpi_parse_lapic(见 ”5.2.1.9“ 节)。
......
if (!count && !x2count) {
printk(KERN_ERR PREFIX "No LAPIC entries present\n");
/* TBD: Cleanup to allow fallback to MPS */
return -ENODEV;
} else if (count < 0 || x2count < 0) {
printk(KERN_ERR PREFIX "Error parsing LAPIC entry\n");
/* TBD: Cleanup to allow fallback to MPS */
return count;
}
......
如果 count 和 x2count 均等于 0,说明未找到 Local APIC,打印错误信息并返回错误码 -ENODEV。
如果 count 和 x2count 均小于 0,说明解析时出现错误,打印错误信息并返回 count。
......
x2count =
acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_X2APIC_NMI,
acpi_parse_x2apic_nmi, 0);
count =
acpi_table_parse_madt(ACPI_MADT_TYPE_LOCAL_APIC_NMI, acpi_parse_lapic_nmi, 0);
if (count < 0 || x2count < 0) {
printk(KERN_ERR PREFIX "Error parsing LAPIC NMI entry\n");
/* TBD: Cleanup to allow fallback to MPS */
return count;
}
return 0;
}
接下来,解析 LOCAL_X2APIC_NMI 和 LOCAL_APIC_NMI 表项,与本文关系不大,略过。
5.2.1.5 acpi_table_parse_madt
acpi_table_parse_madt 函数具体实现请参考 ”5.1.2.7 acpi_table_parse_madt“ 小节。
5.2.1.6 acpi_parse_x2apic
acpi_parse_x2apic 函数用于解析 Local x2APIC 表项,该函数接收 2 个参数:
- @header:Local x2APIC 表项的起始地址;
- @end:Local x2APIC 表项的结束地址。
// file: arch/x86/kernel/acpi/boot.c
static int __init
acpi_parse_x2apic(struct acpi_subtable_header *header, const unsigned long end)
{
struct acpi_madt_local_x2apic *processor = NULL;
int apic_id;
u8 enabled;
processor = (struct acpi_madt_local_x2apic *)header;
if (BAD_MADT_ENTRY(processor, end))
return -EINVAL;
acpi_table_print_madt_entry(header);
apic_id = processor->local_apic_id;
enabled = processor->lapic_flags & ACPI_MADT_ENABLED;
#ifdef CONFIG_X86_X2APIC
/*
* We need to register disabled CPU as well to permit
* counting disabled CPUs. This allows us to size
* cpus_possible_map more accurately, to permit
* to not preallocating memory for all NR_CPUS
* when we use CPU hotplug.
*/
if (!apic->apic_id_valid(apic_id) && enabled)
printk(KERN_WARNING PREFIX "x2apic entry ignored\n");
else
acpi_register_lapic(apic_id, enabled);
#else
printk(KERN_WARNING PREFIX "x2apic entry ignored\n");
#endif
return 0;
}
函数内部,调用 BAD_MADT_ENTRY 宏来检测该表项是否有效。如果无效,返回错误码 -EINVAL。
接下来,调用 acpi_table_print_madt_entry 函数打印表项相关的信息。该函数主要是打印信息,我们就不深入查看了。
再接着,获取处理器的 x2APIC ID 并保存到变量 apic_id 中。acpi_madt_local_x2apic 结构体的 local_apic_id 字段保存的是 32 位的 x2APIC ID。
然后,根据 Flags 字段检测该处理器是否可用,并将结果保存到变量 enabled 中。
从 “2.6.6 Entry Type 9: Processor Local x2APIC” 小节中可知,Flags 字段最低位指示处理器是否可用。
宏 ACPI_MADT_ENABLED 扩展为 1,表示最低位置位:
// file: include/acpi/actbl1.h
#define ACPI_MADT_ENABLED (1) /* 00: Processor is usable if set */
apic_id_valid 函数检查 apic_id 是否有效,其实现依赖于具体的实例,我们就不深入查看了。
如果 apic_id 无效却显示可用,显然是相违背的,则将该处理器忽略,并打印出错误信息;否则,调用 acpi_register_lapic 函数注册该 Local APIC。
5.2.1.7 acpi_register_lapic
acpi_register_lapic 函数接收 2 个参数:
- @id:Local APIC ID;
- @enable:该处理器是否可用
// file: arch/x86/kernel/acpi/boot.c
static void __cpuinit acpi_register_lapic(int id, u8 enabled)
{
unsigned int ver = 0;
if (id >= (MAX_LOCAL_APIC-1)) {
printk(KERN_INFO PREFIX "skipped apicid that is too big\n");
return;
}
if (!enabled) {
++disabled_cpus;
return;
}
if (boot_cpu_physical_apicid != -1U)
ver = apic_version[boot_cpu_physical_apicid];
generic_processor_info(id, ver);
}
如果 id 大于等于 MAX_LOCAL_APIC-1,说明超出最大数量,打印错误信息并返回。
// file: arch/x86/include/asm/apicdef.h
# define MAX_LOCAL_APIC 32768
如果该处理器不可使用,则禁用处理器数量加一,即 ++disabled_cpus ,然后返回。
接下来,获取 apic 的版本,并调用 generic_processor_info 函数生成处理器信息。
5.2.1.8 generic_processor_info
generic_processor_info 函数用来生成处理器信息,该函数接收 2 个参数:
- @apicid:APIC ID
- @version:APIC 版本
// file: arch/x86/kernel/apic/apic.c
void __cpuinit generic_processor_info(int apicid, int version)
{
int cpu, max = nr_cpu_ids;
bool boot_cpu_detected = physid_isset(boot_cpu_physical_apicid,
phys_cpu_present_map);
if (!boot_cpu_detected && num_processors >= nr_cpu_ids - 1 &&
apicid != boot_cpu_physical_apicid) {
int thiscpu = max + disabled_cpus - 1;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i almost"
" reached. Keeping one slot for boot cpu."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
if (num_processors >= nr_cpu_ids) {
int thiscpu = max + disabled_cpus;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i reached."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
num_processors++;
if (apicid == boot_cpu_physical_apicid) {
cpu = 0;
} else
cpu = cpumask_next_zero(-1, cpu_present_mask);
/*
* Validate version
*/
if (version == 0x0) {
pr_warning("BIOS bug: APIC version is 0 for CPU %d/0x%x, fixing up to 0x10\n",
cpu, apicid);
version = 0x10;
}
apic_version[apicid] = version;
if (version != apic_version[boot_cpu_physical_apicid]) {
pr_warning("BIOS bug: APIC version mismatch, boot CPU: %x, CPU %d: version %x\n",
apic_version[boot_cpu_physical_apicid], cpu, version);
}
physid_set(apicid, phys_cpu_present_map);
if (apicid > max_physical_apicid)
max_physical_apicid = apicid;
#if defined(CONFIG_SMP) || defined(CONFIG_X86_64)
early_per_cpu(x86_cpu_to_apicid, cpu) = apicid;
early_per_cpu(x86_bios_cpu_apicid, cpu) = apicid;
#endif
#ifdef CONFIG_X86_32
early_per_cpu(x86_cpu_to_logical_apicid, cpu) =
apic->x86_32_early_logical_apicid(cpu);
#endif
set_cpu_possible(cpu, true);
set_cpu_present(cpu, true);
}
函数很长,我们逐段来看。
int cpu, max = nr_cpu_ids;
bool boot_cpu_detected = physid_isset(boot_cpu_physical_apicid,
phys_cpu_present_map);
首先,声明了一些变量,并将 max 及 boot_cpu_detected 进行初始化。
nr_cpu_ids 指示系统实际支持的最大 cpu 数量,该值默认为 NR_CPUS:
// file: kernel/smp.c
/* Setup number of possible processor ids */
int nr_cpu_ids __read_mostly = NR_CPUS;
NR_CPUS 是系统配置的最大 cpu 数量,即 CONFIG_NR_CPUS:
// file: include/linux/threads.h
/* Places which use this should consider cpumask_var_t. */
#define NR_CPUS CONFIG_NR_CPUS
可通过命令行参数 nr_cpus 修改 nr_cpu_ids 的值。当指定了命令行参数 nr_cpus 后,通过参数处理函数 nrcpus(),设置 nr_cpu_ids 的值。
// file: kernel/smp.c
/* this is hard limit */
static int __init nrcpus(char *str)
{
int nr_cpus;
get_option(&str, &nr_cpus);
if (nr_cpus > 0 && nr_cpus < nr_cpu_ids)
nr_cpu_ids = nr_cpus;
return 0;
}
early_param("nr_cpus", nrcpus);
nrcpus() 函数内部,调用 get_option()函数,解析参数并赋值给 nr_cpus。
如果指定的值小于 nr_cpu_ids 的初始值(即 NR_CPUS),那么将nr_cpu_ids调整为参数 nr_cpus 指定的数值。也就是说,系统实际支持的最大 CPU 数量,是配置选项 CONFIG_NR_CPUS 和命令行参数 nr_cpus 之中较小的那个值。
boot_cpu_detected 指示当前处理器是否 BSP(启动处理器)。
其中 boot_cpu_physical_apicid 是 BSP 的 APIC ID,phys_cpu_present_map 是一个 physid_mask_t 类型的全局变量,用来表示存在的处理器位图。
// file: arch/x86/kernel/apic/apic.c
physid_mask_t phys_cpu_present_map;
typedef struct physid_mask physid_mask_t;
struct physid_mask 是一个使用 unsigned long 数组表示的位图,数组容量为 PHYSID_ARRAY_SIZE:
struct physid_mask {
unsigned long mask[PHYSID_ARRAY_SIZE];
};
PHYSID_ARRAY_SIZE 是 MAX_LOCAL_APIC 转换成 long 类型后的数量:
// file: arch/x86/include/asm/mpspec.h
#define PHYSID_ARRAY_SIZE BITS_TO_LONGS(MAX_LOCAL_APIC)
所以,phys_cpu_present_map 本质上是一个容量为 MAX_LOCAL_APIC (扩展为 32768)的位图。
宏 physid_isset 用来检测指定处理器是否已经存在,其接收 2 个参数:Local APIC ID 以及 cpu 存在位图。
#define physid_isset(physid, map) test_bit(physid, (map).mask)
该宏直接使用位图接口 test_bit 来检测位图中指定的比特位是否置位。
......
if (!boot_cpu_detected && num_processors >= nr_cpu_ids - 1 &&
apicid != boot_cpu_physical_apicid) {
int thiscpu = max + disabled_cpus - 1;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i almost"
" reached. Keeping one slot for boot cpu."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
......
如果当前处理器不是 BSP,且已检测到的处理器数量 num_processors 大于等于 nr_cpu_ids - 1,说明实际插入的处理器数量超出最大允许值了,那么该处理器会被禁用,此时禁用数量加 1,即 disabled_cpus++,然后直接返回。此处之所以将上限值设置为 nr_cpu_ids - 1,是因为检测到的处理器不是 BSP,为了保证 BSP 是可用的,必须为 BSP 保留一个位置。
......
if (num_processors >= nr_cpu_ids) {
int thiscpu = max + disabled_cpus;
pr_warning(
"ACPI: NR_CPUS/possible_cpus limit of %i reached."
" Processor %d/0x%x ignored.\n", max, thiscpu, apicid);
disabled_cpus++;
return;
}
......
如果是 BSP,且已检测到的处理器数量 num_processors 大于等于 nr_cpu_ids,说明实际插入的处理器数量已经超出最大允许值了,那么该处理器会被禁用,此时禁用数量加 1,即 disabled_cpus++。然后直接返回。
......
num_processors++;
if (apicid == boot_cpu_physical_apicid) {
cpu = 0;
} else
cpu = cpumask_next_zero(-1, cpu_present_mask);
......
如果没有超出 nr_cpu_ids 的限制,那么可用的 CPU 数量加 1,即 num_processors++。
接下来要确定处理器编号。如果 apicid == boot_cpu_physical_apicid,说明当前处理器是 BSP,BSP 的编号为 0。否则,说明不是 BSP,那么调用 cpumask_next_zero() 函数,从位图 cpu_present_mask中找出第一个为 0 的位,将该位的索引作为处理器的编号。
......
if (version == 0x0) {
pr_warning("BIOS bug: APIC version is 0 for CPU %d/0x%x, fixing up to 0x10\n",
cpu, apicid);
version = 0x10;
}
apic_version[apicid] = version;
if (version != apic_version[boot_cpu_physical_apicid]) {
pr_warning("BIOS bug: APIC version mismatch, boot CPU: %x, CPU %d: version %x\n",
apic_version[boot_cpu_physical_apicid], cpu, version);
}
......
接下来校验 APIC 版本,如果版本为 0x0,说明是 BIOS 的bug,将版本校正为 0x10,并保存到 apic_version 数组中。如果当前处理器的 apic 版本与 BSP 的不一致,那么打印警告信息。
......
physid_set(apicid, phys_cpu_present_map);
if (apicid > max_physical_apicid)
max_physical_apicid = apicid;
......
既然当前处理器可用,就通过 physid_set 宏将位图 phys_cpu_present_map 中对应的比特位置位。
// file: arch/x86/include/asm/mpspec.h
#define physid_set(physid, map) set_bit(physid, (map).mask)
physid_set 宏内部委托位图接口 set_bit 来实现置位功能。
如果当前处理器的 APIC ID 的值比已探测到的最大值 max_physical_apicid 大,那么更新最大值。
// file: arch/x86/kernel/apic/apic.c
/*
* The highest APIC ID seen during enumeration.
*/
unsigned int max_physical_apicid;
max_physical_apicid 是一个全局变量,用来指示当前探测到的最大的 APIC ID。
......
#if defined(CONFIG_SMP) || defined(CONFIG_X86_64)
early_per_cpu(x86_cpu_to_apicid, cpu) = apicid;
early_per_cpu(x86_bios_cpu_apicid, cpu) = apicid;
#endif
......
如果是 64 位的 SMP 系统,将 apicid 分别保存到对应 cpu 下的 per-cpu 变量 x86_cpu_to_apicid 及 x86_bios_cpu_apicid 中。
early_per_cpu 宏获取指定 cpu 下的 per-cpu 变量,该宏接收 2 个参数:per-cpu 变量、cpu 编号。
关于 per-cpu 变量的实现,请参考:Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一)。
......
#ifdef CONFIG_X86_32
......
#endif
set_cpu_possible(cpu, true);
set_cpu_present(cpu, true);
}
最后,调用 set_cpu_possible() 函数,将 cpu 状态设置为 possible ;调用 set_cpu_present() 函数,将 cpu 状态设置为 present 。
5.2.9 acpi_parse_lapic
acpi_parse_lapic 函数用于解析 "Processor Local APIC" 表项,其解析过程与 acpi_parse_x2apic 函数基本一致。
// file: arch/x86/kernel/acpi/boot.c
static int __init
acpi_parse_lapic(struct acpi_subtable_header * header, const unsigned long end)
{
struct acpi_madt_local_apic *processor = NULL;
processor = (struct acpi_madt_local_apic *)header;
if (BAD_MADT_ENTRY(processor, end))
return -EINVAL;
acpi_table_print_madt_entry(header);
acpi_register_lapic(processor->id, /* APIC ID */
processor->lapic_flags & ACPI_MADT_ENABLED);
return 0;
}
需要说明的是,processor->id 表示的是 8 位的 APIC ID。
acpi_register_lapic 函数的实现见 ”5.2.1.7 acpi_register_lapic“ 小节。
5.3 启动 AP
当 BSP 初始化完成后,会通知 AP 启动。激活 AP 的工作是在 smp_init 函数中执行的。
// file: kernel/smp.c
/* Called by boot processor to activate the rest. */
void __init smp_init(void)
{
......
for_each_present_cpu(cpu) {
if (num_online_cpus() >= setup_max_cpus)
break;
if (!cpu_online(cpu))
cpu_up(cpu);
}
......
}
我们在 “5.2.8 generic_processor_info” 小节中介绍过,每探测到一个可用的处理器,都会将 possible 位图和 present 位图中对应的比特位置位。
在 smp_init 函数中,会遍历 cpu 存在位图中的所有处理器,如果在线处理器数量未超过设置的最大值且处理器不在线,则调用 cpu_up 函数激活该处理器。
cpu 位图相关内容请参考:Linux Kernel:CPU 状态管理之 cpumask。
Linux 内核多处理器启动流程如下图所示:
相对详细的 MP 启动过程可参考:Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(二)。
5.4 AP 拓扑探测
5.4.1 start_secondary
在 AP 启动后,会执行 start_secondary 函数。
// file: arch/x86/kernel/smpboot.c
notrace static void __cpuinit start_secondary(void *unused)
{
cpu_init();
......
smp_callin();
enable_start_cpu0 = 0;
......
set_cpu_online(smp_processor_id(), true);
......
per_cpu(cpu_state, smp_processor_id()) = CPU_ONLINE;
......
}
5.4.1.1 cpu_init
cpu_init 函数会对处理器进行初始化。同时,也会开启处理器的 x2APIC 模式。
// file: arch/x86/kernel/cpu/common.c
#ifdef CONFIG_X86_64
void __cpuinit cpu_init(void)
{
......
cpu = stack_smp_processor_id();
......
enable_x2apic();
......
}
首先,调用 stack_smp_processor_id 函数获取 cpu 编号。
然后,调用 enable_x2apic 函数,开启 x2APIC 功能。
enable_x2apic 函数实现请参考 ”4.1.13 enable_x2apic“ 小节。
5.4.1.2 smp_callin
AP 处理器的拓扑探测工作,主要是在 smp_callin 函数中完成的。在该函数中,首先通过 smp_processor_id 函数获取到当前 cpu 的编号并保存到 cpuid 中,然后调用 smp_store_cpu_info 函数探测并保存该 cpu 的信息。
// file: arch/x86/kernel/smpboot.c
static void __cpuinit smp_callin(void)
{
int cpuid, phys_id;
unsigned long timeout;
cpuid = smp_processor_id();
......
/*
* Save our processor parameters. Note: this information
* is needed for clock calibration.
*/
smp_store_cpu_info(cpuid);
......
}
5.4.1.3 smp_store_cpu_info
// file: arch/x86/kernel/smpboot.c
void __cpuinit smp_store_cpu_info(int id)
{
struct cpuinfo_x86 *c = &cpu_data(id);
*c = boot_cpu_data;
c->cpu_index = id;
/*
* During boot time, CPU0 has this setup already. Save the info when
* bringing up AP or offlined CPU0.
*/
identify_secondary_cpu(c);
}
在 smp_store_cpu_info 函数中,先是通过宏 cpu_data 获取到指定 cpu 的信息 cpu_info。cpu_info 是个 per-cpu 变量,所以每个 cpu 都有一个独立的副本。
// file: arch/x86/include/asm/processor.h
#define cpu_data(cpu) per_cpu(cpu_info, cpu)
接下来将 BSP 的数据 boot_cpu_data 复制给指定 cpu,并修改 cpu_index 字段为指定 cpu 编号。
然后调用 identify_secondary_cpu 函数探测 AP 的参数。
5.4.1.4 identify_secondary_cpu
// file: arch/x86/kernel/cpu/common.c
void __cpuinit identify_secondary_cpu(struct cpuinfo_x86 *c)
{
BUG_ON(c == &boot_cpu_data);
identify_cpu(c);
......
}
我们现在是在 AP 中运行,如果该处理器的 cpu_info 的地址与 BSP 的一致,启动的是 BSP,这是一个内核 bug,调用 BUG_ON 宏打印错误信息并将系统挂起。
接下来,调用 identify_cpu 函数进行探测。identify_cpu 函数的实现见 “4.2.6 identify_cpu” 小节。
5.5 总体步骤示意图
六、参考资料
1、Intel 64 and IA-32 Architectures Software Developer Manuals
3、Detecting CPU Topology (80x86)
5、5.2.12. Multiple APIC Description Table (MADT)
6、MADT
8、Linux Kernel:内存管理之固定映射 (Fixmap)
9、Linux Kernel:内存管理之早期 I/O 内存映射(early ioremap)
10、Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一)