Skip to content

Latest commit

 

History

History
1363 lines (976 loc) · 59.1 KB

File metadata and controls

1363 lines (976 loc) · 59.1 KB

Linux内核初始化 (第四部分)

0 平台相关初始化

在上一篇中start_kernel函数进行平台相关前的初始化,现在调用setup_arch函数进行平台相关初始化。

1 平台初始化函数(setup_arch

setup_archstart_kernel类似,比较复杂,调用了很多函数。既然是平台特性相关,我们需要重新返回arch文件夹。setup_archarch/x86/kernel/setup.c中实现,该函数只有一个参数 -- command_line的地址。接下来,我们看下其实现过程。

2 早期初始化

2.1 保留内存区域

  • 保留_text__end_of_kernel_reserve之间的内存区域
	memblock_reserve(__pa_symbol(_text),
			 (unsigned long)__end_of_kernel_reserve - (unsigned long)_text);

首先,保留_text__end_of_kernel_reserve间的内存区域,_text__end_of_kernel_reservearch/x86/kernel/vmlinux.lds.S中定义。

memblock_reserve将内存存放到memblock中的保留区域,关于memblock的介绍这里不详细介绍。

__pa_symbol是个宏定义,获取给定符号的物理地址。宏展开如下:

#define __pa_symbol(x) \
	__phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))

#define __phys_reloc_hide(x)	(x)

#define __phys_addr_symbol(x) \
	((unsigned long)(x) - __START_KERNEL_map + phys_base)
  • 保留第一个内存页

为了预防L1FT(L1 Terminal Fault)侧信道攻击,保留第一个内存页。

	memblock_reserve(0, PAGE_SIZE);
  • 保留initrd内存

调用early_reserve_initrd保留initrd内存区域。首先,获取RAM DISK的基地址、大小和结束地址;在检查BootLoader提供的ramdisk信息后,保留内存区域。整个过程如下:

	/* Assume only end is not page aligned */
	u64 ramdisk_image = get_ramdisk_image();
	u64 ramdisk_size  = get_ramdisk_size();
	u64 ramdisk_end   = PAGE_ALIGN(ramdisk_image + ramdisk_size);

	if (!boot_params.hdr.type_of_loader ||
	    !ramdisk_image || !ramdisk_size)
		return;		/* No initrd provided by bootloader */

	memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);

基地址和大小通过boot_params获取,以调用get_ramdisk_image获取基地址为例:

static u64 __init get_ramdisk_image(void)
{
	u64 ramdisk_image = boot_params.hdr.ramdisk_image;
	ramdisk_image |= (u64)boot_params.ext_ramdisk_image << 32;
	return ramdisk_image;
}

ramdisk_image的地址由两部分组成,hdr.ramdisk_image(32位的低位地址)和ext_ramdisk_image(32位高位地址),具体可参见Documentation/x86/zero-page.rst

0C0/004	ALL	ext_ramdisk_image	ramdisk_image high 32bits

2.2 OLPC检测

接下来,调用olpc_ofw_detect函数检测是否支持OLPC(One Laptop per Child)。在arch/x86/platform/olpc/olpc_ofw.c实现。

2.3 早期中断设置(idt_setup_early_traps

接下来,我们调用idt_setup_early_traps函数,在arch/x86/kernel/idt.c实现。如下:

void __init idt_setup_early_traps(void)
{
	idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts),
			     true);
	load_idt(&idt_descr);
}

/*
 * Early traps running on the DEFAULT_STACK because the other interrupt
 * stacks work only after cpu_init().
 */
static const __initconst struct idt_data early_idts[] = {
	INTG(X86_TRAP_DB,		debug),
	SYSG(X86_TRAP_BP,		int3),
#ifdef CONFIG_X86_32
	INTG(X86_TRAP_PF,		page_fault),
#endif
};

可以看到,早期中断设置初始化#DB(debug)和#BP(int3)中断处理程序,并在CONFIG_X86_32开启的情况下初始化#PF(page fault)中断处理程序。

idt_setup_from_table的处理过程在上一篇有描述,这里不再描述。

  • #DB中断处理程序

debugarch/x86/include/asm/traps.h中声明。如下:

asmlinkage void debug(void);

asmlinkage属性可以看到debug是汇编语言实现的。同其他处理函数一样,#DB中断处理函数在arch/x86/entry/entry_64.S中实现。如下:

idtentry debug do_debug	has_error_code=0 paranoid=1 shift_ist=IST_INDEX_DB ist_offset=DB_STACK_OFFSET

idtentry是一个定义中断/异常指令入口点的宏。如下:

.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1 ist_offset=0 create_gap=0 read_cr2=0
ENTRY(\sym)
	UNWIND_HINT_IRET_REGS offset=\has_error_code*8

	/* Sanity check */
	.if \shift_ist != -1 && \paranoid != 1
	.error "using shift_ist requires paranoid=1"
	.endif

	.if \create_gap && \paranoid
	.error "using create_gap requires paranoid=0"
	.endif

    ...

idtentry支持8个参数:

  • sym - 中断条目名称;
  • do_sym - 中断处理程序的C函数;
  • has_error_code - 在栈上是否有中断错误码;
  • paranoid - 如果非零,表示可以切换到特殊栈;
  • shift_ist - IST切换栈的次数,在切换栈时递减。为#DB特殊设置的,可能会出现递归栈。
  • ist_offset - IST的偏移量;
  • create_gap - 从内核模式切换时是否创建6个字的栈间隔;
  • read_cr2 - 在调用C函数前,是否加载cr2寄存器值到第三个参数;

idtentry宏展开后,通过ENTRY宏属性定义中断处理程序(如:debug)。整个处理过程如下:

  • 首先,检查输入参数是否正确;
  • 检查是否有错误码(has_error_code),无错误码时将-1压入栈中;
  • 检查paranoid参数,检查处于用户模式时,按需切换栈空间;否则跳转到idtentry_part
  • 检查create_gap参数,检查处于用户模式时,跳转到idtentry_part;否则按需创建栈间隔;
  • 接下来执行idtentry_part宏;

idtentry_part的执行过程如下:

  • 检查paranoid参数是否切换栈空间,切换时调用paranoid_entry(保存通用寄存器值,按需切换用户态gs到内核态gs);否则,调用error_entry(保存通用寄存器值,必要时切换gs);
  • 检查read_cr2参数,需要保存时,将cr2寄存器中值保存到%r12寄存器中;
  • 检查shift_ist,不等于-1时调用TRACE_IRQS_OFF_DEBUG;否则调用TRACE_IRQS_OFFTRACE_IRQS_OFF_DEBUGTRACE_IRQS_OFF进行了封装;
  • 检查paranoid == 0,且当前处于用户模式下;
  • 保存pt_regs%rdi;中断错误码到%rsi%r12(保存的是%cr2)到%rdx;必要时减少CPU_TSS_IST值;
  • 调用do_sym函数,如:do_debug;
  • 中断处理完成后,必要时恢复CPU_TSS_IST值;
  • 检查paranoid参数,通过paranoid_exiterror_exit恢复之前栈空间;

