4. iounmap 函数详解:原型、使用时机与内存泄漏风险

在 Linux 驱动开发中,ioremap 将物理 I/O 地址映射到内核虚拟地址空间,而 iounmap 则负责在驱动卸载或不再需要该映射时,释放该虚拟地址映射关系。正确使用 iounmap 是防止内核虚拟地址泄漏、保证系统长期稳定运行的关键环节。

4.1 iounmap 函数原型

iounmap 定义在 <asm/io.h> 中,其原型如下:

#include <linux/io.h>

void iounmap(volatile void __iomem *addr);
参数 / 返回值 说明
addr 由 ioremap 返回的虚拟地址指针,类型为 volatile void __iomem *。
返回值 无(void)。
关键点:__iomem 是内核中用于标记 I/O 内存地址空间的稀疏检查属性,驱动开发者不应强制转换去掉该属性,否则可能被 sparse 工具检测出类型不匹配。

4.2 iounmap 使用时机

iounmap 的调用时机必须与 ioremap 严格配对。以下场景是典型的释放时机:

  • 驱动卸载时:在模块的 exit 函数中,对 probe 阶段映射的所有 I/O 区域逐一调用 iounmap。
  • 设备热拔插时:当设备从系统中断开,驱动应在移除回调(如 remove)中释放映射。
  • 资源重新配置时:如果驱动需要重新映射不同范围的 I/O 地址,必须先 iounmap 旧映射,再 ioremap 新区域。
  • 错误处理路径:在 probe 函数中,如果后续步骤失败,必须回滚已成功的 ioremap。
最佳实践:建议在驱动私有数据结构中保存 __iomem 指针,并在错误处理和退出路径中统一释放,避免遗漏。

4.3 iounmap 调用流程

下图展示了从 ioremap 到 iounmap 的完整生命周期:

iounmap 调用流程 ioremap() 驱动正常使用 iounmap() 触发 iounmap 的典型条件 • 驱动模块卸载(exit 函数) • 设备热拔插(remove 回调) • 重新映射前释放旧映射 • probe 失败回滚路径

4.4 内存泄漏风险分析

未调用 iounmap 或调用顺序错误,会导致内核虚拟地址空间泄漏。与用户空间内存泄漏不同,内核虚拟地址泄漏的后果更为严重:

风险类型 后果 严重程度
虚拟地址空间耗尽 32 位系统 vmalloc 区域有限,反复映射不释放将导致无法创建新映射。 高
页表污染 残留的页表项可能被其他驱动误访问,导致数据损坏或系统崩溃。 严重
调试困难 泄漏点难以追踪,通常需要 /proc/vmallocinfo 或 kmemleak 辅助定位。 中
系统稳定性下降 长期运行的嵌入式设备可能因累积泄漏而触发 OOM 或内核 panic。 高
注意:即使驱动退出时内核会自动回收模块资源,但 ioremap 创建的映射属于 vmalloc 区域,不会在模块卸载时自动释放。驱动开发者必须显式调用 iounmap。

4.5 典型错误示例与正确写法

错误示例:遗漏 iounmap

static int my_probe(struct platform_device *pdev)
{
    void __iomem *base;

    base = ioremap(0x10000000, SZ_4K);
    if (!base)
        return -ENOMEM;

    /* 使用 base 进行寄存器操作 */
    writel(0x1, base + 0x00);

    /* 错误:probe 成功返回,但未保存 base 指针,导致后续无法释放 */
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    /* 无法获取 base 指针,无法调用 iounmap */
    return 0;
}

正确写法:保存指针并配对释放

struct my_priv {
    void __iomem *base;
    /* 其他成员 */
};

static int my_probe(struct platform_device *pdev)
{
    struct my_priv *priv;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = ioremap(0x10000000, SZ_4K);
    if (!priv->base) {
        dev_err(&pdev->dev, "ioremap failed\n");
        return -ENOMEM;
    }

    platform_set_drvdata(pdev, priv);
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    if (priv->base)
        iounmap(priv->base);   /* 正确释放 */

    return 0;
}

4.6 调试与验证方法

驱动开发者可以通过以下手段检查 iounmap 是否被正确调用:

  • 查看 /proc/vmallocinfo:列出所有 vmalloc 映射,包括 ioremap 创建的条目。卸载驱动后若仍有残留,说明存在泄漏。
  • 使用 kmemleak:内核内存泄漏检测工具,可扫描未释放的 I/O 映射。
  • 内核 sparse 静态检查:确保 __iomem 注解正确,避免类型误用。
  • 手动计数配对:在驱动中维护映射计数器,在 ioremap 时递增,iounmap 时递减,卸载时检查是否归零。
经验总结:在 probe 函数中,建议使用 devm_ioremap(资源管理版本)替代 ioremap,这样驱动卸载时内核会自动释放映射,减少人为遗漏风险。但理解 iounmap 的底层机制仍然是必备基础。

4.7 iounmap 与 devm_ioremap 对比

特性 ioremap / iounmap devm_ioremap
释放方式 手动调用 iounmap 设备卸载时自动释放
适用场景 需要精细控制映射生命周期 大多数标准驱动,减少泄漏风险
错误处理复杂度 较高,需手动回滚 较低,资源自动管理
内核版本支持 所有版本 2.6.34+