映射的意思映射是什么意思(linux学习20,挺复杂的
前面几节较为详细的讨论了 linux 内核中的“链表”和“队列”数据类型,本节再来说说中的映射(idr)机制。
先来说说映射适合用于解决什么问题
linux 系统中的许多资源都用整数 ID 标识,例如进程 PID,文件描述符 ID 等等。使用一个整数标识资源的确非常简洁,一个整数能够记录表达的信息太有限了,所以整数 ID 的背后常常都有一个结构体与之对应,例如每个进程 ID 都对应着一个巨大的 task_struct 结构体记录着进程的资源。
那么,整数 ID 该如何与结构体对应起来呢?这个问题可以简化为如何将两个整数对应起来。因为只要能根据整数 ID 找到结构体的地址就可以了,而结构体的地址也是一个整数。
要解决这个问题,就需要选择一个数据结构。如果使用数组进行索引,一旦 ID 很大,那数组也必须得很大,这样会占据大量的内存空间,所以数组显然是不合适的。而如果使用链表进行索引,则又会有查找效率问题,毕竟链表只能线性遍历查找。
映射就非常适合解决这样的问题。
什么是映射?
映射其实就是将要保存的数值(例如结构体指针)与另一个数关联起来,所谓的“另一个数”常常被称作“键值”。映射一般至少要支持三个模块
- add(key, value),增加映射
- remove(key),删除映射
- value = lookup(key),根据键查找数值
这么看来,“哈希表”就是一种映射,不过也不是所有映射都需要通过“哈希表”才能实现,映射也可以通过自平衡二叉树(之后的文章会介绍二叉树)实现。
虽然“键值映射”是一个非常宽泛的概念,大多情况下都默认是借助自平衡二叉树实现的。
linux 内核是怎样设计和实现“映射”的?
linux 内核实现映射的目标很简单就是将指针与一个唯一标识数(UID)对应起来。为此,linux 内核不仅实现了映射的三个标准模块外,还提供了自动产生 UID 的模块。这么看来,内核实现的映射更像是一种“定制化”的,而不是通用的映射。
先来看看 linux 内核中用于实现映射的数据结构,请看如下C语言代码
- 50 struct idr_layer { | 51 unsigned long bitmap; / A zero bit means "space here" / | 52 struct idr_layer ary; | 53 int count; / When zero, we can release it / | 54 }; 55 - 56 struct idr { | 57 struct idr_layer ; | 58 struct idr_layer id_free; | 59 int layers; | 60 int id_free_t; | 61 spinlock_t lock; | 62 };
idr_layer 结构很像一个链表,IDR_BITS 和平台相关,在 BITS_PER_LONG == 32 的平台上,IDR_BITS 等于 5,在 BITS_PER_LONG == 64 的平台上,IDR_BITS 等于 6。
也就是说,1IDR_BITS 等于 32 或者 64。以下以 IDR_BITS 等于 5 为例行文。
这么看来,idr_layer 结构体可用下图表示
初始化一个 idr(映射)
初始化一个 idr 非常简单,调用 idr_init() 函数即可,它的C语言代码如下,请看
void idr_init(struct idr idp) { memset(idp, 0, sizeof(struct idr)); spin_lock_init(idp-lock); }
idr_init() 函数只是将 idr 清零,并初始化锁而已,所以在调用 idr_init() 函数之前,需要静态定义或者动态分配一个 idr,例如
struct idr myidr; idr_init(myidr);
分配一个新的 UID
建立好一个新的 idr 后,就可以分配新的 UID 了。这一步 linux 内核是通过 idr_pre_get() 和 idr_get_new() 两个函数完成的。idr_pre_get() 函数的C语言代码如下
105 int idr_pre_get(struct idr idp, gfp_t gfp_mask) - 106 { |- 107 while (idp-id_free_t IDR_FREE_MAX) { || 108 struct idr_layer new; || 109 new = kmem_cache_alloc(idr_layer_cache, gfp_mask); || 110 if (new == NULL) || 111 return (0); || 112 free_layer(idp, new); || 113 } | 114 return 1; | 115 }
idr_pre_get() 函数的核心就是 free_layer() 函数。这里可以看出,即使是 linux 内核的源代码,命名也有可能是不太合适的。 free_layer() 函数并不是如字面意思“释放 idr”,而是将新分配的 new 连接到 idr 的 id_free 成员上,请看
61 static void free_layer(struct idr idp, struct idr_layer p) - 62 { | 63 unsigned long flags; | 64 | 65 / | 66 Depends on the return element being zeroed. | 67 / | 68 spin_lock_irqsave(idp-lock, flags); | 69 __free_layer(idp, p); | 70 spin_unlock_irqrestore(idp-lock, flags); | 71 }
idr_pre_get() 函数执行完毕后,得到的数据结构如下图
idr_pre_get() 就是实际执行获取 UID 的函数了,它的C语言代码如下,请看
306 int idr_get_new(struct idr idp, void ptr, int id) - 307 { | 308 int rv; | 309 | 310 rv = idr_get_new_above_int(idp, ptr, 0); | 311 / | 312 This is a cheap hack until the IDR code can be fixed to | 313 return proper error values. | 314 / |- 315 if (rv 0) { || 316 if (rv == -1) || 317 return -EAGAIN; || 318 else / Will be -3 / || 319 return -ENOSPC; || 320 } | 321 id = rv; | 322 return 0; | 323 }
核心是 idr_get_new_above_int() 函数,继续跟踪
236 static int idr_get_new_above_int(struct idr idp, void ptr, int starting_id) - 237 { | 238 struct idr_layer pa; | 239 int id; | 240 | 241 id = idr_get_empty_slot(idp, starting_id, pa); |- 242 if (id = 0) { || 243 / || 244 Suessfully found an empty slot. Install the user || 245 pointer and mark the slot full. || 246 / || 247 pa-ary = (struct idr_layer )ptr; || 248 pa-count ; || 249 idr_mark_full(pa, id); || 250 } | 251 | 252 return id; | 253 }
发现实际负责申请 UID 的动作由 idr_get_empty_slot() 函数完成,它的C语言代码如下
到这里就非常清楚了, int idr_get_new(struct idr idp, void ptr, int id) 函数执行成功后,将获得一个新的 UID,它会被映射到 ptr,并通过指针参数 id 返回,以后想查询 ptr 指向的地址,只需要知道 UID 就行了。
通过 UID 查找指针地址
根据 UID 查找其对应的指针地址就简单许多了,只需调用 idr_find() 函数即可,它的 C语言代码如下
464 void idr_find(struct idr idp, int id) - 465 { | 466 int n; | 467 struct idr_layer p; | 468 | 469 n = idp-layers IDR_BITS; | 470 p = idp-; | 471 | 472 / Mask off upper bits we don't use for the search. / | 473 id = MAX_ID_MASK; | 474 | 475 if (id = (1 n)) | 476 return NULL; | 477 |- 478 while (n 0 p) { || 479 n -= IDR_BITS; || 480 p = p-ary; || 481 } | 482 return((void )p); | 483 }
可以看出 idr_find() 函数执行成功后,会返回 id 关联的指针地址,如果执行失败,则会返回 NULL。从这里也可以看出,应尽量避免将空指针 NULL 与 UID 映射,否则调用 idr_find() 函数时将无法区分是成功还是失败。
到这里,我们就对 linux 内核中的映射有所了解了,其他相关代码的分析(例如删除一个UID映射 idr_remove()函数)也是类似的,这里就不再赘述了。(实际上,linux 内核关于 idr 的设计与实现还是有一定的复杂度的,这里仅仅只是粗略的先留个大体印象,更详细的讨论以后再介绍了。)
欢迎在评论区一起讨论,质疑。文章都是手打原创(本文部分参考linux内核原理和设计),每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。
比赛规则
- 男足世界杯预选赛亚洲赛程 中国男足世界杯预选
- 雷霆火箭比赛直播:雷霆火箭全场回放
- 中国短道速滑队最新消息 中国短道速滑队再创佳
- 苏炳添今天几点比赛 苏炳添今天比赛时间
- 斯诺克今晚直播比赛 斯诺克今晚直播比赛时间
- 乒乓球女团决赛时间 巴黎奥运会乒乓球女团决赛
- 国乒巴黎奥运会参赛名单 国乒巴黎奥运会参赛名
- 王曼昱晋级全运会乒乓球女单决赛 王曼昱全运会
- 男乒乓球团体赛决赛直播 男乒乓球团体赛决赛直
- 阿尔卡拉斯vs德约科维奇决赛 阿尔卡拉斯vs德约科
- 奥运会乒乓球比赛时间 巴黎奥运会乒乓球比赛时
- 中国男篮奥运落选赛 中国男篮奥运落选赛对手
- 2024斯诺克冠军联赛即将开战 2024斯诺克冠军联赛
- 丁俊晖vs奥沙利文直播 丁俊晖对战奥沙利文战绩
- 西甲国王杯半决赛 西甲国王杯半决赛时间
- 2024年乒乓球比赛赛事 2024年乒乓球比赛时间表