#DB中断处理程序调用C函数是do_debug函数,在arch/x86/kernel/traps.c中实现。它接收两个参数,pt_regserror_code

#BP中断处理程序类似,调用do_int3函数。

2.4 早期CPU相关设置

  • 最大访问物理内存设置

设置boot_cpu_data最大访问的物理内存;

  • CPU初始化

调用early_cpu_init函数,在arch/x86/kernel/cpu/common.c中实现。从x86_cpu_dev.init段获取CPU信息(如:供应商信息),并初始化boot_cpu_data

  • Intel理想nops设置

调用arch_init_ideal_nops函数,在arch/x86/kernel/alternative.c中实现。根据boot_cpu_data.x86_vendor设置不同的ideal_nops

  • 跳转标签初始化

调用jump_label_init函数,在kernel/jump_label.c中实现。初始化__jump_table段中跳转标签。跳转标签提升跳转的命中率,参见Documentation/static-keys.txt

2.5 早期ioremap初始化

通常有两种与设备通信的方式,I/O端口设备内存。我们在Linux内核启动过程中见过第一种方式(通过outb/inb指令)。第二种方式将I/O物理地址映射到虚拟地址上,当CPU访问物理地址时,它可以读取到映射了I/O设备的内存。ioremap这种方式就是用来将设备内存映射到内核地址空间。

接下来调用early_ioremap_init函数,将I/O内存映射到内核地址空间,在arch/x86/mm/ioremap.c实现。

early_ioremap_initFIX_BTMAP_BEGINFIX_BTMAP_END之间的固定虚拟地址进行映射。主要执行如下:

	pmd_t *pmd;

#ifdef CONFIG_X86_64
	BUILD_BUG_ON((fix_to_virt(0) + PAGE_SIZE) & ((1 << PMD_SHIFT) - 1));
#else
	WARN_ON((fix_to_virt(0) + PAGE_SIZE) & ((1 << PMD_SHIFT) - 1));

	early_ioremap_setup();

	pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
	memset(bm_pte, 0, sizeof(bm_pte));
	pmd_populate_kernel(&init_mm, pmd, bm_pte);
    
    ...

	if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))) {
		WARN_ON(1);
        ...
	}
#endif

可以看到,进行了如下操作:

  • 定义了pmd_t类型的指针,并检查边界是否正确对齐;
  • 调用early_ioremap_setup填充512个临时的固定映射表;
  • 获取pmd页中间目录项,并设置到内核地址中;
  • 检查结束边界,确保在同一pmd页表中。

early_ioremap_setupmm/early_ioremap.c实现。将512个临时的fixmap映射到到8个slot_virt中,如下:

void __init early_ioremap_setup(void)
{
	int i;
	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		if (WARN_ON(prev_map[i]))
			break;
	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

...
#define NR_FIX_BTMAPS		64
#define FIX_BTMAPS_SLOTS	8
#define TOTAL_FIX_BTMAPS	(NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)

3 设备初始化

3.1 获取设备信息

在调用setup_olpc_ofw_pgd完成pgd设置后,接下来获取设备信息。

  • 获取根设备的主次设备号
	ROOT_DEV = old_decode_dev(boot_params.hdr.root_dev);

设备的主设备号用来识别和这个设备有关的驱动,次设备号用来表示使用该驱动的各设备。old_decode_dev函数从boot_params获取了一个参数,从内核引导协议中可以看到:

Field name:    root_dev
Type:        modify (optional)
Offset/size:    0x1fc/2
Protocol:    ALL

  The default root device device number.  The use of this field is
  deprecated, use the "root=" option on the command line instead.

old_decode_devinclude/linux/kdev_t.h实现。它根据设备主次设备号调用MKDEV宏生成一个dev_t类型的设备。

static __always_inline dev_t old_decode_dev(u16 val)
{
	return MKDEV((val >> 8) & 255, val & 255);
}

其中dev_t是用来表示主/次设备号对的一个内核数据类型。由于历史原因,目前有两种管理主次设备号的方法,第一种方法(old dev)主次设备号占用16bit,主设备号占用8bit,次设备号占用8bit。但是这会引入一个问题:最多只能支持256个主设备号和256个次设备号。因此后来引入了第二种方法(new dev),使用32bit来表示主次设备号,其中主设备号占用12bit,次设备号占用20bit用来表示,你可以在new_decode_dev的实现中找到。

  • 获取设备信息

接下来,从boot_params中获取显示器相关参数、扩展显示识别数据、视频模式、BootLoader类型等。必要时获取apm bios,ist bios,rd_image信息,并设置EFI相关信息。如下:

	screen_info = boot_params.screen_info;
	edid_info = boot_params.edid_info;
...

#ifdef CONFIG_BLK_DEV_RAM
...
#endif
#ifdef CONFIG_EFI
...
		set_bit(EFI_BOOT, &efi.flags);
		set_bit(EFI_64BIT, &efi.flags);
	}
#endif

3.2 资源内存映射

  • 背景介绍

在从boot_params结构中获取到设备信息后,需要设置I/O内存。内核的主要功能是进行资源管理,其中一个资源就是内存。前面我们了解到有两种方式与设备通信(I/O端口和设备内存映射)。有关资源注册的信息可以通过/proc/ioports/proc/iomem获取。

  • /proc/ioports - 提供供设备输入输出的注册端口;
  • /proc/iomem - 提供每个物理设备的物理内存映射区域;

我们先看下/proc/iomem:

cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c99ff : Video ROM
000ca000-000cadff : Adapter ROM
000cb000-000cb5ff : Adapter ROM
000f0000-000fffff : Reserved
  000f0000-000fffff : System ROM
...
240000000-2bfffffff : PCI Bus 0000:00

可以看到,根据不同的层次显示十六进制的一段地址区域。Linux内核提供了一种通用的方式来管理这些设备。全局资源(如:PICs或I/O端口)被划分到与硬件总线相关的子集中。在内核中使用struct resource来表示:

struct resource {
	resource_size_t start;
	resource_size_t end;
	const char *name;
	unsigned long flags;
	unsigned long desc;
	struct resource *parent, *sibling, *child;
};

struct resource将系统资源的以树形结构抽象。该结构在include/linux/ioport.h中定义,包括:资源占用的起止地址范围、资源名称、标记、描述、树形资源结构指针。

  • iomem_resource

每个资源子集都有自己个根资源。如:iomem资源为iomem_resource,在kernel/resource.c中定义,如下:

struct resource iomem_resource = {
	.name	= "PCI mem",
	.start	= 0,
	.end	= -1,
	.flags	= IORESOURCE_MEM,
};
EXPORT_SYMBOL(iomem_resource);

