3. 内核启动第二阶段:setup_arch 与早期内存布局

当内核解压完成并进入 start_kernel 后,第一个关键架构初始化函数是 setup_arch。该函数负责建立处理器与内存子系统之间的基础通信桥梁,为后续设备驱动加载时的 DMA 初始化提供物理内存视图。本节将深入剖析 setup_arch 的执行流程、早期内存布局的建立机制,以及它们如何影响 DMA 地址空间的划分。

3.1 setup_arch 的核心职责

setup_arch 是架构相关的初始化入口,在 ARM64 架构中通常位于 arch/arm64/kernel/setup.c。其主要任务包括:

  • 解析引导参数:从 DTB(设备树)或 ACPI 表中提取内存节点、保留内存区域、DMA 范围等信息。
  • 建立早期页表:为内核线性映射区域(如 vmalloc 区域、固定映射区域)创建一级/二级页表。
  • 初始化内存节点:调用 early_fixmap_init、early_ioremap_init 等函数,建立早期 I/O 映射。
  • 注册物理内存区域:将 DTB 中描述的物理内存块注册到 memblock 分配器中。
  • 保留关键内存区域:为内核代码段、数据段、初始页表、DMA 预留缓冲区等标记保留区域。
关键点: setup_arch 执行完毕后,内核拥有了一个“静态”的物理内存视图,但尚未启动伙伴系统。所有内存分配请求都通过 memblock 分配器完成,这是 DMA 初始化能够安全预留缓冲区的唯一窗口期。

3.2 早期内存布局的建立流程

早期内存布局的建立遵循“先收集、后映射、再保留”的三步策略。以下流程图展示了 ARM64 架构下的典型执行序列:

setup_arch 早期内存布局建立流程 setup_arch 早期内存布局建立流程 1. 解析 DTB / ACPI 内存节点 2. 创建早期内核页表 3. 注册物理内存到 memblock 4. 保留内核/DMA/页表区域 5. 建立 fixmap / ioremap 早期内存布局就绪 说明:步骤 4 中保留的区域包括 DMA 缓冲区、crashkernel、initrd 等,这些区域在后续驱动加载时不可被伙伴系统分配。

3.3 memblock 分配器:DMA 预留的唯一窗口

在伙伴系统(Buddy System)激活之前,memblock 是内核中唯一的内存分配器。它维护两个核心数据结构:

  • memblock.memory:记录所有可用的物理内存区域(类型为 struct memblock_region)。
  • memblock.reserved:记录所有已被保留的内存区域,这些区域将不会进入伙伴系统。

设备驱动在早期初始化阶段(如 arch_initcall 或 subsys_initcall)可以通过以下 API 预留 DMA 缓冲区:

// 预留连续的物理内存区域,用于 DMA 一致性映射
void *memblock_alloc(phys_addr_t size, phys_addr_t align);

// 在指定物理地址范围内预留内存
phys_addr_t memblock_phys_alloc_range(phys_addr_t size,
                                      phys_addr_t align,
                                      phys_addr_t start,
                                      phys_addr_t end);

// 标记指定物理地址区间为保留
int memblock_reserve(phys_addr_t base, phys_addr_t size);
重要约束: memblock 分配器不支持释放操作。一旦调用 memblock_reserve 或 memblock_alloc,该区域将永久保留,直到系统重启。因此,DMA 缓冲区的预留必须在明确知道硬件需求的前提下进行,避免浪费宝贵的低端内存。

3.4 早期内存布局与 DMA 地址空间划分

在 setup_arch 完成后,典型的 ARM64 早期内存布局如下表所示。DMA 相关的区域(如 DMA zone、DMA32 zone)的边界在此阶段被确定。

地址范围(物理) 区域名称 用途 DMA 相关性
0x0000_0000 – 0x00FF_FFFF DMA zone (ZONE_DMA) 16MB 低端内存,用于 ISA/DMA 设备 直接相关:驱动必须从此区域分配 DMA 缓冲区
0x0100_0000 – 0x3FFF_FFFF DMA32 zone (ZONE_DMA32) 1GB 内存,支持 32-bit 地址设备 相关:大多数现代 DMA 设备使用此区域
0x4000_0000 – 0x7FFF_FFFF Normal zone (ZONE_NORMAL) 常规线性映射内存 间接相关:通过 IOMMU/SMMU 可进行 DMA
0x8000_0000 – 0xFFFF_FFFF HighMem / vmalloc 高端内存或 vmalloc 区域 不直接支持:需要特殊映射才能用于 DMA

