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 架构下的典型执行序列:
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 = <ð_dma_region>;
dma-coherent;
};
};
实践建议: 在编写设备驱动时,优先使用
dma_alloc_coherent 或 dma_alloc_attrs 分配 DMA 缓冲区。这些 API 会自动从正确的 DMA zone 分配内存,并处理 cache 一致性。只有在需要预留固定物理地址(如硬件要求特定地址)时,才考虑在 setup_arch 阶段通过 memblock 预留。
3.6 早期内存布局对 DMA 初始化的影响
setup_arch 建立的早期内存布局直接影响后续 DMA 子系统的初始化行为:
- DMA zone 大小:由
arm64_dma_phys_limit决定,该值在setup_arch中根据 DTB 的dma-ranges或默认架构限制计算得出。 - memblock 保留区域:所有在
setup_arch中通过memblock_reserve标记的区域,在伙伴系统初始化时会被自动排除,确保 DMA 缓冲区不会被普通内存分配占用。 - IOMMU 映射基础:如果系统包含 SMMU/IOMMU,其页表基地址和映射范围也在早期内存布局中确定,为后续
iommu_dma_ops的注册提供物理内存支持。
下图展示了早期内存布局与 DMA 初始化之间的依赖关系:
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 预留问题。