iomem_resource定义了资源名称(PCI mem),开始地址(0),标记(IORESOURCE_MEM)。接下来,我们需要设置iomem_resource的结束地址,如下:

	iomem_resource.end = (1ULL << boot_cpu_data.x86_phys_bits) - 1;

    ...
	boot_cpu_data.x86_phys_bits = MAX_PHYSMEM_BITS;

即,iomem_resource可以支持访问最大的内存地址。iomem_resource是通过EXPORT_SYMBOL宏传递的,这个宏可以把指定的符号(例如iomem_resource)做动态链接。换句话说,它可以支持动态加载模块的时候访问对应符号。

  • e820__memory_setup

接下来,调用e820__memory_setup函数实现内存映射。e820__memory_setuparch/x86/kernel/e820.c中实现。如下:

void __init e820__memory_setup(void)
{
	char *who;

	/* This is a firmware interface ABI - make sure we don't break it: */
	BUILD_BUG_ON(sizeof(struct boot_e820_entry) != 20);

	who = x86_init.resources.memory_setup();

	memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
	memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));

	pr_info("BIOS-provided physical RAM map:\n");
	e820__print_table(who);
}

首先,我们来看下x86_init.resources.memory_setupx86_init是一种x86_init_ops类型的结构体,用来进行资源初始化,pci平台特定的一些设置函数等。x86_init的初始化实现在arch/x86/kernel/x86_init.c中。如下:

struct x86_init_ops x86_init __initdata = {

	.resources = {
		.probe_roms		= probe_roms,
		.reserve_resources	= reserve_standard_io_resources,
		.memory_setup		= e820__memory_setup_default,
	},
    ...
	.oem = {
		.arch_setup		= x86_init_noop,
		.banner			= default_banner,
	},
    ...
    ...
}

可以看到,x86_init.resources.memory_setupe820__memory_setup_default,同样在在arch/x86/kernel/e820.c中实现。它对在内核启动过程中所有的E820信息进行整理,并填充到e820_table_kexece820_table_firmware中,在收集所有的区域后,通过e820__print_table输出所有的内存信息。我们可以通过dmsg找到类似下面的信息:

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffdcfff] usable
[    0.000000] BIOS-e820: [mem 0x00000000bffdd000-0x00000000bfffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000023fffffff] usable
...
  • parse_setup_data

接下来,调用parse_setup_data解析boot_params.hdr.setup_data,将存放在其中的不同类型的设备信息(如:DTBEFI、E820_EXT)等。

setup_data指向的是一个struct setup_data结构的单向链表。如下:

struct setup_data {
	__u64 next;
	__u32 type;
	__u32 len;
	__u8 data[0];
};

其中next指向下一个节点的物理地址,最后一个节点为0。

3.3 复制EDD

调用copy_edd函数,复制boot_params结构中EDD相关信息。如下:

static inline void __init copy_edd(void)
{
     memcpy(edd.mbr_signature, boot_params.edd_mbr_sig_buffer,
	    sizeof(edd.mbr_signature));
     memcpy(edd.edd_info, boot_params.eddbuf, sizeof(edd.edd_info));
     edd.mbr_signature_nr = boot_params.edd_mbr_sig_buf_entries;
     edd.edd_info_nr = boot_params.eddbuf_entries;
}

4 内存描述符初始化

  • 背景介绍

每个进程都有自己运行的内存地址空间,这个地址空间有个特殊的数据结构叫做内存描述符(memory descriptor)。Linux内核中使用mm_struct来表示内存描述符,该结构在include/linux/mm_types.h定义。

mm_struct包含了许多与进程地址空间有关的字段,如:代码段的起始和结束地址、数据段的起始和结束地址、brk的起始和结束地址、内存区域的数量等等。task_struct结构中mmactive_mm字段包含了每个进程自己的内存描述符。 我们的第一个init_task进程也有自己的内存描述符,在之前的描述中可以看到初始化信息:

struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
	__init_task_data
#endif
= {
	...
	.mm		= NULL,
	.active_mm	= &init_mm,
	...
}

mm表示进程实际的地址空间,active_mm表示匿名进程的地址空间,通常指向init_mm。在Documentation/vm/active_mm.rst可以了解更多内容。

init_mm是初始化阶段的内存描述符定义,在mm/init-mm.c定义,如下:

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.arg_lock	=  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	.cpu_bitmap	= CPU_BITS_NONE,
	INIT_MM_CONTEXT(init_mm)
};
  • init_mm段相关初始化

接下来,我们在初始化阶段完成内存描述发中内核代码段、数据段和brk段的初始化:

	if (!boot_params.hdr.root_flags)
		root_mountflags &= ~MS_RDONLY;
	init_mm.start_code = (unsigned long) _text;
	init_mm.end_code = (unsigned long) _etext;
	init_mm.end_data = (unsigned long) _edata;
	init_mm.brk = _brk_end;
  • 内存扩展保护初始化mpx_mm_init

mpx_mm_initarch/x86/include/asm/mpx.h定义。

  • 段资源初始化

接下来,进行代码段、数据段、bss段资源的初始化。

	code_resource.start = __pa_symbol(_text);
	code_resource.end = __pa_symbol(_etext)-1;
	data_resource.start = __pa_symbol(_etext);
	data_resource.end = __pa_symbol(_edata)-1;
	bss_resource.start = __pa_symbol(__bss_start);
	bss_resource.end = __pa_symbol(__bss_stop)-1;

	...
static struct resource data_resource = {
	.name	= "Kernel data",
	.start	= 0,
	.end	= 0,
	.flags	= IORESOURCE_BUSY | IORESOURCE_SYSTEM_RAM
};

在上一部分中,我们对resource进行了描述。现在,我们把数据段、代码段、bss段资源的初始化,在/proc/iomem可以看到:

00100000-bffdcfff : System RAM
  01000000-01e00e70 : Kernel code
  01e00e71-0284cc7f : Kernel data
  02b17000-02ffffff : Kernel bss

5 解析早期参数

5.1 命令行初始化

接下来,根据不同的配置选项,获取boot_command_linebuiltin_cmdline,并最终初始化command_line。如下:

	strlcpy(command_line, boot_command_line, COMMAND_LINE_SIZE);
	*cmdline_p = command_line;

cmdline_psetup_arch(&command_line)的入参,现在进行了赋值。

