计算机体系结构基础:CPU、内存、总线、中断、DMA、MMU 与驱动开发

设备驱动是操作系统与硬件之间的“翻译官”,而翻译官必须同时精通两门语言:操作系统内核的软件逻辑,以及 CPU、内存、总线、中断控制器、DMA 引擎、MMU 等硬件单元的工作机制。如果只懂 Linux 驱动框架而不理解硬件行为,写出的驱动往往在功能上“能用”,却在性能、稳定性、并发安全上漏洞百出。本章从驱动开发者的视角,拆解计算机体系结构中六个核心硬件概念,并逐一说明它们与驱动代码、中断处理、内存映射、DMA 传输之间的直接关系。

驱动视角的硬件核心链路 CPU 核心 执行驱动代码 异常/中断响应 MMU 虚拟地址→物理地址 DDR 内存 DMA 缓冲区 MMIO 映射区 系统总线 AXI / AHB / PCIe 中断控制器 GIC / APIC DMA 控制器 搬运数据,不占 CPU 外设控制器 UART / SPI / I2C Ethernet / USB 设备寄存器 控制/状态/数据 设备 FIFO / SRAM 硬件数据暂存 读写请求 TLB 命中/缺失 物理地址 中断信号 DMA 请求 总线事务 寄存器访问 DMA 搬运路径 数据流 图例 数据/控制总线 地址转换路径 中断信号 DMA 数据流 驱动开发关注:寄存器读写、中断申请、DMA 映射、MMU 一致性

上图展示了驱动开发中最核心的硬件协作链路。CPU 通过系统总线访问外设寄存器,MMU 负责将虚拟地址转换为物理地址,中断控制器将外设事件异步通知给 CPU,DMA 控制器则在不占用 CPU 的情况下完成数据搬运。驱动开发者必须理解这条链路上每一个环节的行为,才能正确配置硬件、高效处理 I/O。

2.1 CPU 与驱动:执行模型与并发基础

核心原理

CPU 是驱动代码的执行引擎。驱动中的每个函数——无论是 probe()、read()、write() 还是中断处理函数——最终都变成 CPU 上的指令流。CPU 的体系结构直接决定了驱动的编写方式:

  • 寄存器宽度:32 位(ARMv7、x86)与 64 位(ARMv8、x86-64)影响地址空间大小、数据类型长度、设备寄存器访问方式。
  • 内存模型:ARM 是弱内存序(Weak Memory Ordering),x86 是强内存序(TSO)。驱动中必须使用 wmb()、rmb()、mb() 或 dma_wmb() 等屏障指令保证访问顺序。
  • 特权级:Linux 驱动运行在内核态(EL1/ring0),可以直接访问硬件资源;用户态(EL0/ring3)必须通过系统调用陷入内核。
  • 并发模型:SMP 系统上多个 CPU 核可能同时执行驱动代码,必须使用自旋锁、互斥锁、原子操作保护共享资源。

判断方法

在驱动开发中,判断 CPU 相关问题的常用手段包括:

  1. 查看 /proc/cpuinfo 确认处理器型号、特性(如是否支持硬件浮点、虚拟化扩展)。
  2. 使用 perf 工具分析驱动热点函数,确认 CPU 是否在空转等待 I/O。
  3. 通过 top 或 mpstat 观察多核负载,判断驱动是否存在 CPU 亲和性问题。
  4. 检查内核日志中的 BUG: soft lockup 或 BUG: hard lockup,这通常意味着驱动在中断上下文或持锁状态下执行了过长时间。

案例拆解:spinlock 导致的 CPU 软锁死

某网络驱动在 ndo_start_xmit() 中持自旋锁调用 msleep(),导致 CPU 软锁死。具体过程如下:

  • 背景:双核 ARM Cortex-A9 平台,驱动需要等待 PHY 芯片完成自动协商。
  • 现象:内核日志出现 BUG: soft lockup - CPU#0 stuck for 22s!,系统网络完全中断。
  • 分析:msleep() 会触发调度,但自旋锁持有期间不允许睡眠。CPU0 在 schedule() 中自旋等待锁释放,而锁的持有者正是 CPU0 自己,形成死锁。
  • 修复:将自旋锁改为互斥锁 mutex,并确保在等待 PHY 时释放锁。
  • 复盘:驱动中任何可能睡眠的操作(msleep、kmalloc(GFP_KERNEL)、copy_to_user)都不能在自旋锁保护区内执行。
