操作系统基础 | 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 对比总结
:--- | :--- | :--- |
核心函数 |
idr_pre_get()
,
idr_get_new()
| idr_preload()
,
idr_alloc()
, idr_preload_end()
|调用模式 | 两步过程,必须配合重试循环 | 单次调用 (
idr_alloc
),无需循环 |预分配内存 | 全局共享,易被其他 CPU 消耗 | 每 CPU 独享,配合禁用抢占,不会被偷 |
错误处理 | 可能返回
-EAGAIN
,要求调用者重试 | 仅在所有内存分配都失败时才报错
|原子上下文 | 支持,但重试时必须退出原子上下文 | 更好支持,通过
preload
/preload_end
保障
|代码复杂度 | 高,需要大量重复的样板代码 | 低,调用逻辑非常简洁 |
结论
这篇文章记录了一个经典的内核优化案例:通过巧妙的设计(利用每 CPU 数据和禁用抢占)将一个复杂、易错、充满竞争条件的旧接口,重构为一个简洁、可靠、高效的新接口。尽管存在一些争议,但简化并提升广泛使用的底层 API 的价值是极其巨大的,这很可能是新方案最终被采纳的原因。这正是 Linux 内核持续演进的一个缩影。