5.2 NX位设置(x86_configure_nx

NX-bit或者no-execute位是页目录条目的第63比特位,它的作用是控制被映射的物理页面是否具有执行代码的能力。只有在EFER.NXE置为1(使能)的情况下,即,no-execute页保护机制开启的情况下,才能被使用或设置。

x86_configure_nx函数在arch/x86/mm/setup_nx.c中实现。该函数会检查CPU是否支持NX-bit,以及是否被禁用。在检查后,我们把结果赋值给_supported_pte_mask

void x86_configure_nx(void)
{
	if (boot_cpu_has(X86_FEATURE_NX) && !disable_nx)
		__supported_pte_mask |= _PAGE_NX;
	else
		__supported_pte_mask &= ~_PAGE_NX;
}

5.3 解析早期参数(parse_early_param

  • 背景介绍

根据名称我们可以了解到,这个函数解析命令行参数,并基于给定的参数创建不同的服务。所有的内核命令行参数可以在Documentation/admin-guide/kernel-parameters.txt找到。

在前面的章节中,我们在初始化earlyprintk时用arch/x86/boot/cmdline.c中的__cmdline_find_option, __cmdline_find_option_bool函数寻找内核参数及值。现在,我们在通用内核部分,不依赖特定的系统架构,这里使用另一种方法。

在查看Linux内核源代码时,你可能会注意到这样的调用:

early_param("debug", debug_kernel);
early_param("quiet", quiet_kernel);

parse_early_param正是解析命令行参数,并对early_param相关函数调用的。early_param宏需要两个参数,即:命令行参数的名称调用函数。该宏在include/linux/init.h定义,如下:

struct obs_kernel_param {
	const char *str;
	int (*setup_func)(char *);
	int early;
};

#define __setup_param(str, unique_id, fn, early)			\
	static const char __setup_str_##unique_id[] __initconst		\
		__aligned(1) = str; 					\
	static struct obs_kernel_param __setup_##unique_id		\
		__used __section(.init.setup)				\
		__attribute__((aligned((sizeof(long)))))		\
		= { __setup_str_##unique_id, fn, early }

#define __setup(str, fn)						\
	__setup_param(str, fn, fn, 0)

#define early_param(str, fn)						\
	__setup_param(str, fn, fn, 1)

可以看到,early_param只是调用__setup_param。而__setup_param在内部根据unique_id(即函数名称)创建了obs_kernel_param类型的变量,并将其存放在__section(.init.setup)段。在include/asm-generic/vmlinux.lds.h可以看到,.init.setup段被放置在 __setup_start__setup_end之间,如下:

#define INIT_SETUP(initsetup_align)					\
		. = ALIGN(initsetup_align);				\
		__setup_start = .;					\
		KEEP(*(.init.setup))					\
		__setup_end = .;
  • 实现过程

parse_early_paraminit/main.c实现。如下:

/* Check for early params. */
static int __init do_early_param(char *param, char *val,
				 const char *unused, void *arg)
{
	const struct obs_kernel_param *p;
	for (p = __setup_start; p < __setup_end; p++) {
		if ((p->early && parameq(param, p->str)) ||
		    (strcmp(param, "console") == 0 &&
		     strcmp(p->str, "earlycon") == 0)
		) {
			if (p->setup_func(val) != 0)
				pr_warn("Malformed early option '%s'\n", param);
		}
	}
	/* We accept everything at this stage. */
	return 0;
}

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
	static int done __initdata;
	static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

	if (done)
		return;

	/* All fall through to do_early_param. */
	strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
	parse_early_options(tmp_cmdline);
	//parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param);
	done = 1;
}

parse_early_param函数主要过程如下:

  • 在内部定义了两个静态变量,done用来检查该函数是否已经调用过,tmp_cmdline用来存放临时存放命令行;
  • 在对tmp_cmdline赋值后,调用同文件中parse_early_options函数。parse_early_options调用kernel/params.c中的parse_args函数;
  • parse_args解析命令参数,并调用do_early_param函数;
  • do_early_param__setup_start循环到__setup_end,逐个判断obs_kernel_param实例中的earlystr字段,符合时,调用setup_func进行对应的操作。

5.4 打印NX信息(x86_report_nx

x86_report_nx函数在arch/x86/mm/setup_nx.c中实现。该函数打印有关NX的提示信息。

在上面我们调用了x86_configure_nx配置了NX位。值得注意的是,x86_report_nx函数不一定在x86_configure_nx函数之后调用,但是一定在parse_early_param之后调用。答案很简单: 因为内核支持noexec参数,所以我们一定在parse_early_param调用并且解析noexec参数之后才能调用x86_report_nxx86_report_nx的输出信息如下:

[    0.000000] NX (Execute Disable) protection: active
[    0.000000] SMBIOS 2.8 present.

6 内存解析完成

接下来,这部分涉及到memblocke820相关的函数,包括:

	memblock_x86_reserve_range_setup_data();
	e820__reserve_setup_data();
	e820__finish_early_params();

	...
	e820_add_kernel_range();
	trim_bios_range();
	max_pfn = e820__end_of_ram_pfn();
	max_low_pfn = e820__end_of_low_ram_pfn();
  • memblock_x86_reserve_range_setup_data

setup_data段进行重新映射并保留内存块。

  • e820__reserve_setup_data

功能同memblock_x86_reserve_range_setup_data类似,除了重新映射之外,还会调用e820__range_update更新e820_tablee820_table_kexec的映射区域。

  • e820__finish_early_params

e820中可以通过memmemmap这两个early_param更新e820_table。这里在通过memmap选项配置后,重新更新e820_table,并输出内存信息。

  • e820_add_kernel_range

_text_end之间的物理内存区域进行映射。如果.text.data.bss这几个段没有被标记为E820_TYPE_RAM,输出提示信息后,重新映射。

  • trim_bios_range

将前4KB内存标记为E820_TYPE_RESERVED;如果BIOS区域(640->1MB)是RAM时,释放该区域;并更新e820_table

  • e820__end_of_ram_pfn

获取最后最后一个内存页的的编号,每个内存页都有唯一的编号(页帧号,page frame number)。PFN通过entry->addr >> PAGE_SHIFT计算得到的,因此,最大的页帧标号(MAX_ARCH_PFN)在x86_64定义为MAXMEM>>PAGE_SHIFT,即,0x400000000(4级页表的情况下)。

dmesg的输出中可以看到last_pfn:

[    0.028861] last_pfn = 0x240000 max_arch_pfn = 0x400000000
  • e820__end_of_low_ram_pfn

获取低端内存(或4GB内存)的页帧编号。

7 建立设备树

7.1 桌面管理接口设置(DMI)

接下来,调用dmi_setup函数,收集桌面管理接口(DMI,Desktop Management Interface)信息。

dmi_setup函数在drivers/firmware/dmi_scan.c实现,如下:

void __init dmi_setup(void)
{
	dmi_scan_machine();
	if (!dmi_available)
		return;

	dmi_memdev_walk();
	dump_stack_set_arch_desc("%s", dmi_ids_string);
}
  • dmi_scan_machine

dmi_scan_machine函数遍历SMBIOS, System Management BIOS结构,并提取信息。目前有两种方式来访问SMBIOS表:第一种方式从EFI配置表中获取SMBIOS的地址;第二种方式是扫描0xF0000~0x10000之间的物理内存。两种方式获取DMI的方式类似,均是从iomap区域读取内存后,对读取的内存调用dmi_smbios3_presentdmi_present。这两个函数检查内存是否以_SM3__SM_开始的字符串,并获取SMBIOS的版本和_DMI_的属性(如_DMI_版本、数量、地址等)。在dmesg中可以看到相关信息:

[    0.000000] SMBIOS 2.8 present.
[    0.000000] DMI: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.14.0-0-g155821a1990b-prebuilt.qemu.org 0
  • dmi_memdev_walk

该函数定义实现如下:

static void __init dmi_memdev_walk(void)
{
	if (dmi_walk_early(count_mem_devices) == 0 && dmi_memdev_nr) {
		dmi_memdev = dmi_alloc(sizeof(*dmi_memdev) * dmi_memdev_nr);
		if (dmi_memdev)
			dmi_walk_early(save_mem_devices);
	}
}

...
static int __init dmi_walk_early(void (*decode)(const struct dmi_header *,
		void *))
{
	...
}

dmi_walk_early函数有一个参数,是个回调函数。在逐个遍历DMI信息后,进行回调操作。

dmi_memdev_walk函数通过dmi_walk_early函数收集内存设备的相关信息。count_mem_devices累加dmi_memdev_nr的值,save_mem_devicesdmi_header转换为dmi_memdev_infodmi_decode解析dmi_header

7.2 虚拟机管理器初始化(init_hypervisor_platform

Hypervisor是一种虚拟化技术。init_hypervisor_platform函数在arch/x86/kernel/cpu/hypervisor.c实现。

void __init init_hypervisor_platform(void)
{
	const struct hypervisor_x86 *h;
	h = detect_hypervisor_vendor();
	if (!h)
		return;
	copy_array(&h->init, &x86_init.hyper, sizeof(h->init));
	copy_array(&h->runtime, &x86_platform.hyper, sizeof(h->runtime));
	x86_hyper_type = h->type;
	x86_init.hyper.init_platform();
}

init_hypervisor_platform函数通过detect_hypervisor_vendor获取hypervisor_x86后,将相关信息复制到x86_init.hyperx86_platform.hyper,最后调用x86_init.hyper.init_platform初始化虚拟机平台。

目前,支持的虚拟化平台定义如下:

static const __initconst struct hypervisor_x86 * const hypervisors[] =
{
	&x86_hyper_xen_pv, //"Xen PV"
	&x86_hyper_xen_hvm, //"Xen HVM"
	&x86_hyper_vmware, //"VMware"
	&x86_hyper_ms_hyperv, //"Microsoft Hyper-V"
	&x86_hyper_kvm, //"KVM"
	&x86_hyper_jailhouse, //"Jailhouse"
	&x86_hyper_acrn, //"ACRN"
};

7.3 时间戳计数器早期初始化(tsc_early_init

时间戳计数器(TSC, Time Stamp Counter)记录CPU复位后的周期数。

tsc_early_init函数在arch/x86/kernel/tsc.c中实现。

该函数校准CPU,获取CPU的周期频率,计算loops_per_jiffy。在dmesg可以看出如下信息:

[    0.000000] tsc: Fast TSC calibration using PIT
[    0.000000] tsc: Detected 2592.073 MHz processor

7.4 建立iomem资源树

在前面,对iomem_resource进行了描述。我们字段resource是一个树形结构,通过parent,sibling,child这三个形成树形结构,如下:

+-------------+
|    parent   |
+-------------+
       |
+-------------+      +-------------+
|    current  |------|    sibling  |
+-------------+      +-------------+
       |
+-------------+
|    child    | 
+-------------+

接下来,构建iomem_resource下面的资源组织结构。如下:

x86_init.resources.probe_roms();
insert_resource(&iomem_resource, &code_resource);
insert_resource(&iomem_resource, &data_resource);
insert_resource(&iomem_resource, &bss_resource);

x86_init.resources.probe_roms();定义为probe_roms,在arch/x86/kernel/probe_roms.c中实现。将system_rom_resource,extension_rom_resource,adapter_rom_resources,video_rom_resource逐个调用request_resource函数挂载在iomem_resource下。

request_resourceinsert_resource函数都在kernel/resource.c中实现。insert_resource函数执行过程中会调用request_resource

7.5 早期GART内存检查(early_gart_iommu_check

GART(Graphics address remapping table)是供APG和PCIe显卡使用的IO内存管理单元。

early_gart_iommu_check函数在arch/x86/kernel/aperture_64.c中实现。

其中search_agp_bridge函数遍历PCI信息,每个PCI域可以承载多达256条总线,并且每条总线可以承载多达32个设备,依次读取read_pci_config。如下:

	for (bus = 0; bus < 256; bus++) {
		for (slot = 0; slot < 32; slot++) {
			for (func = 0; func < 8; func++) {
				class = read_pci_config(bus, slot, func, PCI_CLASS_REVISION);
			}
		}
	}

8 均衡多处理器配置(find_smp_config

接下来是解析SMP(Symmetric multiprocessing)的配置信息。find_smp_config函数的实现如下:

static inline void find_smp_config(void)
{
        x86_init.mpparse.find_smp_config();
}

//arch/x86/kernel/x86_init.c
.find_smp_config = default_find_smp_config,

在函数的内部,x86_init.mpparse.find_smp_config函数即arch/x86/kernel/mpparse.c中的default_find_smp_config函数。default_find_smp_config函数从内存中的最低的1K基础内存(640K)的最后1Kbios中的64K的区域来寻找SMP的配置信息,并在找到它们的时候返回:

	if (smp_scan_config(0x0, 0x400) ||
	    smp_scan_config(639 * 0x400, 0x400) ||
	    smp_scan_config(0xF0000, 0x10000))
		return;

smp_scan_config函数在指定的内存区域中循环查找MP floating pointer structure,这个结构定义为struct mpf_intel。通过检查当前字节是否指向SMP签名(_MP_),检查签名的校验和,并且检查标准版本号值(这个值只能是1或者4)。struct mpf_intel定义如下:

struct mpf_intel {
        char signature[4];
        unsigned int physptr;
        unsigned char length;
        unsigned char specification;
        unsigned char checksum;
        unsigned char feature1;
        unsigned char feature2;
        unsigned char feature3;
        unsigned char feature4;
        unsigned char feature5;
};

如果搜索成功,就调用memblock_reserve函数保留该区域内存,并为多处理器配置表保留物理地址。

9 保留内存区域设置

  • 分配页表内存(early_alloc_pgt_buf)

下一步,我们可以看到early_alloc_pgt_buf函数的调用,这个函数在早期阶段分配页表缓冲区。页表缓冲区将被放置在brk段中。如下:

#ifndef CONFIG_RANDOMIZE_MEMORY
#define INIT_PGD_PAGE_COUNT      6
#else
#define INIT_PGD_PAGE_COUNT      12
#endif
#define INIT_PGT_BUF_SIZE	(INIT_PGD_PAGE_COUNT * PAGE_SIZE)
RESERVE_BRK(early_pgt_alloc, INIT_PGT_BUF_SIZE);
void  __init early_alloc_pgt_buf(void)
{
        unsigned long tables = INIT_PGT_BUF_SIZE;
        phys_addr_t base;

        base = __pa(extend_brk(tables, PAGE_SIZE));

        pgt_buf_start = base >> PAGE_SHIFT;
        pgt_buf_end = pgt_buf_start;
        pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}

首先这个函数获得页表缓冲区的大小(即:INIT_PGT_BUF_SIZE),这个值为6 * PAGE_SIZE12 * PAGE_SIZE(在开启地址随机化选项的情况下)。我们得到了页表缓冲区的大小后,调用extend_brk函数扩展brk区域。extend_brk需要传入两个参数: sizealign。在linux内核链接脚本中看到brk区段在内存中的位置就在BSS区段后面:

	. = ALIGN(PAGE_SIZE);
	.brk : AT(ADDR(.brk) - LOAD_OFFSET) {
		__brk_base = .;
		. += 64 * 1024;		/* 64k alignment slop space */
		*(.brk_reservation)	/* areas brk users have reserved */
		__brk_limit = .;
	}

我们也可以使用readelf工具来找到它:

#x86_64-elf-readelf  -S
  [58] .bss              NOBITS           ffffffff82b17000  01f17000
       00000000004e9000  0000000000000000  WA       0     0     4096
  [59] .brk              NOBITS           ffffffff83000000  01f17000
       000000000002c000  0000000000000000  WA       0     0     1

之后我们用_pa宏得到了新的brk区段的物理地址,并计算页表缓冲区的基地址和结束地址。

  • 保留brk段内存(reserve_brk)

接下来,我们调用reserve_brk函数将brk区段设置为保留内存块:

static void __init reserve_brk(void)
{
	if (_brk_end > _brk_start)
		memblock_reserve(__pa_symbol(_brk_start),
				 _brk_end - _brk_start);

	_brk_start = 0;
}

注意在reserve_brk的最后,我们把_brk_start赋值为0,因为在这之后我们不会再为brk分配内存了。

  • 清理高映射内存(cleanup_highmap)

我们需要使用cleanup_highmap函数来释放内核映射中越界的内存区域。内核映射是__START_KERNEL_map__START_KERNEL_map + size区间的内存,(其中,size = _end - _text) 或者level2_kernel_pgt对内核codedatabss区段的映射。clean_high_map函数在arch/x86/mm/init_64.c中实现,如下:

	unsigned long vaddr = __START_KERNEL_map;
	unsigned long vaddr_end = __START_KERNEL_map + KERNEL_IMAGE_SIZE;
	unsigned long end = roundup((unsigned long)_brk_end, PMD_SIZE) - 1;
	pmd_t *pmd = level2_kernel_pgt;

	if (max_pfn_mapped)
		vaddr_end = __START_KERNEL_map + (max_pfn_mapped << PAGE_SHIFT);

	for (; vaddr + PMD_SIZE - 1 < vaddr_end; pmd++, vaddr += PMD_SIZE) {
		if (pmd_none(*pmd))
			continue;
		if (vaddr < (unsigned long) _text || vaddr > end)
			set_pmd(pmd, __pmd(0));
	}

检查内核映射的开始和结束位置,循环遍历所有内核页中间目录条目, 并且清除不在_textend区段中的PMD目录项。

  • 限制memblock大小(memblock_set_current_limit)

在这之后,我们使用memblock_set_current_limit函数来为memblock分配内存设置一个界限。这个界限可以是`ISA_END_ADDRESS(0x00100000)。

  • 填充e820到memblock(e820__memblock_setup)

然后调用e820__memblock_setup函数将e820_table里的内存信息填充到memblock中。在命令行里有memblock=debug参数时,可以在dmesg中看到以下类似信息:

MEMBLOCK configuration:
 memory size = 0x1fff7ec00 reserved size = 0x1e30000
 memory.cnt  = 0x3
 memory[0x0]	[0x00000000001000-0x0000000009efff], 0x9e000 bytes flags: 0x0
 memory[0x1]	[0x00000000100000-0x000000bffdffff], 0xbfee0000 bytes flags: 0x0
 memory[0x2]	[0x00000100000000-0x0000023fffffff], 0x140000000 bytes flags: 0x0
 reserved.cnt  = 0x3
 reserved[0x0]	[0x0000000009f000-0x000000000fffff], 0x61000 bytes flags: 0x0
 reserved[0x1]	[0x00000001000000-0x00000001a57fff], 0xa58000 bytes flags: 0x0
 reserved[0x2]	[0x0000007ec89000-0x0000007fffffff], 0x1377000 bytes flags: 0x0
  • reserve_ibft_region

reserve_ibft_region函数用来寻找ibft(iSCSI Boot Format Table)区域,存在相关区域后保留该区域内存。该区域在IBFT_START(0x80000, 512K)IBFT_END(0x100000, 1MB)之间,但需要避开VGA区域(0xA0000 ~ 0xC0000)。在drivers/firmware/iscsi_ibft_find.c实现该区域的查找。

  • reserve_bios_regions

reserve_bios_regions函数保留系统BIOS固件内存区域。在arch/x86/kernel/ebda.c中实现。

  • early_reserve_e820_mpc_new

early_reserve_e820_mpc_new函数在e820_table_kexec中为多处理器规格表分配额外的内存。在arch/x86/kernel/mpparse.c中实现。

  • reserve_real_mode

reserve_real_mode函数保留从0x0 ~ 1M的低端内存用作到实模式的跳板(用于重启等...)。在arch/x86/realmode/init.c中实现。

  • trim_platform_memory_ranges

trim_platform_memory_ranges函数用于清除掉以0x20050000,0x20110000,0x20130000,0x20138000,0x40004000等地址开头的内存空间。Sandy Bridge graphics在这些内存区域出现一些问题。在arch/x86/kernel/setup.c文件中实现。

  • trim_low_memory_range

trim_low_memory_range函数保留memblock中的前4KB~64KB大小内存。大小可通过reservelow参数设置,默认64KB。在arch/x86/kernel/setup.c文件中实现。

  • init_mem_mapping

init_mem_mapping函数用于在PAGE_OFFSET处重建物理内存(0 ~ max_pfn << PAGE_SHIFT)的直接映射,在命令行传入memtest的参数时,测试内存。在arch/x86/mm/init.c中实现。

  • early_trap_pf_init

early_trap_pf_init函数用于建立#PF的中断处理函数。在arch/x86/kernel/idt.c中实现。

  • 更新mmu_cr4_features

接下来,使用当前CR4寄存器值更新mmu_cr4_features(并且间接的更新trampoline_cr4_features)。

	mmu_cr4_features = __read_cr4() & ~X86_CR4_PCIDE;

10 设置日志缓冲区(setup_log_buf

setup_log_buf函数在kernel/printk/printk.c中实现。它设置内核日志循环缓冲区,其大小取决于CONFIG_LOG_BUF_SHIFT的配置。在内核中,日志缓冲区的定义如下:

#define LOG_ALIGN __alignof__(struct printk_log)
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
#define LOG_BUF_LEN_MAX (u32)(1 << 31)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
static char *log_buf = __log_buf;
static u32 log_buf_len = __LOG_BUF_LEN;

setup_log_buf函数有一个参数,标识是否为早期设置。第一次调用时是早期设置,后续调用时分配precpu区域。具体实现过程为:首先检查当前缓冲区是否为空,是否为早期设置等,不是早期设置,调用log_buf_add_cpu为每个CPU增加缓冲区大小;接下来,检查new_log_buf_len大小(根据命令行参数log_buf_len计算),更新内核缓冲区的大小,必要时调用memblock_alloc分配新的缓冲区。

11 保留initrd(reserve_initrd

在前面我们调用early_reserve_initrd保留了initrd镜像区域,并通过init_mem_mapping重建了直接内存映射。现在调用reserve_initrd函数将initrd移动到直接映射内存。

reserve_initrd函数主要执行过程如下:

  • 必要的检查

获取initrd的镜像地址和大小,计算memblock映射内存的大小。在映射内存不足时,调用Kernel panic函数,打印panic信息。

	mapped_size = memblock_mem_size(max_pfn_mapped);
	if (ramdisk_size >= (mapped_size>>1))
		panic("initrd too large to handle, "
		       "disabling initrd (%lld needed, %lld available)\n",
		       ramdisk_size, mapped_size>>1);
  • 映射initrd

调用pfn_range_is_mapped检查initrd区域是否已经映射。如果已经映射了,重新设置initrd_startinitrd_end的位置。否则,调用relocate_initrd进行重新映射。relocate_initrd调用memblock_find_in_range查找直接映射区域,没找符合大小的区域是,进入panic。正常情况下,调用copy_from_early_memioremem区域移动到重新映射区域。

  • 释放映射memblock内存

relocate_initrd映射后,调用memblock_free释放early_reserve_initrd预留的内存。

12 ACPI相关初始化

12.1 ACPI初始化

接下来,进行ACPI(Advanced Configuration and Power Interface)初始化。

首先,调用acpi_table_upgrade函数从固件内存中查找对应的文件信息,并填充acpi_initrd_files,在drivers/acpi/tables.c中实现。之后,调用acpi_boot_table_init初始化initial_tables,并保留相关区域内存。

存在ACPI信息是,在dmesg中可以找到如下信息:

[    0.065291] ACPI: Early table checksum verification disabled
[    0.066126] ACPI: RSDP 0x00000000000F5A20 000014 (v00 BOCHS )
...
[    0.069098] ACPI: Reserving FACP table memory at [mem 0xbffe15ff-0xbffe1672]
[    0.069141] ACPI: Reserving DSDT table memory at [mem 0xbffdfd80-0xbffe15fe]
[    0.069155] ACPI: Reserving FACS table memory at [mem 0xbffdfd40-0xbffdfd7f]
...

12.2 VSMP初始化(vsmp_init

vsmp_init函数实现ScaleMP vSMP系统的初始化。在arch/x86/kernel/vsmp_64.c中实现。

12.3 IO延时初始化(io_delay_init)

io_delay_init函数运行我们重新设置默认的I/O延时端口(即,0x80)。我们在启动阶段进入保护模式前,设置过io_delay。接下来,我们看下其实现过程。io_delay_init函数在arch/x86/kernel/io_delay.c中实现。如下:

void __init io_delay_init(void)
{
	if (!io_delay_override)
		dmi_check_system(io_delay_0xed_port_dmi_table);
}

在判断io_delay_override变量允许时进行I/O延时端口设置。io_delay_override通过命令行io_delay选项设置(即,通过early_param("io_delay", io_delay_param)参数),io_delay选项包括:

io_delay=    [X86] I/O delay method
    0x80
        Standard port 0x80 based delay
    0xed
        Alternate port 0xed based delay (needed on some systems)
    udelay
        Simple two microseconds delay
    none
        No delay

dmi_check_system函数检查io_delay_0xed_port_dmi_table中DMI设备,并通过dmi_io_delay_0xed_port回调函数将其I/O端口设置为0xeddmi_check_system函数在drivers/firmware/dmi_scan.c中实现。

12.4 早期ACPI启动初始化(early_acpi_boot_init)

early_acpi_boot_init函数在arch/x86/kernel/acpi/boot.c中实现。

在检查acpi_disabled参数设置后;调用acpi_table_init_complete初始化所有的initial_tables,并检查Multiple APIC Description Table (MADT);然后,调用acpi_table_parse函数解析ACPI_SIG_BOOT表;接下来,调用acpi_blacklisted判断是否在黑名单里,并根据acpi=force判断是禁用acpi还是强制使用;接下来,在存在madt的情况下,调用early_acpi_process_madt函数处理;最后,调用acpi_reduced_hw_init进行硬件初始化。

可通过early_param("acpi", parse_acpi);进行acpi相关设置;

13 硬件访问相关内存设置

13.1 NUMA内存初始化(initmem_init)

initmem_init函数在arch/x86/mm/numa_64.c中实现,直接调用x86_numa_init函数。x86_numa_init函数在arch/x86/mm/numa.c中实现。进行非统一内存访问(UNMA,Non-uniform memory access)初始化。

13.2 分配DMA区域(dma_contiguous_reserve

接下来,我们需要调用dma_contiguous_reserve函数分配直接内存访问(DMA,Direct memory access)内存区域。dma_contiguous_reserve函数在kernel/dma/contiguous.c中实现。

DMA是设备不通过CPU直接访问内存的特殊模式,dma_contiguous_reserve函数需要一个参数,即:保留内存的限制。实现如下:

	phys_addr_t selected_size = 0;
	phys_addr_t selected_base = 0;
	phys_addr_t selected_limit = limit;
	bool fixed = false;
	...
	if (size_cmdline != -1) {
		...
	} else {
		...
	}
	
	if (selected_size && !dma_contiguous_default_area) {
		...
		dma_contiguous_reserve_area(selected_size, selected_base,
					    selected_limit,
					    &dma_contiguous_default_area,
					    fixed);
	}

首先,定义变量,selected_size表示保留区大小,selected_base表示保留区的基地址,selected_limit表示保留区的结束地址,fixed表示保留区存放的位置。fixed = true表示我们只使用memblock_reserve的保留区域,否则,使用memblock_phys_alloc_range分配内存。

接下来,检查size_cmdline大小,判断使用内核默认设置还是通过cma命令行参数。通过early_param("cma", early_cma);早期参数可设置保留区间,参数为cma=nn[MG]@[start[MG][-end[MG]]]。如果没有设置cma参数,则使用系统配置选项,配置选项包括以下选项:

  • CONFIG_CMA_SIZE_SEL_MBYTES - MB大小, 默认的全局CAM区域,大小为 CMA_SIZE_MBYTES * SZ_1M 或者 CONFIG_CMA_SIZE_MBYTES * 1M
  • CONFIG_CMA_SIZE_SEL_PERCENTAGE - 占所有内存的比例;
  • CONFIG_CMA_SIZE_SEL_MIN - 使用默认值和比例值之间的较小值;
  • CONFIG_CMA_SIZE_SEL_MAX - 使用默认值和比例值之间的较大值;

在计算保留区域的大小后,我们通过调用dma_contiguous_reserve_area函数保留该区域内存。dma_contiguous_reserve_area函数根据基地址和大小保留连续的内存区域。

在后面,通过memblock_find_dma_reserve函数计算DMA区域的大小。

13.3 稀疏内存初始化

接下来,我们调用x86_init.paging.pagetable_init();函数。pagetable_init值为native_pagetable_init,而native_pagetable_init是个宏定义,定义为paging_initpaging_initarch/x86/mm/init_64.c中实现。

paging_init函数初始化稀疏内存(sparse memory)的区域大小。稀疏内存是Linux内核中内存管理的一个特殊的基础,它在NUMA系统中即将内存区域分成不同的内存库。实现如下:

void __init paging_init(void)
{
	sparse_memory_present_with_active_regions(MAX_NUMNODES);
	sparse_init();

	node_clear_state(0, N_MEMORY);
	if (N_MEMORY != N_NORMAL_MEMORY)
		node_clear_state(0, N_NORMAL_MEMORY);

	zone_sizes_init();
}

首先,我们调用sparse_memory_present_with_active_regions函数记录每个NUMA节点内存区域到mem_section结构数组里,mem_section结构数组包括指向struct page的指针;sparse_memory_present_with_active_regions函数在mm/page_alloc.c中实现; 接下来,调用sparse_init函数分配非线性区域段(mem_section),并为每个段分配mem_map记录物理地址的映射;sparse_init函数在mm/sparse.c中实现; 接下来,调用node_clear_state清除节点状态, 最后,zone_sizes_init函数初始化区(zone)的大小;每个NUMA节点都被划分成若干块,每块称为区(zone);zone_sizes_init函数在arch/x86/mm/init.c中实现。

14 映射vsyscall

虚拟系统调用(vsyscall, virtual system call)是一种特殊的系统调用,不需要任何特殊的特级权限即可运行。如gettimeofday(),它所做的就是读取内核当前时间,内存运行将当前时间的内存页以只读方式映射到用户空间。使用vsyscall时,可以不用切换到内核空间。

map_vsyscall函数映射vsyscall的内存空间,在arch/x86/entry/vsyscall/vsyscall_64.c中实现。如下:

void __init map_vsyscall(void)
{
	extern char __vsyscall_page;
	unsigned long physaddr_vsyscall = __pa_symbol(&__vsyscall_page);

	if (vsyscall_mode == EMULATE) {
		__set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall,
			     PAGE_KERNEL_VVAR);
		set_vsyscall_pgtable_user_bits(swapper_pg_dir);
	}

	if (vsyscall_mode == XONLY)
		gate_vma.vm_flags = VM_EXEC;

	BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
		     (unsigned long)VSYSCALL_ADDR);
}

在函数的开始,我们定义了两个变量。第一个变量是extern char __vsyscall_page,作为一个外部变量,在其他的源代码文件中定义,我们在arch/x86/entry/vsyscall/vsyscall_emu_64.S找到了其定义。__vsyscall_page符号指向gettimeofday之类vsyscalls的对齐页。定义如下:

__PAGE_ALIGNED_DATA
	.globl __vsyscall_page
	.balign PAGE_SIZE, 0xcc
	.type __vsyscall_page, @object
__vsyscall_page:

	mov $__NR_gettimeofday, %rax
	syscall
	ret

	.balign 1024, 0xcc
	mov $__NR_time, %rax
	syscall
	ret

	.balign 1024, 0xcc
	mov $__NR_getcpu, %rax
	syscall
	ret

	.balign 4096, 0xcc

	.size __vsyscall_page, 4096

第二个变量是physaddr_vsyscall指向__vsyscall_page变量的物理内存。接下来,我们检查vsyscall_mode变量,它支持三种不同的设置EMULATE, XONLY, NONE,可通过early_param("vsyscall", vsyscall_setup);来设置。

vsyscall_mode设置为EMULATE时,将physaddr_vsyscall映射到fixmap;设置为XONLY时,将gate_vma.vm_flags设置为EXEC(可执行)标记,gate_vma是一个struct vm_area_struct,即内存区域结构。

15 SMP设置

15.1 获取SMP配置

在前面,我们通过find_smp_config函数查找SMP配置信息,现在我们需要调用get_smp_config函数获取SMP的配置信息。get_smp_config函数调用x86_init.mpparse.get_smp_config(0);,指向default_get_smp_configdefault_get_smp_config函数在arch/x86/kernel/mpparse.c中实现。

default_get_smp_config首先检查smp_found_config变量(smp_scan_config找到SMP配置的标记),mpf_found等变量;存在smp配置信息后,从内存中读取struct mpf_intel,进行相关初始化,如:feature1标记,检查physptr等。

15.2 CPU设置

接下来,调用prefill_possible_map函数,填充所有可用的CPU的cpumask,设置为在线状态。该函数arch/x86/kernel/smpboot.c中实现。

init_cpu_to_node函数在初始化早期设置所有可用CPU到NUMA节点。该函数在arch/x86/mm/numa.c中实现。

16 setup_arch的其余部分

前面只是介绍了setup_arch函数中部分初始化功能,其他的功能当然很重要,但这些细节不会包含在这部分。剩余的部分包含了和NUMASMPAPICAPICEFI相关特性。如:

  • init_apic_mappings

init_apic_mappings函数设置本地APIC的地址。在arch/x86/kernel/apic/apic.c中实现。

  • io_apic_init_mappings

io_apic_init_mappings函数初始化本地 I/O APIC。在arch/x86/kernel/apic/io_apic.c中实现。

  • x86_init.resources.reserve_resources

x86_init.resources.reserve_resources函数指向的是reserve_standard_io_resources,保留标准I/O资源(如:DMAtimerFPU等)。在arch/x86/kernel/setup.c中实现。

  • mcheck_init

mcheck_init函数初始化MCE, Machine check exception。在arch/x86/kernel/cpu/mce/core.c中实现。

  • register_refined_jiffies

register_refined_jiffies函数注册Jiffy。在kernel/time/jiffies.c中实现。

  • unwind_init

unwind_init函数初始化栈展开信息。在arch/x86/kernel/unwind_orc.c中实现。对在include/asm-generic/vmlinux.lds.h中定义orc_unwind_iporc_unwindorc_lookup信息进行排序。

17 结束语

本文描述了Linux内核平台相关初始化过程,主要进行内存、CPU、内核早期参数设置、外接设备等初始化。

本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue

如果你发现中文翻译有任何问题,请提交PR或者创建issue