3. ioremap 原理:物理地址到虚拟地址映射、MMU 页表操作、ioremap 与 ioremap_nocache 区别

在嵌入式 Linux 驱动开发中,CPU 通过 虚拟地址 访问外设寄存器。而硬件外设的寄存器位于 物理地址 空间。ioremap 系列函数是连接这两个世界的桥梁,其核心原理涉及 MMU 页表操作与缓存策略控制。

3.1 为什么需要 ioremap?

现代嵌入式处理器(ARM、RISC-V、x86)都启用了 MMU。内核空间虽然能够访问物理内存,但为了统一管理、隔离保护和缓存一致性,内核通常运行在虚拟地址模式下。直接使用物理地址访问外设寄存器会导致以下问题:

  • 缓存不一致: 外设寄存器可能被 CPU 缓存命中,导致读写顺序错误或读到过期值。
  • 页表缺失: 物理地址范围可能没有对应的页表项,访问会触发缺页异常或总线错误。
  • 不可移植: 不同 SoC 的物理地址布局不同,驱动代码应使用虚拟地址抽象。
核心原则: 驱动代码永远不要直接使用物理地址读写寄存器。必须通过 ioremap 获得虚拟地址后,再使用 readl/writel 等访问函数。

3.2 ioremap 映射流程与 MMU 页表操作

ioremap 的本质是:在内核虚拟地址空间中分配一段连续的虚拟地址区间,并在 MMU 页表中建立该虚拟地址到目标物理地址的映射关系。同时,根据缓存策略设置页表属性。

ioremap 内部关键步骤:
  1. 查找空闲虚拟地址区间: 在 vmalloc 区域(通常为 VMALLOC_START ~ VMALLOC_END)中分配一段虚拟地址。
  2. 创建页表项: 遍历物理地址对应的页面,为每个页面建立二级页表(或三级页表)映射。
  3. 设置页表属性: 根据调用者传入的缓存标志(如 ioremap 或 ioremap_nocache),设置页表描述符中的 C(Cacheable)、B(Bufferable)等位。
  4. 刷新 TLB: 确保新建立的映射立即生效。
ioremap 映射流程与 MMU 页表操作 物理地址空间 0x01C2_0000 0x01C2_1000 0x01C2_2000 0x01C2_3000 外设寄存器物理页 MMU 页表 页表项 1: VA→PA 页表项 2: VA→PA 页表项 3: VA→PA 页表项 4: VA→PA 属性位:C/B/TEX 内核虚拟地址空间 0xF000_0000 0xF000_1000 0xF000_2000 0xF000_3000 vmalloc 区域 ioremap 在 vmalloc 区域分配虚拟地址,通过 MMU 页表建立与物理寄存器的映射,并设置缓存属性

3.3 ioremap 与 ioremap_nocache 的区别

Linux 内核提供了多个 ioremap 变体,最常用的是 ioremap 和 ioremap_nocache。它们的核心区别在于 页表缓存属性 的设置,这直接影响外设访问的正确性与性能。

特性 ioremap ioremap_nocache
缓存策略 非缓存(Uncacheable)或强序(Strongly Ordered) 非缓存(Uncacheable)
写缓冲 禁止写缓冲(No Write Buffer) 禁止写缓冲(No Write Buffer)
合并写 禁止 禁止
推测读 禁止 禁止
ARM 平台实际行为 设置页表为 Strongly Ordered(Device-nGnRnE) 设置页表为 Device-nGnRnE(与 ioremap 相同)
x86 平台实际行为 设置 PAT 为 UC-(Uncacheable Minus) 设置 PAT 为 UC(Uncacheable)
适用场景 大多数外设寄存器(ARM 上两者等价) 对缓存一致性要求极高的场景(x86 上更严格)
重要提示: 在 ARM 架构中,ioremap 和 ioremap_nocache 的页表属性最终都映射为 Device-nGnRnE(非缓存、非合并、非推测、非重排),因此两者行为完全一致。在 x86 架构上,ioremap_nocache 使用更严格的 UC 类型,禁止任何弱排序优化。驱动开发时,推荐统一使用 ioremap_nocache 以确保最大兼容性。

