操作系统基础 | 4.6 内核API重构案例:IDR API

文章内容来自 LWN.net

文章摘要 (Summary)

这篇发表于 2013 年 2 月的文章讨论了 Linux 内核中 IDR 子系统 API 的一次重大简化改革,由开发者 Tejun Heo 主导。IDR 机制用于高效分配和管理整数 ID(例如设备名、POSIX 定时器 ID 等),其旧 API 因其复杂性和潜在的竞争条件而闻名。

核心问题:旧 API 的缺陷 1. 两步分配:需要先调用 idr_pre_get() 预分配内存(可休眠),再调用 idr_get_new() 获取 ID(可原子上下文)。 2. 必须重试循环idr_get_new() 可能因预分配内存被其他 CPU 耗尽而失败(返回 -EAGAIN),要求调用者编写冗长且易错的循环重试代码。 3. 全局资源竞争idr_pre_get() 预分配的内存是全局的,多个 CPU 竞争时,后执行的 idr_get_new() 可能因资源不足而失败,迫使代码退出原子上下文进行重试,这条路径往往缺乏测试。

解决方案:新 API 的改进 Tejun Heo 引入了三个新函数来简化流程: 1. idr_preload(gfp_t gfp_mask): 为当前 CPU 预分配内存,并禁用抢占以防止预分配的内存被偷。 2. idr_alloc(...): 单次调用即可完成 ID 分配和关联。它接受 ID 范围参数,并仅在真正需要时(未预分配或预分配不足)才使用 gfp_mask 分配内存。它只会在内存分配彻底失败时报错,消除了对 -EAGAIN 的重试循环需求。 3. idr_preload_end(): 在 idr_alloc 后调用,重新启用抢占

关键优势: * 更简单:消除了遍布内核的百余处重复、易错的样板代码。 * 更可靠:通过每 CPU 预分配和禁用抢占,基本消除了在原子上下文中因资源竞争而失败的需要。 * 更灵活idr_alloc 可以指定 ID 范围,并且如果能在进程上下文调用,甚至可以完全省略 idr_preload/idr_preload_end

社区反应: 尽管大部分开发者接受了这个改动(给出了 Acked-by),但 Eric Biederman 表达了强烈反对,认为新 API 的 idr_preload 像是一种难以理解的“魔法”。然而,文章作者(Jonathan Corbet)预测,新 API 带来的巨大简化优势将使其最终被内核社区接受

新旧 API 对比总结

特性 | 旧 API (2013 年前) | 新 API (Tejun Heo 提议) |
:--- | :--- | :--- |
核心函数 | idr_pre_get(), idr_get_new() | idr_preload(), idr_alloc(), idr_preload_end() |
调用模式 | 两步过程,必须配合重试循环 | 单次调用 (idr_alloc),无需循环 |
预分配内存 | 全局共享,易被其他 CPU 消耗 | 每 CPU 独享,配合禁用抢占,不会被偷 |
错误处理 | 可能返回 -EAGAIN,要求调用者重试 | 仅在所有内存分配都失败时才报错 |
原子上下文 | 支持,但重试时必须退出原子上下文 | 更好支持,通过 preload/preload_end 保障 |
代码复杂度 | 高,需要大量重复的样板代码 | 低,调用逻辑非常简洁 |

结论

这篇文章记录了一个经典的内核优化案例:通过巧妙的设计(利用每 CPU 数据和禁用抢占)将一个复杂、易错、充满竞争条件的旧接口,重构为一个简洁、可靠、高效的新接口。尽管存在一些争议,但简化并提升广泛使用的底层 API 的价值是极其巨大的,这很可能是新方案最终被采纳的原因。这正是 Linux 内核持续演进的一个缩影。