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 地址到内核虚拟地址的完整映射过程:

ioremap 映射流程 物理 IO 地址 0x10000000 内核页表 建立映射关系 内核虚拟地址 0xF0000000 ioremap(offset, size) • offset = 0x10000000(物理基地址) • size = 0x1000(4KB,一页大小) void __iomem * 返回值 • 成功:0xF0000000(虚拟地址) • 失败:NULL 映射后:通过 ioread32(虚拟地址) / iowrite32(值, 虚拟地址) 访问外设

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 从建立到销毁的完整生命周期,以及各阶段的关键操作:

ioremap 生命周期管理 阶段 1 request_mem_region 申请物理地址所有权 阶段 2 ioremap 建立页表映射 阶段 3 ioread32/iowrite32 读写寄存器 阶段 4 iounmap 错误处理关键点 • ioremap 失败:立即调用 release_mem_region 释放已申请的物理区域 • 驱动移除:先 iounmap,再 release_mem_region,顺序不可颠倒 最佳实践 1. 始终配对使用 request_mem_region / release_mem_region 2. 始终配对使用 ioremap / iounmap 3. 使用 __iomem 标记指针,配合 Sparse 静态检查工具

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 确认)。