1.概述
IDR(Integer ID Allocator)是 Linux 内核提供的小整数 ID 动态管理机制,核心目标是解决 "整数 ID 与指针的双向映射" 问题。在内核开发中,大量场景需要为对象(如设备、文件、共享内存块)分配唯一整数标识(ID),并支持通过 ID 快速查找对应对象。IDR 通过基数树(Radix Tree)实现高效的 ID 分配、查找与释放,兼顾动态扩展性与查询性能。
1.1 与其他 ID 管理方案的对比
方案 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
数组 | 查找 O (1),实现简单 | 需预先知道最大 ID 范围,浪费内存 | ID 范围固定且密集(如文件描述符) |
哈希表 | 平均查找 O (1) | 冲突处理复杂,内存开销大 | ID 范围大但查询频繁 |
链表 | 动态扩展,内存紧凑 | 查找 O (n),性能差 | ID 数量极少的场景 |
IDR | 动态扩展,查找 O (logN),内存高效 | 实现较复杂 | 动态 ID 分配、稀疏 ID 范围、需双向映射 |
IDR 的核心优势在于对稀疏 ID 范围的高效支持。例如,当需要分配 1~10000 的 ID 但实际仅使用其中 100 个时,数组会浪费 99% 的内存,而 IDR 通过基数树仅存储实际使用的 ID 节点,大幅减少内存开销。
1.2 典型应用场景
IDR 在 Linux 内核中应用广泛,典型场景包括:
-
设备驱动:管理设备号(如块设备、字符设备的主 / 次设备号);
-
DRM 子系统:为 GPU 共享内存对象(GEM Object)分配 handle;
-
进程管理:为线程、信号量等对象分配标识 ID;
-
网络子系统:管理套接字、连接的唯一标识符;
-
文件系统:为 inode、dentry 等对象分配临时 ID。
以 DRM 为例,当用户态程序通过ioctl
创建 GEM 对象时,内核需分配一个 32 位 handle(ID)与对象绑定,用户态通过 handle 操作对象,内核通过 IDR 快速将 handle 映射到内核对象指针。
2. IDR 底层实现原理
2.1 基数树(Radix Tree)基础
IDR 的底层数据结构是基数树—— 一种多叉树,每个节点的子节点数量为 2^n(称为 "基数")。Linux 内核中 IDR 使用的基数树通常为64 叉树(节点子节点数为 64),通过将 ID 分解为多个 6 位的片段(64=2^6),每段作为树的一层索引。
2.1.1 32 位 ID 的层级分解
32 位 ID 被拆分为 5 个 6 位片段(6*5=30,剩余 2 位作为最高层):
-
第 1 层(根节点):ID 的 26~31 位(6 位);
-
第 2 层:ID 的 20~25 位(6 位);
-
第 3 层:ID 的 14~19 位(6 位);
-
第 4 层:ID 的 8~13 位(6 位);
-
第 5 层:ID 的 2~7 位(6 位);
-
第 6 层(叶子节点):ID 的 0~1 位(2 位)。
2.1.2 动态节点创建
基数树采用延迟创建策略:仅当需要存储 ID 时,才动态分配对应层级的节点。例如,分配 ID=0 时,仅创建根节点和叶子节点,中间层节点无需分配,大幅节省内存。
这种分解方式确保 32 位 ID 可被基数树完全覆盖,查找时通过逐层解析 ID 片段定位叶子节点,叶子节点中存储与 ID 关联的指针。
2.2 IDR 核心数据结构
IDR 的核心结构体定义在<linux/idr.h>
中,关键成员如下:
struct idr {
struct radix_tree_root idr_rt; // 基数树根节点
unsigned int idr_next; // 下一个尝试分配的ID(优化连续分配)
struct mutex idr_mutex; // 保护IDR操作的互斥锁(可选)
// 其他内部成员(如版本号、最大深度等)
};
idr_rt
:基数树的根节点,所有 ID 映射通过该树管理;idr_next
:记录下一次分配 ID 时的起始点,优化连续 ID 分配效率(避免从 0 开始查找);idr_mutex
:用于并发控制,多线程访问时需加锁保护(可通过DEFINE_IDR_WITH_LOCK
自动绑定锁)。
3. IDR 核心 API 详解
3.1 初始化与销毁
3.1.1 idr_init()
:手动初始化
void idr_init(struct idr *idp);
3.1.2 DEFINE_IDR()
:静态初始化
#define DEFINE_IDR(name) \
struct idr name = { .idr_rt = RADIX_TREE_INIT, .idr_next = 0 }
- 功能:静态声明并初始化 IDR 结构体,等价于
idr_init()
但更简洁; - 优势:编译期初始化,无需运行时调用函数,适合全局或静态 IDR;
3.1.3 DEFINE_IDR_WITH_LOCK()
:带锁的静态初始化
#define DEFINE_IDR_WITH_LOCK(name) \
struct idr name = { .idr_rt = RADIX_TREE_INIT, \
.idr_next = 0, \
.idr_mutex = __MUTEX_INITIALIZER(name.idr_mutex) }
-
功能:销毁 IDR,释放基数树中所有节点的内存;
-
注意事项:
-
销毁前需确保所有 ID 已通过
idr_remove()
释放(否则会导致内存泄漏); -
销毁后 IDR 不可再使用,除非重新初始化;
-
3.2 ID 分配
3.2.1 基础分配:idr_alloc()
int idr_alloc(struct idr *idp, void *ptr, int start,
int end, gfp_t gfp_mask);
- 参数详解:
-
idp
:指向已初始化的 IDR 结构体; -
ptr
:与 ID 关联的指针(如设备结构体指针); -
start
:分配的起始 ID(含),通常设为 0; -
end
:分配的结束 ID(不含),0 表示使用默认最大值(INT_MAX); -
gfp_mask
:内存分配标志(如GFP_KERNEL
允许休眠,GFP_ATOMIC
用于中断上下文);
-
-
返回值:
-
成功:返回分配的 ID(≥start 且 < end);
-
失败:返回负数(
-ENOSPC
表示 ID 范围耗尽,-ENOMEM
表示内存不足)。
-
示例:为设备分配 ID(1~100 范围)
struct my_device dev; // 设备结构体
int id = idr_alloc(&my_idr, &dev, 1, 100, GFP_KERNEL);
if (id < 0) {
pr_err("ID allocation failed: %d\n", id);
return id;
}
3.2.2 循环分配:idr_alloc_cyclic()
int idr_alloc_cyclic(struct idr *idp, void *ptr, int start, int end, gfp_t gfp_mask);
-
功能:与
idr_alloc()
类似,但当 ID 范围接近上限时,从start
重新开始分配(循环重用旧 ID); -
适用场景:ID 可能频繁创建与销毁,需要避免 ID 无限增长(如进程 ID 循环分配);
3.3 ID 查找:idr_find()
void *idr_find(const struct idr *idp, int id);
-
功能:通过 ID 查找关联的指针;
-
参数:
-
idp
:目标 IDR 结构体; -
id
:要查找的 ID(必须是通过idr_alloc()
分配的合法 ID);
-
-
返回值:
-
成功:返回与 ID 关联的指针(需强制类型转换);
-
失败:返回
NULL
(ID 不存在或已被移除);
-
-
性能:时间复杂度为 O (log₆₄N),N 为已分配的 ID 数量;
3.4 ID 释放:idr_remove()
void idr_remove(struct idr *idp, int id);
-
功能:释放指定 ID,从基数树中删除该 ID 与指针的映射(不释放指针指向的内存);
-
参数:
-
idp
:目标 IDR 结构体; -
id
:要释放的 ID(必须存在);
-
-
注意事项:
-
释放后该 ID 可被重新分配;
-
需手动释放指针指向的对象内存(如
kfree(dev)
)。
-
3.5 遍历 IDR:idr_for_each()
与idr_for_each_entry()
3.5.1 回调遍历:idr_for_each()
int idr_for_each(const struct idr *idp,
int (*fn)(int id, void *p, void *data),
void *data);
-
功能:遍历 IDR 中所有已分配的 ID,对每个 ID 执行回调函数
fn
; -
参数:
-
fn
:回调函数,原型为int (*fn)(int id, void *p, void *data)
,返回 0 继续遍历,非 0 终止; -
data
:传递给回调函数的上下文数据;
-
3.5.2 条目遍历:idr_for_each_entry()
#define idr_for_each_entry(idr, entry, id) \
for (id = 0; ((entry) = idr_get_next((idr), &(id))) != NULL; ++id)
-
功能:通过循环遍历 IDR,直接获取每个 ID 关联的对象指针;
-
参数:
-
entry
:循环中当前对象的指针(需声明为目标类型); -
id
:循环中当前的 ID 值(需声明为int
);
-
3.5.3 范围遍历:idr_get_next()
void *idr_get_next(const struct idr *idp, int *nextid);
-
功能:从
*nextid
开始查找下一个已分配的 ID,适合自定义范围遍历; -
示例:查找 ID≥10 的第一个设备:
int id = 10;
struct my_device *dev = idr_get_next(&my_idr, &id);
if (dev) {
pr_info("First ID ≥10: %d\n", id);
}
4. 并发控制与最佳实践
IDR 本身不保证线程安全,多线程同时访问(如分配 / 查找 / 删除)时需通过锁保护。推荐使用以下两种方式:
4.1 显式加锁(适合复杂场景)
static DEFINE_IDR(my_idr);
static DEFINE_MUTEX(my_idr_lock); // 独立的互斥锁
// 分配ID时加锁
mutex_lock(&my_idr_lock);
int id = idr_alloc(&my_idr, dev, 0, 0, GFP_KERNEL);
mutex_unlock(&my_idr_lock);
// 查找ID时加锁
mutex_lock(&my_idr_lock);
struct my_device *dev = idr_find(&my_idr, id);
mutex_unlock(&my_idr_lock);
4.2 内置锁(适合简单场景)
使用DEFINE_IDR_WITH_LOCK
绑定内置锁,通过idr_lock()
/idr_unlock()
操作。
static DEFINE_IDR_WITH_LOCK(my_idr); // 带内置锁的IDR
// 分配ID时加锁
idr_lock(&my_idr);
int id = idr_alloc(&my_idr, dev, 0, 0, GFP_KERNEL);
idr_unlock(&my_idr);
5. 完整示例:设备驱动中的 IDR 应用
以下是一个字符设备驱动中使用 IDR 管理设备 ID 的完整示例。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/idr.h>
#include <linux/mutex.h>
// 设备结构体
struct my_dev {
int id; // IDR分配的ID
char name[32]; // 设备名称
struct cdev cdev; // 字符设备结构体
};
// IDR与锁
static DEFINE_IDR_WITH_LOCK(dev_idr); // 带内置锁的IDR
static dev_t dev_base; // 设备号基址
static const int MAX_DEVICES = 10; // 最大设备数量
// 设备操作函数(简化)
static int my_open(struct inode *inode, struct file *filp) {
int id = iminor(inode) - 1; // 从次设备号获取ID
struct my_dev *dev;
idr_lock(&dev_idr);
dev = idr_find(&dev_idr, id); // 通过ID查找设备
idr_unlock(&dev_idr);
if (!dev)
return -ENODEV;
filp->private_data = dev;
return 0;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
// 其他操作(read/write等)
};
// 初始化设备
static int create_devices(void) {
int i, ret;
struct my_dev *dev;
// 申请设备号(假设主设备号动态分配)
ret = alloc_chrdev_region(&dev_base, 1, MAX_DEVICES, "my_dev");
if (ret < 0)
return ret;
// 批量创建设备并分配ID
idr_preload(GFP_KERNEL);
idr_lock(&dev_idr);
for (i = 0; i < MAX_DEVICES; i++) {
dev = kzalloc(sizeof(*dev), GFP_NOWAIT); // 非阻塞分配(预加载已保证内存)
if (!dev) {
ret = -ENOMEM;
goto err;
}
// 分配ID(范围1~MAX_DEVICES)
dev->id = idr_alloc(&dev_idr, dev, 1, MAX_DEVICES + 1, GFP_NOWAIT);
if (dev->id < 0) {
ret = dev->id;
kfree(dev);
goto err;
}
// 初始化字符设备
snprintf(dev->name, sizeof(dev->name), "my_dev%d", dev->id);
cdev_init(&dev->cdev, &my_fops);
cdev_add(&dev->cdev, MKDEV(MAJOR(dev_base), dev->id), 1);
}
idr_unlock(&dev_idr);
idr_preload_end();
return 0;
err:
// 出错时清理已分配的设备
idr_for_each_entry(&dev_idr, dev, i) {
idr_remove(&dev_idr, dev->id);
cdev_del(&dev->cdev);
kfree(dev);
}
idr_unlock(&dev_idr);
idr_preload_end();
unregister_chrdev_region(dev_base, MAX_DEVICES);
return ret;
}
// 模块退出时清理
static void cleanup_devices(void) {
struct my_dev *dev;
int id;
idr_lock(&dev_idr);
// 遍历所有设备并释放
idr_for_each_entry(&dev_idr, dev, id) {
idr_remove(&dev_idr, id);
cdev_del(&dev->cdev);
kfree(dev);
}
idr_unlock(&dev_idr);
idr_destroy(&dev_idr); // 销毁IDR
unregister_chrdev_region(dev_base, MAX_DEVICES);
}
module_init(create_devices);
module_exit(cleanup_devices);
MODULE_LICENSE("GPL");
示例说明:
-
用
DEFINE_IDR_WITH_LOCK
创建带内置锁的 IDR,确保多线程安全; -
批量创建设备时使用
idr_preload
优化内存分配; -
通过 IDR 管理设备 ID 与
struct my_dev
的映射,open
时通过次设备号(ID)查找设备; -
退出时遍历 IDR 释放所有设备,避免内存泄漏。
6. 总结
IDR 机制通过基数树实现了高效的 ID 管理,兼顾动态扩展性与查询性能,是 Linux 内核中处理整数 ID 映射的核心方案。其核心优势包括:
-
动态分配:支持 ID 范围动态扩展,无需预先定义大小;
-
高效查找:基数树保证 O (logN) 的查找复杂度,适合大量 ID 场景;
-
内存高效:延迟创建节点,对稀疏 ID 范围友好。
在实际开发中,需注意并发控制(加锁)、错误处理(检查返回值)及资源清理(释放 ID 与对象),结合DEFINE_IDR_WITH_LOCK
等宏可简化使用。掌握 IDR 机制对内核驱动、子系统开发至关重要,尤其在需要管理大量动态对象的场景中能显著提升代码质量与性能。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。