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

843 阅读33分钟
CPU_topolopy_detect_xmind.png

五、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_datacpuinfo_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_lapicsmp_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 函数会将表的地址及大小分别保存到入参 tablesize 中,供后续使用。

如果 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 流程示意图

BSP_topolopy_detect.png

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,不能使用内存映射的方式,只能使用 wrmsrrdmsr 指令。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;
	}
......

如果 countx2count 均等于 0,说明未找到 Local APIC,打印错误信息并返回错误码 -ENODEV

如果 countx2count 均小于 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);

首先,声明了一些变量,并将 maxboot_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_SIZEMAX_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_apicidx86_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_start_up.png

相对详细的 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_infocpu_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 总体步骤示意图

cpu_detect_step_total.png

六、参考资料

1、Intel 64 and IA-32 Architectures Software Developer Manuals

2、Simultaneous MultiThreading

3、Detecting CPU Topology (80x86)

4、x86 Topology

5、5.2.12. Multiple APIC Description Table (MADT)

6、MADT

7、Intel 8259

8、Linux Kernel:内存管理之固定映射 (Fixmap)

9、Linux Kernel:内存管理之早期 I/O 内存映射(early ioremap)

10、Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一)

11、Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(二)

12、Linux Kernel:CPU 状态管理之 cpumask