ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 内核初始化第六部分:深入 x86_64 架构相关的 setup_arch 初始化流程

Linux 内核初始化第六部分:深入 x86_64 架构相关的 setup_arch 初始化流程 文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载本文是 Linux 内核初始化系列Initialization/README.md的第六篇承接 linux-initialization-5.md 对arch/x86/kernel/setup.c中setup_arch函数的学习继续剖析该巨型函数中紧随x86_configure_nx之后的架构相关初始化步骤早期内核命令行参数解析、e820 内存图收尾、DMI 系统信息采集、SMP 配置扫描以及早期页表缓冲区brk的分配。读完本文你将掌握内核如何用early_param宏登记并解析引导参数、如何从 e820 表推导max_pfn/max_low_pfn/high_memory、如何通过 SMBIOS 与 MP 浮点指针结构收集硬件信息以及内核如何在 brk 区域为早期分页建立页表缓冲区——这些正是理解start_kernel启动全貌所必需的架构级细节。引言回到 setup_arch 函数在上一篇 linux-initialization-5.md 中我们学习了arch/x86/kernel/setup.c中架构相关本文以x86_64为例的初始化内容并停留在x86_configure_nx函数处——该函数根据对 NX bit禁止执行位的支持情况设置_PAGE_NX标志。setup_arch与start_kernel都非常庞大因此本文以及下一篇将继续围绕架构相关的初始化过程展开。紧随x86_configure_nx之后的调用依次是parse_early_param解析内核命令行中的早期参数x86_report_nx报告 NX 位配置结果memblock_x86_reserve_range_setup_data为setup_data预留内存acpi_mps_check检查 ACPI/MP 表PCI 早期 dumppciearlydump时触发一组 e820 内存图收尾函数DMI 系统信息采集SMP 配置扫描brk 区域页表缓冲区分配等。下面逐一深入。早期内核命令行参数解析parse_early_param在最早的启动阶段见 linux-bootstrap-2.md内核曾使用cmdline_find_option、__cmdline_find_option、__cmdline_find_option_bool这些位于arch/x86/boot/cmdline.c的辅助函数来查找内核参数。但那是引导协议阶段、与架构强相关的做法进入通用内核部分后采用了一套完全不同的、与架构无关的机制early_param宏。early_param 宏登记一个早期参数在内核源码中经常能看到如下调用early_param(gbpages, parse_direct_gbpages_on);early_param宏接受两个参数命令行参数名以及当该参数被传入时将被调用的处理函数。它定义在include/linux/init.h中#define early_param(str, fn) \ __setup_param(str, fn, fn, 1)可以看到early_param只是对__setup_param宏的一次封装最后一个参数1表示这是“早期参数”#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 }这个宏做了两件事定义一个名为__setup_str_函数名的__initconst常量字符串数组其内容就是命令行参数名如gbpages并用__aligned(1)保证按 1 字节对齐定义一个类型为obs_kernel_param的静态变量__setup_函数名用__used防止被优化掉并用__section(.init.setup)将它放入.init.setup段初始化字段为{ 参数名, 处理函数, 是否早期 }。obs_kernel_param 结构obs_kernel_param结构体定义如下struct obs_kernel_param { const char *str; int (*setup_func)(char *); int early; };它包含三个字段str内核参数的名字setup_func根据参数执行配置的函数指针setup_func(char *)入参是该参数后面的值字符串可为NULLearly该参数是否为早期参数1为是0为否。.init.setup 段与链接脚本注意__setup_param宏使用__section(.init.setup)属性这意味着所有__setup_str_*变量都会被放置到.init.setup段。结合include/asm-generic/vmlinux.lds.h中的链接脚本定义可以看到它们会被放置在__setup_start与__setup_end两个符号之间#define INIT_SETUP(initsetup_align) \ . ALIGN(initsetup_align); \ VMLINUX_SYMBOL(__setup_start) .; \ *(.init.setup) \ VMLINUX_SYMBOL(__setup_end) .;正是因为所有early_param注册的条目都被链接器集中放到__setup_start与__setup_end之间内核才能在运行时统一遍历它们。parse_early_param 的实现parse_early_param定义在init/main.c中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); done 1; }它做了三件事用静态变量done保证该函数只执行一次早期参数只需解析一次用strlcpy把boot_command_line内核完整命令行拷贝到临时缓冲区tmp_cmdline调用同文件的parse_early_options(tmp_cmdline)。parse_early_options随后调用kernel/params.c中的parse_args后者逐词解析命令行并对每个早期参数调用do_early_param。do_early_param从__setup_start遍历到__setup_end凡是obs_kernel_param中early字段为真的条目就调用其setup_func。至此所有依赖早期命令行参数的服务均已完成配置。报告 NX 位配置x86_report_nxparse_early_param之后的调用是x86_report_nx定义于arch/x86/mm/setup_nx.c它只负责打印 NX 位相关的信息。需要注意的是x86_report_nx并没有紧跟在x86_configure_nx之后调用而是等parse_early_param完成后再调用。原因很简单——内核支持noexec命令行参数noexec [X86] On X86-32 available only on PAE configured kernels. noexecon: enable non-executable mappings (default) noexecoff: disable non-executable mappings也就是说NX 位最终是否启用取决于用户在命令行传入的noexecon/off因此必须先解析早期参数再报告最终状态。启动时的 dmesg 中可以看到类似输出图注内核启动日志中NX (Execute Disable) protection: active一行表示基于 NX 位的不可执行映射保护已在初始化阶段生效。为 setup_data 预留内存memblock_x86_reserve_range_setup_data下一个调用是memblock_x86_reserve_range_setup_data();该函数同样定义在arch/x86/kernel/setup.c中。它负责为setup_data引导加载程序通过setup_data链表传递给内核的附加数据更多背景见 linux-initialization-5.md重新映射内存并在 memblock 分配器中为setup_data预留内存块。setup_data链表中的每一项都指向物理内存中的一段数据内核必须在后续内存管理接管前保护这些区域不被覆盖。关于ioremap与memblock的详细机制可参阅 linux-mm-1.md 与 linux-mm-2.md。ACPI/MPS 检查acpi_mps_check接下来是一个条件判断if (acpi_mps_check()) { #ifdef CONFIG_X86_LOCAL_APIC disable_apic 1; #endif setup_clear_cpu_cap(X86_FEATURE_APIC); }acpi_mps_check定义在arch/x86/kernel/acpi/boot.c其行为依赖CONFIG_X86_LOCAL_APIC与CONFIG_X86_MPPARSE两个配置项int __init acpi_mps_check(void) { #if defined(CONFIG_X86_LOCAL_APIC) !defined(CONFIG_X86_MPPARSE) /* mptable code is not built-in*/ if (acpi_disabled || acpi_noirq) { printk(KERN_WARNING MPS support code is not built-in.\n Using acpioff or acpinoirq or pcinoacpi may have problem\n); return 1; } #endif return 0; }它检查内核是否内建了 MultiProcessor SpecificationMP 规范表支持。当CONFIG_X86_LOCAL_APIC被启用但CONFIG_X86_MPPARSE未启用时即 MP 表代码没有编入内核如果用户又在命令行传入了acpioff、acpinoirq或pcinoacpi之一该函数就会打印警告信息并返回1提示关闭 ACPI/PCI 中断可能与 MP 表冲突。当acpi_mps_check返回1时内核会通过disable_apic 1禁用本地 APIC在CONFIG_X86_LOCAL_APIC开启的情况下并用setup_clear_cpu_cap(X86_FEATURE_APIC)清除当前 CPU 能力位图中的X86_FEATURE_APIC位。关于 CPU 能力位图CPU masks可参阅 linux-cpu-2.md。早期 PCI dumppciearlydump下一个步骤与 PCI 设备早期转储有关#ifdef CONFIG_PCI if (pci_early_dump_regs) early_dump_pci_devices(); #endifpci_early_dump_regs变量定义在arch/x86/pci/common.c其取值取决于内核命令行参数pciearlydump。该参数通过以下方式登记early_param(pci, pci_setup);定义于drivers/pci/pci.c。pci_setup函数取出pci后面的字符串并逐一分析其中会调用pcibios_setup。这个函数在drivers/pci/pci.c中以__weak弱符号形式定义各架构可以定义同名的强符号覆盖它。例如x86_64架构版本位于arch/x86/pci/common.cchar *__init pcibios_setup(char *str) { ... } else if (!strcmp(str, earlydump)) { pci_early_dump_regs 1; return NULL; } ... }于是当CONFIG_PCI开启且命令行传入pciearlydump时内核会调用arch/x86/pci/early.c中的early_dump_pci_devices。该函数首先检查noearlyPCI 参数if (!early_pci_allowed()) return;若传入了pcinoearly则直接返回。随后由于每个 PCI domain 最多可容纳 256 条总线、每条总线最多挂 32 个设备、每个设备最多 8 个功能function内核以三重循环遍历并读取 PCI 配置空间for (bus 0; bus 256; bus) { for (slot 0; slot 32; slot) { for (func 0; func 8; func) { ... ... ... } } }循环内通过read_pci_config读取每个设备的配置寄存器并打印。本文不再深入 PCI 细节更完整的内容将放在专门的Drivers/PCI章节。收尾内存解析e820 相关函数组在early_dump_pci_devices之后是一组与可用内存和 e820 内存图在 linux-bootstrap-2.md 的“内核设置第一步”中通过 BIOS0xE820中断收集相关的函数/* update the e820_saved too */ e820_reserve_setup_data(); finish_e820_parsing(); ... ... ... e820_add_kernel_range(); trim_bios_range(void); max_pfn e820_end_of_ram_pfn(); early_reserve_e820_mpc_new();e820_reserve_setup_data第一个函数e820_reserve_setup_data与前面看到的memblock_x86_reserve_range_setup_data做的事情几乎一样但区别在于它还会调用e820_update_range把新的区域以E820_RESERVED_KERN类型加入e820map从而同时更新 e820 内存图保证setup_data占用的物理区域在 e820 层面也被标记为内核保留。finish_e820_parsingfinish_e820_parsing通过sanitize_e820_map函数对e820map做“清洗”合并相邻的同类型区域、去除无效条目、确保区域有序等得到一份干净、规整的物理内存布局。e820_add_kernel_rangee820_add_kernel_range取得内核镜像的物理起始与结束地址u64 start __pa_symbol(_text); u64 size __pa_symbol(_end) - start;然后检查.text、.data、.bss这些段在e820map中都被标记为E820RAM可用 RAM如果不是则打印警告。trim_bios_rangetrim_bios_range文档中写作trm_bios_range对应实际函数trim_bios_range将 e820 图中前 4096 字节更新为E820_RESERVEDBIOS 数据区不可被内核当作普通内存使用并再次调用sanitize_e820_map重新清洗。e820_end_of_ram_pfn计算最大页帧号接下来内核需要知道物理内存的最大页帧号Page Frame NumberPFN。每个内存页都有一个唯一编号e820_end_of_ram_pfn通过调用e820_end_pfn得到最大值unsigned long __init e820_end_of_ram_pfn(void) { return e820_end_pfn(MAX_ARCH_PFN); }其中e820_end_pfn接受的MAX_ARCH_PFN是当前架构允许的最大页帧号在x86_64上是0x400000000。e820_end_pfn遍历所有 e820 槽位只统计类型为E820_RAM或E820_PRAM的条目只有这两类才参与页帧号计算对每个条目取起始与结束地址换算成页帧号并做上限检查for (i 0; i e820.nr_map; i) { struct e820entry *ei e820.map[i]; unsigned long start_pfn; unsigned long end_pfn; if (ei-type ! E820_RAM ei-type ! E820_PRAM) continue; start_pfn ei-addr PAGE_SHIFT; end_pfn (ei-addr ei-size) PAGE_SHIFT; if (start_pfn limit_pfn) continue; if (end_pfn limit_pfn) { last_pfn limit_pfn; break; } if (end_pfn last_pfn) last_pfn end_pfn; }循环结束后再把结果限制在架构上限之内并打印if (last_pfn max_arch_pfn) last_pfn max_arch_pfn; printk(KERN_INFO e820: last_pfn %#lx max_arch_pfn %#lx\n, last_pfn, max_arch_pfn); return last_pfn;这段输出可以在 dmesg 中看到... [ 0.000000] e820: last_pfn 0x41f000 max_arch_pfn 0x400000000 ...max_low_pfn 与 high_memory得到max_pfn整个物理内存的最大页帧号之后内核继续计算max_low_pfn——即“低端内存”第一个 4 GB 以下中的最大页帧号if (max_pfn (1UL(32 - PAGE_SHIFT))) max_low_pfn e820_end_of_low_ram_pfn(); else max_low_pfn max_pfn; high_memory (void *)__va(max_pfn * PAGE_SIZE - 1) 1;如果安装的内存超过 4 GBmax_low_pfn就是e820_end_of_low_ram_pfn的结果——该函数与e820_end_of_ram_pfn逻辑相同只是把上限限制在 4 GB 处否则max_low_pfn与max_pfn相同。随后用__va宏把物理地址转换为虚拟地址计算high_memory。high_memory定义了直接映射区direct map内存的上界即线性映射所能覆盖的虚拟地址上限。DMI 扫描dmi_scan_machine 与 dmi_memdev_walk处理完内存区域与 e820 槽位之后内核开始收集计算机硬件信息使用的是 Desktop Management InterfaceDMI对应调用dmi_scan_machine(); dmi_memdev_walk();dmi_scan_machinedmi_scan_machine定义在drivers/firmware/dmi_scan.c它遍历 System Management BIOSSMBIOS结构并提取信息。访问 SMBIOS 表有两条途径从 EFI 的配置表取得表指针或者在物理内存0xF00000xFFFFF共0x10000字节范围内扫描。内核优先采用扫描物理内存的方式void __init dmi_scan_machine(void) { char __iomem *p, *q; char buf[32]; ... ... ... p dmi_early_remap(0xF0000, 0x10000); if (p NULL) goto error;这里用dmi_early_remap即early_ioremap的别名把0xF0000起始、0x10000字节大小的物理区域映射为虚拟地址然后在其中寻找_SM_魔数串。根据 SMBIOS 规范_SM_字符串必然位于000F0000h与000FFFFFh之间。代码以 16 字节为步长迭代扫描memset(buf, 0, 16); for (q p; q p 0x10000; q 16) { memcpy_fromio(buf 16, q, 16); if (!dmi_smbios3_present(buf) || !dmi_present(buf)) { dmi_available 1; dmi_early_unmap(p, 0x10000); goto out; } memcpy(buf, buf 16, 16); }memcpy_fromio等价于memcpy每次拷贝 16 字节到buf然后调用dmi_smbios3_present与dmi_present检查确认缓冲区前 4 字节是_SM_串、解析 SMBIOS 版本并取出_DMI_相关的属性如 DMI 结构表长度、表地址等。一旦命中就置dmi_available 1解除早期映射并跳出循环。扫描成功后dmesg 中会出现类似信息[ 0.000000] SMBIOS 2.7 present. [ 0.000000] DMI: Gigabyte Technology Co., Ltd. Z97X-UD5H-BK/Z97X-UD5H-BK, BIOS F6 06/17/2014函数末尾用dmi_early_unmap(p, 0x10000)解除之前建立的临时映射避免早期映射区域被长期占用。dmi_memdev_walk第二个函数dmi_memdev_walk负责遍历内存设备memory devicesvoid __init dmi_memdev_walk(void) { if (!dmi_available) return; 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); } }它先确认前一步的dmi_available为真然后用dmi_walk_early(count_mem_devices)统计内存设备数量再调用dmi_alloc为dmi_memdev数组分配空间最后用dmi_walk_early(save_mem_devices)真正保存每个内存设备的信息。dmi_alloc的定义如下#ifdef CONFIG_DMI RESERVE_BRK(dmi_alloc, 65536); #endifRESERVE_BRK定义在arch/x86/include/asm/setup.h其作用是在brk段中按给定大小这里是 64 KB预留空间供早期阶段尚未启用正式分配器时使用更多见下文 brk 部分。setup_arch 中的其他中间调用在 DMI 扫描前后setup_arch还会执行一批与资源注册和平台探测相关的调用init_hypervisor_platform(); x86_init.resources.probe_roms(); insert_resource(iomem_resource, code_resource); insert_resource(iomem_resource, data_resource); insert_resource(iomem_resource, bss_resource); early_gart_iommu_check();init_hypervisor_platform()探测并初始化当前运行的 hypervisor虚拟机监控器平台x86_init.resources.probe_roms()探测系统 ROM 区域三次insert_resource把内核的code_resource代码段、data_resource数据段、bss_resourceBSS 段作为子资源插入全局iomem_resourceI/O 内存资源树使/proc/iomem能够展示内核镜像占用的物理地址范围early_gart_iommu_check()早期检查 GARTGraphics Aperture Remapping TableIOMMU 相关配置。SMP 配置find_smp_config 与 smp_scan_config下一步是解析 SMP对称多处理配置通过调用find_smp_config完成static inline void find_smp_config(void) { x86_init.mpparse.find_smp_config(); }x86_init.mpparse.find_smp_config实际指向arch/x86/kernel/mpparse.c中的default_find_smp_config。该函数在几个固定内存区域中扫描 SMP 配置一旦找到即返回if (smp_scan_config(0x0, 0x400) || smp_scan_config(639 * 0x400, 0x400) || smp_scan_config(0xF0000, 0x10000)) return;扫描区域分别是物理地址0x0起 1 KB实模式 IVT/BDA 区域、639 * 0x400即0x9FC00扩展 BIOS 数据区附近起 1 KB以及0xF0000起的 64 KB系统 BIOS 区域。smp_scan_config首先定义两个关键变量unsigned int *bp phys_to_virt(base); struct mpf_intel *mpf;前者是把待扫描物理地址转换为虚拟地址后者是指向mpf_intel结构的指针。mpf_intel表示 MP 浮点指针结构MP Floating Pointer Structurestruct 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; };根据 MP 规范文档系统 BIOS 的核心职责之一就是构造 MP 浮点指针结构和 MP 配置表操作系统必须能够访问这些多处理器配置信息。mpf_intel中的physptr字段第二个成员保存的就是 MP 配置表的物理地址。smp_scan_config在给定内存范围内循环查找 MP 浮点指针结构检查当前 4 字节是否为SMP魔数SMP_MAGIC_IDENT、结构长度是否为 1、16 字节校验和是否正确以及mpf-specification是否为 1 或 4规范只允许这两个取值while (length 0) { if ((*bp SMP_MAGIC_IDENT) (mpf-length 1) !mpf_checksum((unsigned char *)bp, 16) ((mpf-specification 1) || (mpf-specification 4))) { mem virt_to_phys(mpf); memblock_reserve(mem, sizeof(*mpf)); if (mpf-physptr) smp_reserve_memory(mpf); } }搜索成功后用memblock_reserve在 memblock 中为 MP 浮点指针结构预留内存并通过smp_reserve_memory进一步保留physptr指向的 MP 配置表物理内存。有关 MP 规范的完整定义可参见 Intel 的 MultiProcessor Specification 文档更深入的 SMP 内容将在专门的章节中展开。早期页表缓冲区early_alloc_pgt_bufsetup_arch下一步调用early_alloc_pgt_buf为早期阶段分配页表缓冲区。这个缓冲区将被放置在brk区域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在当前 Linux 内核 4.0 中为6 * PAGE_SIZE6 个页约 24 KB调用extend_brk(size, align)扩展brk区域——从名字即可看出它把brk区向后延伸参数分别为大小与对齐值这里是PAGE_SIZE用__pa把新brk区的虚拟地址转换为物理地址base计算出页表缓冲区的起始页帧号pgt_buf_start、当前可用终点pgt_buf_end初始等于起点以及上限pgt_buf_top。brk 区域在链接脚本中的位置在 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 .; }__brk_base与__brk_limit标记了brk区域的起止其中预置了 64 KB 的对齐余量空间而*(.brk_reservation)收集所有通过RESERVE_BRK宏声明的早期保留区域如前文 DMI 部分用到的RESERVE_BRK(dmi_alloc, 65536)。可以用readelf工具查看内核 ELF 文件中.brk段的位置图注readelf -S输出中可见.bss与.brk段及其地址属性.brk段紧随 BSS 之后是早期内存分配的“临时仓库”。reserve_brk预留并关闭 brk拿到页表缓冲区之后内核用reserve_brk为brk区域在 memblock 中预留内存块static void __init reserve_brk(void) { if (_brk_end _brk_start) memblock_reserve(__pa_symbol(_brk_start), _brk_end - _brk_start); _brk_start 0; }注意函数末尾把_brk_start清零这意味着从此刻起内核不再通过brk分配内存所有早期临时分配都已定型后续内存分配将交给正式的内存管理机制。cleanup_highmap清理内核映射中的越界区域预留brk之后还需要用cleanup_highmap解除内核映射中越界的内存区域。回顾一下内核映射的范围是__START_KERNEL_map到_end - _text由level2_kernel_pgt页中间目录映射内核的_text、data与bss。cleanup_highmap一开始就定义这些边界unsigned long vaddr __START_KERNEL_map; unsigned long end roundup((unsigned long)_end, PMD_SIZE) - 1; pmd_t *pmd level2_kernel_pgt; pmd_t *last_pmd pmd PTRS_PER_PMD;然后遍历所有内核页中间目录PMD条目把不在_text与end之间的条目清零for (; pmd last_pmd; pmd, vaddr PMD_SIZE) { if (pmd_none(*pmd)) continue; if (vaddr (unsigned long) _text || vaddr end) set_pmd(pmd, __pmd(0)); }pmd_none(*pmd)跳过空条目对地址落在内核镜像范围之外的条目用set_pmd(pmd, __pmd(0))将其置为空。这样既保证内核镜像本身_text/data/bss被正确映射又避免在高端虚拟地址区域留下多余的映射防止 speculative 访问穿越到未映射区域。设置 memblock 分配上限并填充内存信息接着内核用memblock_set_current_limit设置 memblock 的分配上限该值为ISA_END_ADDRESS即0x1000001 MB意味着后续 memblock 分配不会越过这个地址早期低端内存分配必须先满足 ISA 区域的需求。然后调用memblock_x86_fill根据清洗后的 e820 图填充 memblock 的 memory/reserved 区域信息。该函数的执行结果可以在内核初始化阶段的输出中看到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其中memory数组列出物理内存的可用区域第一段从0x1000到0x9efff第二段从 1 MB 到0xbffdffff第三段在 4 GB 之上reserved数组则列出已被内核预留的区域。这些信息正是后续伙伴系统buddy allocator建立的基础。关于 memblock 数据结构的完整介绍见 linux-mm-1.md。memblock_x86_fill 之后的收尾函数memblock_x86_fill之后还有一批函数完成最后的早期准备early_reserve_e820_mpc_new在e820map中为 MultiProcessor Specification 表分配额外的槽位因为 MP 表内存可能未被 BIOS 标记为保留需内核自己补上reserve_real_mode为实模式跳板trampoline用于重启、休眠唤醒等场景在0x0到 1 MB 的低端内存中预留空间trim_platform_memory_ranges裁剪某些特定的内存区域从0x20050000、0x20110000等地址开始。这些区域必须被排除因为 Sandy Bridge 等平台对这些区域的处理存在问题trim_low_memory_range在 memblock 中预留第一个 4 KB 物理页第 0 页通常被标记保留防止空指针陷阱init_mem_mapping重建直接内存映射并在PAGE_OFFSET处建立物理内存的直接映射early_trap_pf_init设置#PF页错误处理函数详细内容将在中断章节展开setup_real_mode设置实模式跳板代码为后续切换到实模式做准备。总结本文内核初始化系列的第六部分继续深入setup_arch函数覆盖了从x86_configure_nx之后的架构相关初始化流程早期命令行参数解析parse_early_param/early_param宏、NX 位报告、setup_data内存预留、ACPI/MPS 检查、PCI 早期转储、e820 内存图收尾与max_pfn/max_low_pfn/high_memory的计算、DMI/SMBIOS 硬件信息采集、SMP 配置扫描以及 brk 页表缓冲区分配与 memblock 填充。setup_arch仍然十分庞大本文尚未覆盖全部函数如early_gart_iommu_check、MTRR 初始化等。内核初始化系列还包括linux-initialization-5.mdsetup_arch前段initrd 预留、早期陷阱与 IDT 初始化、x86_configure_nx等linux-initialization-7.mdsetup_arch的收尾部分linux-initialization-8.md调度器初始化linux-initialization-10.md初始化过程的终点。相关内存管理机制可继续阅读 linux-mm-1.mdmemblock 分配器与 linux-mm-2.md固定映射地址与 ioremap。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐使用 Glasskube 在 Kubernetes 上安装与运维 Quickwit完整指南使用 Glasskube 在 Kubernetes 上安装与运维 Quickwit完整指南 Quickwit 是一款面向可观测性场景日志、追踪的云原生搜索文档教程操作系统调度器初始化全流程解析Linux 内核初始化第八部分linux-insides-zh调度器初始化全流程解析Linux 内核初始化第八部分linux insides zh 本篇技术指南以 linux insides zh 仓库 InitiaLinux 内核初始化七setup_arch 收尾、initrd 重定位与 start_kernel 通用初始化Linux 内核初始化七setup_arch 收尾、initrd 重定位与 start_kernel 通用初始化 导读 本文是「Linux 内核初始化」系上一篇如何为 HiDream-O1-Dev-FP16 创建自定义工作流ComfyUI节点深度定制指南 下一篇如何使用StickyLand让Jupyter Notebook突破线性展示的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表