2. Linux内核启动第一阶段:从BIOS/UEFI到start_kernel
设备驱动加载前的内存管理基础,始于系统上电后内核正式接管硬件之前。第一阶段的核心任务是从固件(BIOS/UEFI)手中接过控制权,完成早期页表建立、内存区域探测和临时内存分配器的初始化,最终跳转到C语言入口 start_kernel。这一阶段的内存布局直接决定了后续DMA缓冲区的可用物理地址范围和连续性。
2.1 固件交接:实模式与保护模式切换
CPU上电后默认运行在实模式(16位),地址空间仅1MB,且无内存保护。BIOS/UEFI完成硬件自检(POST)后,将启动介质第一个扇区(MBR/GPT引导代码)加载到物理地址 0x7C00,并跳转执行。
关键地址约定: 实模式下
0x7C00 是引导扇区加载地址;0x0000:0x7C00 对应物理地址 0x07C00。内核引导程序(arch/x86/boot/header.S)在此处开始执行,并立即准备切换到保护模式。
切换保护模式的核心步骤:
- 加载GDT(全局描述符表):在内存中构建包含代码段、数据段描述符的GDT,并执行
lgdt指令。 - 设置CR0保护模式位:将CR0寄存器的PE位(bit 0)置1。
- 远跳转刷新流水线:通过
ljmp指令跳转到32位代码段,CPU进入保护模式。 - 初始化段寄存器:将DS、ES、FS、GS、SS加载为平坦模型数据段选择子。
2.2 内核解压缩与早期页表建立
保护模式下的引导代码(arch/x86/boot/compressed/head_64.S)负责将内核镜像解压缩到物理内存高位区域。解压缩完成后,立即建立恒等映射(identity mapping)和内核虚拟地址映射的早期页表。
| 映射类型 | 物理地址范围 | 虚拟地址范围 | 页表级别 | 用途 |
|---|---|---|---|---|
| 恒等映射 | 0 ~ 内核末尾 | 0 ~ 内核末尾 | 2MB大页(PMD) | 解压缩后仍能访问低端物理内存 |
| 内核虚拟映射 | 内核加载基址 | __START_KERNEL_map(0xFFFFFFFF80000000) | 1GB大页(PUD) | 内核代码段/数据段运行 |
| 临时堆栈映射 | boot_params 所在页 | 固定虚拟地址 | 4KB页(PTE) | 早期C函数调用栈 |
| 特性 | bootmem(旧) | memblock(新) |
|---|---|---|
| 数据结构 | 位图(bitmap) | 区域链表(region array) |
| 分配粒度 | 最小1页(4KB) | 任意大小(对齐到cache line) |
| 释放支持 | 不支持释放 | 支持释放和重新分配 |
| 内存类型跟踪 | 无 | 支持MEMBLOCK_NONE/MEMBLOCK_HOTPLUG |
| DMA区域预留 | 需手动预留 | 通过memblock_reserve()自动预留 |
| 性能 | 线性扫描位图,O(n) | 链表操作,O(log n)(排序插入) |
DMA初始化关键点: 在
•
•
• 设备固件DMA缓冲区(如GPU的VRAM映射)
这些预留必须在伙伴系统初始化之前完成,否则后续DMA分配可能失败。
memblock 阶段,内核会调用 memblock_reserve() 为DMA操作预留关键区域,包括:•
swiotlb 缓冲区(如果启用)•
crashkernel 内存(kdump)• 设备固件DMA缓冲区(如GPU的VRAM映射)
这些预留必须在伙伴系统初始化之前完成,否则后续DMA分配可能失败。
2.5 进入start_kernel:内存管理接力
当引导代码完成页表建立、内存探测和 memblock 初始化后,通过 jmp *%rax 跳转到 start_kernel(位于 init/main.c)。此时系统状态:
- CPU模式:长模式(x86_64)或保护模式(x86_32),分页启用。
- 页表:恒等映射 + 内核虚拟映射,覆盖全部物理内存。
- 内存分配器:
memblock已就绪,可分配/预留物理页。 - 内存布局:E820/UEFI映射已存储到
boot_params.e820_table。 - DMA预备:
swiotlb所需物理地址范围已计算,但尚未分配。
// init/main.c 简化入口
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
// ... 早期CPU初始化 ...
setup_arch(&command_line); // 架构相关初始化,包括memblock完善
mm_init(); // 伙伴系统初始化,接管memblock
// ... 中断、定时器、调度器 ...
rest_init(); // 创建kernel_init线程
}
setup_arch() 是DMA初始化前最重要的函数,它完成:
- 解析
mem、crashkernel、swiotlb等内核参数。 - 调用
memblock_reserve()保护固件、ACPI、IOMMU等区域。 - 初始化
dma_ops指针(指向dma_direct_ops或dma_swiotlb_ops)。 - 设置
zone_dma和zone_dma32的物理边界。
2.6 第一阶段对DMA初始化的影响
从BIOS/UEFI到 start_kernel 的启动过程,为后续DMA初始化奠定了三个关键基础:
| DMA需求 | 第一阶段提供的条件 | 潜在风险 |
|---|---|---|
| 物理连续缓冲区 | memblock保留大块连续区域 | 碎片化预留导致DMA分配失败 |
| 地址范围限制 | zone_dma/zone_dma32边界已设定 | 设备DMA掩码与zone不匹配 |
| IOMMU/SWIOTLB | dma_ops指针已选择 | swiotlb缓冲区大小不足 |
| 内存类型识别 | E820表标记保留/ACPI区域 | 固件标记错误导致DMA踩踏 |
实战检查点: 在调试DMA相关启动失败时,应首先验证
dmesg 中 e820 输出是否包含预期的可用区域,以及 memblock 预留是否覆盖了设备MMIO空间。使用 cat /proc/iomem 可查看当前物理内存分配状态。