风险提示:在中断上下文(如 request_irq 注册的处理函数)中,禁止使用可能睡眠的锁(mutex、semaphore),只能使用 spin_lock 或 raw_spin_lock。否则会触发 BUG: scheduling while atomic。

2.2 内存与 MMU:地址转换与一致性管理

核心原理

MMU(Memory Management Unit)负责将 CPU 发出的虚拟地址(VA)转换为物理地址(PA)。驱动开发中涉及三类内存操作:

内存类型 地址空间 驱动使用方式 典型场景
内核线性映射区 虚拟地址直接偏移对应物理地址 kmalloc()、kzalloc() 普通数据结构、小缓冲区
vmalloc 区 非连续虚拟地址映射到分散物理页 vmalloc() 大块内存、模块加载
MMIO 映射区 外设寄存器物理地址映射到虚拟地址 ioremap() / ioremap_nocache() 访问设备控制寄存器
DMA 缓冲区 物理连续或经 IOMMU 映射 dma_alloc_coherent() / dma_map_single() 网卡收发缓冲区、USB 传输

MMU 还提供页级保护属性(可读、可写、可执行、缓存策略)。驱动通过 pgprot_t 控制页属性,例如将设备寄存器映射为 Device-nGnRnE(ARM)或 UC(x86),禁止 CPU 缓存,保证每次读写都直接访问硬件。

判断方法

排查内存相关驱动问题,可遵循以下流程:

  1. 确认虚拟地址与物理地址的转换关系:使用 /proc/iomem 查看物理地址分配,使用 /proc/vmallocinfo 查看 vmalloc 映射。
  2. 检查页表属性:通过 debugfs 中的 page_tables 节点(ARM64)或 /sys/kernel/debug/kernel_page_tables 确认映射是否带缓存。
  3. 验证一致性:如果外设和 CPU 共享数据,必须使用 dma_alloc_coherent() 分配一致性内存,或使用 dma_map_single() 配合 dma_sync_single_for_cpu() / dma_sync_single_for_device() 手动同步。
  4. 使用 CONFIG_DEBUG_PAGEALLOC 和 CONFIG_DEBUG_KMEMLEAK 检测非法访问和内存泄漏。

案例拆解:MMIO 映射未使用 ioremap 导致总线错误

某 SPI 驱动直接通过物理地址指针访问寄存器,在 ARM64 平台上触发 Unhandled fault: synchronous external abort。原因如下:

  • 背景:平台为 ARM64,外设寄存器物理地址为 0xFF040000。
  • 错误代码:volatile uint32_t *reg = (volatile uint32_t *)0xFF040000; 然后直接 *reg = value;
  • 分析:ARM64 内核中,物理地址不能直接作为虚拟地址访问。CPU 发出的地址必须经过 MMU 转换,未映射的物理地址会导致数据中止异常。
  • 修复:使用 void __iomem *base = ioremap(0xFF040000, SZ_4K); 然后通过 writel(value, base + offset) 访问。
  • 复盘:所有外设寄存器访问必须通过 ioremap 系列函数建立映射,并使用 readl/writel 等访问器,保证编译器不优化且访问顺序正确。
经验:在 ARM 平台上,readl/writel 会生成 ldr/str 指令并附带内存屏障,确保访问顺序。不要用普通 C 指针直接访问 MMIO 地址。

2.3 总线与中断:设备发现与异步通知

核心原理

总线是 CPU 与外设之间的通信通道。常见总线包括:

  • 平台总线(Platform Bus):用于内存映射设备,通过设备树(Device Tree)描述硬件信息。
  • PCIe 总线:支持枚举、配置空间、MSI/MSI-X 中断、DMA 等高级特性。
  • I2C/SPI/UART:低速串行总线,驱动通过控制器寄存器进行读写。
  • USB:树状拓扑,支持热插拔、端点通信、OTG 等。