3.5 设备树中的 DMA 描述与解析

设备驱动在早期初始化阶段需要从 DTB 中获取 DMA 相关信息。setup_arch 调用 early_init_dt_scan 系列函数解析以下关键属性:

  • dma-ranges:描述设备可访问的 DMA 地址范围,用于设置 struct device->dma_pfn_offset。
  • dma-coherent:标记设备是否支持硬件一致性(无需软件 cache 同步)。
  • memory-region:指向预留内存区域(通过 reserved-memory 节点定义),用于驱动专用 DMA 池。

以下是一个典型的设备树片段,展示了如何为 DMA 设备预留专用缓冲区:

/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        /* 为网卡预留 64MB DMA 缓冲区 */
        eth_dma_region: eth-dma@80000000 {
            compatible = "shared-dma-pool";
            reg = <0x0 0x80000000 0x0 0x04000000>;
            no-map;  /* 不映射到内核线性空间 */
        };
    };

    ethernet@1c00000 {
        compatible = "vendor,eth-controller";
        reg = <0x0 0x1c00000 0x0 0x1000>;
        memory-region = <&eth_dma_region>;
        dma-coherent;
    };
};
实践建议: 在编写设备驱动时,优先使用 dma_alloc_coherent 或 dma_alloc_attrs 分配 DMA 缓冲区。这些 API 会自动从正确的 DMA zone 分配内存,并处理 cache 一致性。只有在需要预留固定物理地址(如硬件要求特定地址)时,才考虑在 setup_arch 阶段通过 memblock 预留。

3.6 早期内存布局对 DMA 初始化的影响

setup_arch 建立的早期内存布局直接影响后续 DMA 子系统的初始化行为:

  1. DMA zone 大小:由 arm64_dma_phys_limit 决定,该值在 setup_arch 中根据 DTB 的 dma-ranges 或默认架构限制计算得出。
  2. memblock 保留区域:所有在 setup_arch 中通过 memblock_reserve 标记的区域,在伙伴系统初始化时会被自动排除,确保 DMA 缓冲区不会被普通内存分配占用。
  3. IOMMU 映射基础:如果系统包含 SMMU/IOMMU,其页表基地址和映射范围也在早期内存布局中确定,为后续 iommu_dma_ops 的注册提供物理内存支持。

下图展示了早期内存布局与 DMA 初始化之间的依赖关系:

早期内存布局与 DMA 初始化依赖关系 早期内存布局与 DMA 初始化依赖关系 setup_arch 解析 DTB / 建立页表 memblock 布局 memory + reserved 区域 DMA 子系统初始化 dma_ops / IOMMU 注册 依赖关系说明 1. DMA zone 边界由 setup_arch 根据 dma-ranges 确定 2. memblock.reserved 中的区域在伙伴系统初始化时被排除 3. IOMMU 页表分配依赖 memblock_alloc 获取物理内存 4. 驱动 dma_alloc_coherent 最终从 ZONE_DMA/DMA32 分配

3.7 常见问题与调试方法

在早期内存布局阶段,DMA 初始化可能遇到以下典型问题:

问题现象 根本原因 调试方法
dma_alloc_coherent 返回 NULL ZONE_DMA 空间耗尽,或预留区域被其他驱动占用 检查 /proc/zoneinfo 中 DMA zone 的 present 和 free 值
设备 DMA 访问触发总线错误 DMA 缓冲区物理地址超出设备地址范围 验证 DTB 中 dma-ranges 属性是否与设备能力匹配
系统启动时 panic:memblock_alloc 失败 预留区域重叠或 memblock 空间不足 添加 memblock=debug 内核参数,查看 memblock 分配日志
调试技巧: 在 setup_arch 完成后,可以通过 print_memory_layout() 或 memblock_dump_all() 打印完整的 memblock 布局。在 ARM64 中,添加 earlycon 和 keep_bootcon 内核参数可以在非常早期的阶段输出调试信息,帮助定位 DMA 预留问题。