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)在此处开始执行,并立即准备切换到保护模式。

切换保护模式的核心步骤:

  1. 加载GDT(全局描述符表):在内存中构建包含代码段、数据段描述符的GDT,并执行 lgdt 指令。
  2. 设置CR0保护模式位:将CR0寄存器的PE位(bit 0)置1。
  3. 远跳转刷新流水线:通过 ljmp 指令跳转到32位代码段,CPU进入保护模式。
  4. 初始化段寄存器:将DS、ES、FS、GS、SS加载为平坦模型数据段选择子。
实模式到保护模式切换流程 实模式 → 保护模式切换流程 BIOS/UEFI 加载引导扇区 构建GDT并加载 设置CR0.PE=1 ljmp 远跳转刷新流水线 初始化段寄存器(平坦模型) 进入32位保护模式

2.2 内核解压缩与早期页表建立

保护模式下的引导代码(arch/x86/boot/compressed/head_64.S)负责将内核镜像解压缩到物理内存高位区域。解压缩完成后,立即建立恒等映射(identity mapping)和内核虚拟地址映射的早期页表。

DMA开发者提示: 早期页表使用2MB/1GB大页,意味着此时物理内存的连续性极高。DMA控制器通常要求缓冲区物理连续,这一阶段的页表粒度恰好反映了硬件对连续内存的天然偏好。后续 memblock 分配器会尽量保留大块连续区域供DMA使用。

2.3 内存探测:E820与UEFI内存映射

在进入 start_kernel 之前,引导代码必须收集完整的物理内存布局。x86平台通过E820(BIOS中断0x15)或UEFI GetMemoryMap获取内存区域类型。

// arch/x86/boot/memory.c 简化逻辑
struct boot_e820_entry {
    unsigned long long addr;   // 起始物理地址
    unsigned long long size;   // 长度
    unsigned int type;         // 1=可用, 2=保留, 3=ACPI, 4=休眠, 5=损坏
};

int detect_memory_e820(void) {
    int count = 0;
    do {
        e820_entry[count].addr = ...;   // 通过int 0x15, eax=0xE820获取
        e820_entry[count].size = ...;
        e820_entry[count].type = ...;
        count++;
    } while (count < E820_MAX_ENTRIES && !done);
    return count;
}

E820返回的内存区域类型直接影响DMA可用性:

  • 类型1(可用):普通DMA缓冲区首选区域,通常位于16MB以下(ISA DMA限制)或4GB以下(32位DMA)。
  • 类型2(保留):包含ACPI表、IOAPIC、本地APIC等,DMA不可用。
  • 类型3(ACPI回收):内核启动后可回收,但DMA需谨慎。
  • 类型5(损坏):硬件标记的坏内存区域,DMA必须避开。
E820内存布局示例 典型x86 E820内存布局(4GB地址空间) 可用内存(类型1)— 0 ~ 640KB 保留 可用 640KB~1MB ACPI/保留 可用 1MB~4GB(DMA主要区域) 保留/MMIO ISA DMA限制:<16MB 32位DMA限制:<4GB SWIOTLB回退:>4GB DMA缓冲区优先从绿色区域分配

2.4 引导内存分配器:bootmem与memblock

在 start_kernel 早期,伙伴系统尚未就绪,内核使用引导内存分配器管理物理内存。早期内核使用 bootmem(位图分配器),现代内核已全面迁移到 memblock。

映射类型 物理地址范围 虚拟地址范围 页表级别 用途
恒等映射 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初始化关键点: 在 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初始化前最重要的函数,它完成:

  1. 解析 mem、crashkernel、swiotlb 等内核参数。
  2. 调用 memblock_reserve() 保护固件、ACPI、IOMMU等区域。
  3. 初始化 dma_ops 指针(指向 dma_direct_ops 或 dma_swiotlb_ops)。
  4. 设置 zone_dma 和 zone_dma32 的物理边界。
start_kernel 内存管理初始化流程 start_kernel 内存管理初始化流程 start_kernel() 入口 setup_arch() 解析内核参数 memblock_reserve() 初始化dma_ops mm_init() 伙伴系统 rest_init() → kernel_init

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 可查看当前物理内存分配状态。