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 内部关键步骤:
- 查找空闲虚拟地址区间: 在 vmalloc 区域(通常为 VMALLOC_START ~ VMALLOC_END)中分配一段虚拟地址。
- 创建页表项: 遍历物理地址对应的页面,为每个页面建立二级页表(或三级页表)映射。
- 设置页表属性: 根据调用者传入的缓存标志(如
ioremap或ioremap_nocache),设置页表描述符中的 C(Cacheable)、B(Bufferable)等位。 - 刷新 TLB: 确保新建立的映射立即生效。
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 缓冲区 |
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提供更严格的非缓存保证。