3.4 页表缓存属性详解

MMU 页表描述符中的缓存属性位决定了 CPU 如何与内存/外设交互。以下是 ARMv7/ARMv8 架构中常见的缓存属性组合:

属性组合 C 位 B 位 TEX 位 行为描述
Strongly Ordered 0 0 000 完全非缓存,所有访问严格顺序执行,适合外设寄存器
Device 0 0 001 非缓存,允许少量合并,适合帧缓冲等
Normal (Cacheable) 1 1 010 可缓存,适合 DDR 内存,性能最优
Normal (Non-Cacheable) 0 1 010 非缓存但允许写缓冲,适合 DMA 缓冲区
ioremap 与 ioremap_nocache 页表属性对比 ioremap() ARM: Device-nGnRnE x86: UC- (Uncacheable Minus) C=0, B=0, TEX=000 无缓存 · 无写缓冲 · 严格顺序 ioremap_nocache() ARM: Device-nGnRnE (同左) x86: UC (Uncacheable) C=0, B=0, TEX=000 无缓存 · 无写缓冲 · 严格顺序 ARM 等价 ARM 上两者完全等价;x86 上 ioremap_nocache 使用更严格的 UC 类型

3.5 驱动代码示例:ioremap 与 ioremap_nocache 的使用

以下代码展示了在驱动初始化函数中如何使用 ioremap 映射 GPIO 寄存器,并对比两种映射方式:

#include <linux/io.h>
#include <linux/ioport.h>

#define GPIO_BASE_PHYS  0x01C20800
#define GPIO_REG_SIZE   0x24

static void __iomem *gpio_base;
static void __iomem *gpio_base_nc;

static int my_driver_probe(struct platform_device *pdev)
{
    struct resource *res;

    /* 方法1:使用标准 ioremap */
    res = request_mem_region(GPIO_BASE_PHYS, GPIO_REG_SIZE, "my_gpio");
    if (!res) {
        dev_err(&pdev->dev, "Failed to request memory region\n");
        return -EBUSY;
    }

    gpio_base = ioremap(GPIO_BASE_PHYS, GPIO_REG_SIZE);
    if (!gpio_base) {
        dev_err(&pdev->dev, "ioremap failed\n");
        release_mem_region(GPIO_BASE_PHYS, GPIO_REG_SIZE);
        return -ENOMEM;
    }

    /* 方法2:使用 ioremap_nocache(ARM 上等价,x86 更严格) */
    gpio_base_nc = ioremap_nocache(GPIO_BASE_PHYS, GPIO_REG_SIZE);
    if (!gpio_base_nc) {
        dev_err(&pdev->dev, "ioremap_nocache failed\n");
        iounmap(gpio_base);
        release_mem_region(GPIO_BASE_PHYS, GPIO_REG_SIZE);
        return -ENOMEM;
    }

    /* 通过虚拟地址读写寄存器 */
    writel(0x01, gpio_base + 0x00);  /* 设置 GPIO 方向 */
    val = readl(gpio_base + 0x10);   /* 读取 GPIO 电平 */

    dev_info(&pdev->dev, "GPIO mapped at virt 0x%p (nocache: 0x%p)\n",
             gpio_base, gpio_base_nc);
    return 0;
}

static int my_driver_remove(struct platform_device *pdev)
{
    iounmap(gpio_base_nc);
    iounmap(gpio_base);
    release_mem_region(GPIO_BASE_PHYS, GPIO_REG_SIZE);
    return 0;
}
关键点总结:
  • ioremap 返回的指针类型为 void __iomem *,必须使用 readl()/writel() 等访问函数,不能直接解引用。
  • 映射前必须调用 request_mem_region() 进行资源申请,避免地址冲突。
  • 卸载驱动时,必须使用 iounmap() 解除映射,并 release_mem_region() 释放资源。
  • 在 ARM 平台上,ioremap 和 ioremap_nocache 行为一致;在 x86 上,ioremap_nocache 提供更严格的非缓存保证。