2. Linux 内核 IO 访问模型:PIO 与 MMIO 对比

在 Linux 驱动开发中,CPU 与外部设备交换数据主要通过两种 IO 访问模型:PIO(Programmed IO,程序控制 IO)和 MMIO(Memory-Mapped IO,内存映射 IO)。理解这两种模型的差异,是正确选择 IO 访问方式、优化驱动性能的基础。

2.1 PIO(程序控制 IO)模型

PIO 是最早期的 IO 访问方式,CPU 通过专用的 IO 指令(如 x86 架构的 in/out 指令)与设备寄存器进行数据交换。在这种模型下,CPU 直接参与每一次数据传输,并且 IO 端口与内存地址空间是隔离的。

核心特征:IO 端口拥有独立的地址空间,CPU 使用专用指令访问,每次传输都需要 CPU 全程介入。

PIO 工作流程

  1. CPU 执行 in/out 指令,指定端口地址。
  2. CPU 通过系统总线(如 PCI/ISA)向设备发送读写请求。
  3. 设备响应请求,数据通过 CPU 寄存器中转。
  4. CPU 将数据写入内存或继续处理下一个字节。
性能瓶颈:PIO 模式下,CPU 必须等待 IO 操作完成,无法执行其他任务。对于大数据块传输(如网络数据包、磁盘块),PIO 会严重占用 CPU 时间,导致系统吞吐量下降。

2.2 MMIO(内存映射 IO)模型

MMIO 将设备的寄存器和内存统一映射到系统的物理内存地址空间中。CPU 访问设备寄存器时,直接使用标准的 ldr/str(ARM)或 mov(x86)等内存访问指令,无需专用 IO 指令。设备寄存器在内存地址空间中占据一段连续的地址区域。

直观理解:在 MMIO 模型下,驱动开发者可以把设备寄存器当作普通内存变量来读写,但背后硬件会自动将访问路由到设备,而不是真正的 RAM。

MMIO 工作流程

  1. 系统启动时,固件或内核为设备分配一段物理内存地址区间。
  2. 驱动通过 ioremap() 将这段物理地址映射到内核虚拟地址空间。
  3. 驱动使用 readl()/writel() 等访问函数读写映射后的虚拟地址。
  4. CPU 的内存控制器检测到目标地址属于设备区域,自动将访问转发到设备总线。

2.3 PIO 与 MMIO 核心对比

对比维度 PIO(程序控制 IO) MMIO(内存映射 IO)
地址空间 独立的 IO 端口地址空间 统一的内存地址空间
访问指令 专用指令(如 in/out) 标准内存访问指令
CPU 参与度 每次传输 CPU 全程参与 CPU 发起后可由 DMA 或总线控制器接管
传输效率 低,尤其大数据块场景 高,支持突发传输和缓存一致性
编程复杂度 低,指令直接操作端口 中等,需要地址映射和访问顺序控制
可移植性 差,依赖架构专用指令 好,Linux 内核提供统一 API
适用场景 低速控制类设备(如 GPIO、UART) 高速数据类设备(如网卡、GPU、存储控制器)

2.4 MMIO 的优势与适用场景

MMIO 的主要优势

  • 性能更高:MMIO 支持 CPU 缓存和写合并(write-combining),减少总线事务次数;配合 DMA 可实现零 CPU 干预的大数据量传输。
  • 编程模型统一:驱动开发者只需掌握内存访问 API(readl/writel/ioread32/iowrite32),无需关心底层架构差异。
  • 地址空间更大:现代 64 位系统的内存地址空间远大于传统 IO 端口空间(x86 IO 端口仅 64KB),可映射更多设备资源。
  • 支持复杂设备:MMIO 可以映射大块设备内存(如显存、网络包缓冲区),便于驱动直接操作。

典型适用场景

设备类型 原因
千兆/万兆网卡 需要高速访问描述符环和包缓冲区,MMIO 配合 DMA 实现线速转发
GPU/显示控制器 帧缓冲和命令队列通常映射为 MMIO 区域,驱动直接读写
NVMe/SSD 控制器 提交队列和完成队列通过 MMIO 访问,降低 IO 延迟
USB 3.0/xHCI 控制器 寄存器空间和门铃(doorbell)寄存器使用 MMIO
PCIe 配置空间 PCIe 扩展配置空间(256 字节以上)只能通过 MMIO 访问

2.5 PIO 与 MMIO 访问路径对比图

PIO 与 MMIO 访问路径对比 PIO 与 MMIO 访问路径对比 PIO 模型 CPU 专用 IO 指令 IO 端口地址空间 设备寄存器 MMIO 模型 CPU 标准内存指令 统一内存地址空间 设备寄存器 MMIO 将设备寄存器映射到内存地址空间,CPU 使用统一的内存指令访问,无需专用 IO 指令

2.6 实际驱动开发中的选择建议

经验法则:对于新设计的 Linux 驱动,优先选择 MMIO。除非目标设备是 legacy 硬件且只支持 PIO,或者设备寄存器极少且对延迟不敏感(如简单的 GPIO 控制器),否则 MMIO 是更现代、更高效的选择。

在 Linux 内核中,MMIO 的访问通过一组标准 API 完成,驱动开发者无需关心底层是 PIO 还是 MMIO——内核的 IO 访问抽象层已经封装了差异。例如,ioread32() 在 MMIO 设备上执行内存读取,在 PIO 设备上自动转换为 inl() 指令。这种透明性使得驱动代码具有更好的可移植性。

代码示例:MMIO 访问 vs PIO 访问

/* MMIO 方式:通过 ioremap 映射后使用 readl/writel */
void __iomem *regs = ioremap(0xFE001000, 0x1000);
u32 val = readl(regs + 0x10);   /* 读取设备寄存器 */
writel(0x01, regs + 0x10);      /* 写入设备寄存器 */
iounmap(regs);

/* PIO 方式:使用 inl/outl 直接操作端口(x86 架构) */
u32 val = inl(0x3F0);           /* 从 IO 端口 0x3F0 读取 */
outl(0x01, 0x3F0);              /* 写入 IO 端口 0x3F0 */
注意事项:MMIO 访问必须使用 readl/writel 等带内存屏障的访问函数,不能直接使用指针解引用。这是因为编译器可能优化掉多次访问,或者 CPU 乱序执行导致访问顺序错误。内核提供的这些函数保证了访问的顺序性和正确性。