中断是外设主动通知 CPU 的机制。中断控制器(GIC/APIC)负责收集各设备的中断请求,按优先级仲裁后发送给 CPU。驱动通过 request_irq() 注册中断处理函数,并在其中完成快速响应(如读取 FIFO 数据、清除中断标志)。

判断方法

中断相关问题的排查路径:

  1. 查看 /proc/interrupts 确认中断是否注册、触发次数是否增长。
  2. 使用 cat /proc/irq/xxx/smp_affinity 设置中断亲和性,将中断绑定到指定 CPU 核。
  3. 检查设备树中 interrupt-parent、interrupts 属性是否正确。
  4. 对于 PCIe 设备,确认是否支持 MSI/MSI-X,使用 lspci -vvv 查看中断模式。

案例拆解:共享中断处理不当导致设备无响应

某 Ethernet 驱动与 GPIO 按键共享同一中断号(IRQ 7),驱动在中断处理函数中无条件清除中断状态寄存器,导致 GPIO 中断丢失。具体过程:

  • 背景:x86 平台,两个设备共享 IRQ 7。
  • 现象:按下 GPIO 按键无反应,但网卡工作正常。
  • 分析:共享中断要求每个处理函数在入口处先判断中断是否属于自己的设备。该驱动直接读取并清除了中断状态寄存器,把 GPIO 设备的中断状态也清掉了。
  • 修复:在中断处理函数开头检查设备中断状态位,如果未置位则立即返回 IRQ_NONE,不执行清除操作。
  • 复盘:共享中断的处理函数必须返回 IRQ_HANDLED 或 IRQ_NONE,且不能干扰其他设备的中断状态。

2.4 DMA:高效数据搬运与一致性挑战

核心原理

DMA(Direct Memory Access)允许外设直接读写内存,无需 CPU 逐字节搬运。驱动开发中 DMA 的关键点:

  • 一致性 DMA 映射:dma_alloc_coherent() 分配的内存,CPU 和外设看到的内容始终一致,适用于控制结构、状态信息。
  • 流式 DMA 映射:dma_map_single() / dma_unmap_single() 用于一次性数据传输,需要手动同步缓存。
  • SG(Scatter-Gather)列表:dma_map_sg() 将多个不连续内存段映射为 DMA 可访问的地址列表,适用于网络包、存储 I/O。
  • IOMMU/SMMU:在虚拟化或需要地址隔离的场景下,IOMMU 将设备 DMA 地址翻译为物理地址,提供设备级内存保护。

判断方法

DMA 调试的常用手段:

  1. 启用内核 CONFIG_DMA_API_DEBUG,检测 DMA 映射泄漏、重复映射、错误大小等。
  2. 使用 dma_debug 模块记录每次映射的调用栈。
  3. 检查 /sys/kernel/debug/dma-api/ 下的统计信息。
  4. 对于缓存一致性问题,观察数据是否出现“旧值”或“撕裂”现象。

案例拆解:DMA 缓冲区未对齐导致性能骤降

某音频驱动使用 kmalloc() 分配 DMA 缓冲区,但未指定 GFP_DMA 标志,导致缓冲区物理地址不在 DMA 控制器支持的地址范围内。具体表现:

  • 背景:32 位 ARM 平台,DMA 控制器仅支持 24 位物理地址(16MB 以下)。
  • 现象:音频播放时频繁出现 DMA transfer error,系统日志报 dma_alloc_coherent failed。
  • 分析:kmalloc(GFP_KERNEL) 分配的内存可能位于 16MB 以上,DMA 控制器无法访问。
  • 修复:改用 dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL),由 DMA API 自动选择合适的地址范围。
  • 复盘:DMA 缓冲区必须使用 DMA API 分配,不能直接使用 kmalloc 或 vmalloc,除非明确知道硬件支持全地址范围。
核心总结:驱动开发本质上是“硬件资源的管理者”。CPU 负责执行逻辑,MMU 负责地址翻译与保护,总线负责连接设备,中断负责异步通知,DMA 负责高效搬运。理解这五个硬件单元的行为边界,是写出正确、高效、可维护驱动的基石。任何驱动问题,最终都可以归结为对某个硬件单元行为理解的偏差。