3. ioremap 函数详解
在 Linux 驱动开发中,ioremap 是连接物理 IO 内存与内核虚拟地址空间的核心桥梁。驱动程序无法直接访问物理地址,必须通过 ioremap 将物理 IO 内存区域映射到内核虚拟地址空间,才能使用标准的内存访问指令(如 readb/writel)进行读写。本节将深入剖析 ioremap 的原型、参数、返回值及其与 ioremap_nocache 的关键区别。
核心概念:ioremap 并不分配新的物理内存,它仅为指定的物理地址范围建立页表映射,使内核能够像访问普通内存一样访问外设的寄存器或内存空间。
3.1 ioremap 函数原型与参数
ioremap 函数在 <asm/io.h> 中声明,其原型如下:
#include <asm/io.h>
void __iomem *ioremap(phys_addr_t offset, unsigned long size);
| 参数 / 返回值 | 类型 | 含义 |
|---|---|---|
| offset | phys_addr_t |
要映射的物理 IO 内存起始地址(通常是外设寄存器的基地址) |
| size | unsigned long |
要映射的物理地址空间长度(以字节为单位) |
| 返回值 | void __iomem * |
映射成功返回内核虚拟地址指针;失败返回 NULL |
编程提示:返回值类型
void __iomem * 是一个“标记指针”,用于指示该地址指向 IO 内存而非系统 RAM。驱动应使用专用的访问函数(如 ioread32/iowrite32)操作该指针,不应直接解引用。
3.2 ioremap 映射流程
下图展示了从物理 IO 地址到内核虚拟地址的完整映射过程:
3.3 ioremap 与 ioremap_nocache 的区别
Linux 内核提供了 ioremap_nocache 函数,用于建立非缓存的 IO 内存映射。在大多数现代架构上,ioremap 默认行为已经等同于 ioremap_nocache,但了解其历史区别仍然重要。
| 对比维度 | ioremap | ioremap_nocache |
|---|---|---|
| 缓存策略 | 默认可能启用 CPU 缓存(取决于架构) | 强制禁用 CPU 缓存和写缓冲 |
| 内存屏障 | 不保证顺序一致性 | 保证读写顺序(隐含内存屏障) |
| 适用场景 | 普通 IO 内存(寄存器、FIFO) | 对时序敏感的 IO 内存(DMA 描述符、中断状态寄存器) |
| 现代内核行为 | 大多数架构已默认实现为 nocache | 与 ioremap 相同(兼容旧代码) |
| 推荐使用 | 新驱动应使用 ioremap |
仅用于需要显式强调非缓存的场景 |
重要警告:对于外设寄存器,永远不要使用
ioremap_cache 或普通 remap_pfn_range 建立缓存映射。CPU 缓存会导致寄存器读取返回陈旧值,写入操作被延迟或合并,造成设备驱动逻辑错误甚至硬件损坏。
3.4 典型使用模式
以下代码展示了驱动中 ioremap 的标准使用模式:
#include <linux/io.h>
#include <linux/slab.h>
struct my_device {
void __iomem *regs;
unsigned long phys_base;
unsigned long region_size;
};
static int my_device_probe(struct platform_device *pdev)
{
struct my_device *dev;
struct resource *res;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// 1. 获取物理地址和大小
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
dev->phys_base = res->start;
dev->region_size = resource_size(res);
// 2. 请求 IO 内存区域
if (!request_mem_region(dev->phys_base, dev->region_size, "my_device")) {
dev_err(&pdev->dev, "无法请求 IO 内存区域\n");
kfree(dev);
return -EBUSY;
}
// 3. 建立映射
dev->regs = ioremap(dev->phys_base, dev->region_size);
if (!dev->regs) {
dev_err(&pdev->dev, "ioremap 失败\n");
release_mem_region(dev->phys_base, dev->region_size);
kfree(dev);
return -ENOMEM;
}
// 4. 访问寄存器(示例:读取设备 ID)
u32 device_id = ioread32(dev->regs + 0x00);
dev_info(&pdev->dev, "设备 ID: 0x%08x\n", device_id);
// 5. 写入控制寄存器
iowrite32(0x01, dev->regs + 0x04);
platform_set_drvdata(pdev, dev);
return 0;
}
static int my_device_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
// 6. 解除映射并释放资源
if (dev->regs) {
iounmap(dev->regs);
release_mem_region(dev->phys_base, dev->region_size);
}
kfree(dev);
return 0;
}
3.5 ioremap 映射与取消映射的生命周期
下图展示了 ioremap 从建立到销毁的完整生命周期,以及各阶段的关键操作:
3.6 关键注意事项
- 映射粒度:ioremap 以页为单位进行映射,即使指定 size 小于一页(4KB),内核也会映射整个页。多余部分不可访问,但不会报错。
- 多次映射:同一物理地址可以被多次 ioremap,每次返回不同的虚拟地址。但应避免对同一寄存器区域建立多个映射,防止缓存一致性问题。
- 原子上下文:ioremap 可能睡眠(分配页表),不能在中断上下文、spinlock 持有期间调用。
- 64 位地址:在 64 位系统上,phys_addr_t 为 64 位宽,可映射高于 4GB 的物理地址。
- 调试辅助:使用
/proc/iomem查看系统 IO 内存分配情况,使用/proc/ioports查看 IO 端口分配。
调试技巧:如果 ioremap 返回 NULL,首先检查是否已调用
request_mem_region 且未与其他驱动冲突。其次检查物理地址是否在系统 IO 内存范围内(可通过 cat /proc/iomem 确认)。