1.. include:: ../disclaimer-zh_CN.rst 2 3:Original: Documentation/core-api/assoc_array.rst 4 5:翻译: 6 7 司延腾 Yanteng Si <siyanteng@loongson.cn> 8 9:校译: 10 11 12 13.. _cn_core-api_assoc_array: 14 15================== 16通用关联数组的实现 17================== 18 19简介 20==== 21 22这个关联数组的实现是一个具有以下属性的对象容器: 23 241. 对象是不透明的指针。该实现不关心它们指向哪里(如果有的话)或它们指向什么(如果有的 25 话)。 26 27 .. note:: 28 29 指向对象的指针 *必须* 在最小有效位为零。 30 312. 对象不需要包含供数组使用的链接块。这允许一个对象同时位于多个数组中。相反,数组是 32 由指向对象的元数据块组成的。 33 343. 对象需要索引键来定位它们在阵列中的位置。 35 364. 索引键必须是唯一的。插入一个与已经在数组中的且具有相同键值的对象将取代旧的对象。 37 385. 索引键可以是任何长度,也可以是不同的长度。 39 406. 索引键应该在早期就对长度进行编码,即在任何由于长度引起的变化出现之前。 41 427. 索引键可以包括一个哈希值,以便将对象分散到整个数组中。 43 448. 该数组可以迭代。对象不一定会按索引键的顺序出现。 45 469. 数组可以在被修改的时候进行迭代,只要RCU的读锁被迭代器持有。然而,请注意,在这种情 47 况下,一些对象可能会被看到不止一次。如果这是个问题,迭代器应该锁定以防止修改。然 48 而,除非删除,否则对象不会被错过。 49 5010. 数组中的对象可以通过其索引键进行查询。 51 5211. 当数组被修改时,对象可以被查询,前提是进行查询的线程持有RCU的读锁。 53 54该实现在内部使用了一棵由16个指针节点组成的树,这些节点在每一层都由索引键的小数点进行索 55引,其方式与基数树相同。为了提高内存效率,可以放置快捷键,以跳过本来是一系列单占节点的地 56方。此外,节点将叶子对象指针打包到节点的空闲空间中,而不是做一个额外的分支,直到有对象 57需要被添加到一个完整的节点中。 58 59公用API 60======= 61 62公用API可以在 ``<linux/assoc_array.h>`` 中找到。关联数组的根是以下结构:: 63 64 struct assoc_array { 65 ... 66 }; 67 68该代码是通过启用 ``CONFIG_ASSOCIATIVE_ARRAY`` 来选择的,以:: 69 70 ./script/config -e ASSOCIATIVE_ARRAY 71 72 73编辑脚本 74-------- 75 76插入和删除功能会产生一个“编辑脚本”,以后可以应用这个脚本来实现更改,而不会造成 ``ENOMEM`` 77风险。这保留了将被安装在内部树中的预分配的元数据块,并跟踪应用脚本时将从树中删除的元数 78据块。 79 80在脚本应用后,这也被用来跟踪死块和死对象,以便以后可以释放它们。释放是在RCU宽限期过后 81进行的--因此允许访问功能在RCU读锁下进行。 82 83脚本在API之外显示为一个类型为:: 84 85 struct assoc_array_edit; 86 87有两个处理脚本的功能: 88 891. 应用一个编辑脚本:: 90 91 void assoc_array_apply_edit(struct assoc_array_edit *edit); 92 93这将执行编辑功能,插值各种写屏障,以允许在RCU读锁下的访问继续进行。然后,编辑脚本将被 94传递给 ``call_rcu()`` ,以释放它和它所指向的任何死的东西。 95 962. Cancel an edit script:: 97 98 void assoc_array_cancel_edit(struct assoc_array_edit *edit); 99 100这将立即释放编辑脚本和所有预分配的内存。如果这是为了插入,新的对象不会被这个函数释放, 101而是必须由调用者释放。 102 103这些函数保证不会失败。 104 105 106操作表 107------ 108 109各种功能采用了一个操作表:: 110 111 struct assoc_array_ops { 112 ... 113 }; 114 115这指出了一些方法,所有这些方法都需要提供: 116 1171. 从调用者数据中获取索引键的一个块:: 118 119 unsigned long (*get_key_chunk)(const void *index_key, int level); 120 121这应该返回一个由调用者提供的索引键的块,从level参数给出的 *比特* 位置开始。level参数将 122是 ``ASSOC_ARRAY_KEY_CHUNK_SIZE`` 的倍数,该函数应返回 ``ASSOC_ARRAY_KEY_CHUNK_SIZE`` 123位。不可能出现错误。 124 125 1262. 获取一个对象的索引键的一个块:: 127 128 unsigned long (*get_object_key_chunk)(const void *object, int level); 129 130和前面的函数一样,但是从数组中的一个对象而不是从调用者提供的索引键中获取数据。 131 132 1333. 看看这是否是我们要找的对象:: 134 135 bool (*compare_object)(const void *object, const void *index_key); 136 137将对象与一个索引键进行比较,如果匹配则返回 ``true`` ,不匹配则返回 ``false`` 。 138 139 1404. 对两个对象的索引键进行比较:: 141 142 int (*diff_objects)(const void *object, const void *index_key); 143 144返回指定对象的索引键与给定索引键不同的比特位置,如果它们相同,则返回-1。 145 146 1475. 释放一个对象:: 148 149 void (*free_object)(void *object); 150 151释放指定的对象。注意,这可能是在调用 ``assoc_array_apply_edit()`` 后的一个RCU宽限期内 152调用的,所以在模块卸载时可能需要 ``synchronize_rcu()`` 。 153 154 155操控函数 156-------- 157 158有一些函数用于操控关联数组: 159 1601. 初始化一个关联数组:: 161 162 void assoc_array_init(struct assoc_array *array); 163 164这将初始化一个关联数组的基础结构。它不会失败。 165 166 1672. 在一个关联数组中插入/替换一个对象:: 168 169 struct assoc_array_edit * 170 assoc_array_insert(struct assoc_array *array, 171 const struct assoc_array_ops *ops, 172 const void *index_key, 173 void *object); 174 175这将把给定的对象插入数组中。注意,指针的最小有效位必须是0,因为它被用来在内部标记指针的类 176型。 177 178如果该键已经存在一个对象,那么它将被新的对象所取代,旧的对象将被自动释放。 179 180``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。 181 182这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足的错误,会 183返回 ``-ENOMEM`` 。 184 185调用者应专门锁定数组的其他修改器。 186 187 1883. 从一个关联数组中删除一个对象:: 189 190 struct assoc_array_edit * 191 assoc_array_delete(struct assoc_array *array, 192 const struct assoc_array_ops *ops, 193 const void *index_key); 194 195这将从数组中删除一个符合指定数据的对象。 196 197``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。 198 199这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。 ``-ENOMEM`` 在出现内存不足 200的错误时返回。如果在数组中没有找到指定的对象,将返回 ``NULL`` 。 201 202调用者应该对数组的其他修改者进行专门锁定。 203 204 2054. 从一个关联数组中删除所有对象:: 206 207 struct assoc_array_edit * 208 assoc_array_clear(struct assoc_array *array, 209 const struct assoc_array_ops *ops); 210 211这个函数删除了一个关联数组中的所有对象,使其完全为空。 212 213这个函数没有对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足 214的错误,则返回 ``-ENOMEM`` 。 215 216调用者应专门锁定数组的其他修改者。 217 218 2195. 销毁一个关联数组,删除所有对象:: 220 221 void assoc_array_destroy(struct assoc_array *array, 222 const struct assoc_array_ops *ops); 223 224这将破坏关联数组的内容,使其完全为空。在这个函数销毁数组的同时,不允许另一个线程在RCU读锁 225下遍历数组,因为在内存释放时不执行RCU延迟,这需要分配内存。 226 227调用者应该专门针对数组的其他修改者和访问者进行锁定。 228 229 2306. 垃圾回收一个关联数组:: 231 232 int assoc_array_gc(struct assoc_array *array, 233 const struct assoc_array_ops *ops, 234 bool (*iterator)(void *object, void *iterator_data), 235 void *iterator_data); 236 237这是对一个关联数组中的对象进行迭代,并将每个对象传递给 ``iterator()`` 。如果 ``iterator()`` 返回 238true,该对象被保留。如果它返回 ``false`` ,该对象将被释放。如果 ``iterator()`` 函数返回 ``true`` ,它必须 239在返回之前对该对象进行适当的 ``refcount`` 递增。 240 241如果可能的话,内部树将被打包下来,作为迭代的一部分,以减少其中的节点数量。 242 243``iterator_data`` 被直接传递给 ``iterator()`` ,否则会被函数忽略。 244 245如果成功,该函数将返回 ``0`` ,如果没有足够的内存,则返回 ``-ENOMEM`` 。 246 247在这个函数执行过程中,其他线程有可能在RCU读锁下迭代或搜索阵列。调用者应该专门针对数组的其他 248修改者进行锁定。 249 250 251访问函数 252-------- 253 254有两个函数用于访问一个关联数组: 255 2561. 遍历一个关联数组中的所有对象:: 257 258 int assoc_array_iterate(const struct assoc_array *array, 259 int (*iterator)(const void *object, 260 void *iterator_data), 261 void *iterator_data); 262 263这将数组中的每个对象传递给迭代器回调函数。 ``iterator_data`` 是该函数的私有数据。 264 265在数组被修改的同时,可以在数组上使用这个方法,前提是RCU读锁被持有。在这种情况下,迭代函数有 266可能两次看到某些对象。如果这是个问题,那么修改应该被锁定。然而,迭代算法不应该错过任何对象。 267 268如果数组中没有对象,该函数将返回 ``0`` ,否则将返回最后一次调用的迭代器函数的结果。如果对迭代函数 269的任何调用导致非零返回,迭代立即停止。 270 271 2722. 在一个关联数组中寻找一个对象:: 273 274 void *assoc_array_find(const struct assoc_array *array, 275 const struct assoc_array_ops *ops, 276 const void *index_key); 277 278这将直接穿过数组的内部树,到达索引键所指定的对象。 279 280这个函数可以在修改数组的同时用在数组上,前提是RCU读锁被持有。 281 282如果找到对象,该函数将返回对象(并将 ``*_type`` 设置为对象的类型),如果没有找到对象,将返回 ``NULL`` 。 283 284 285索引键形式 286---------- 287 288索引键可以是任何形式的,但是由于算法没有被告知键有多长,所以强烈建议在任何由于长度而产生的变化 289对比较产生影响之前,索引键应该很早就包括其长度。 290 291这将导致具有不同长度键的叶子相互分散,而具有相同长度键的叶子则聚集在一起。 292 293我们还建议索引键以键的其余部分的哈希值开始,以最大限度地提高整个键空间的散布情况。 294 295分散性越好,内部树就越宽,越低。 296 297分散性差并不是一个太大的问题,因为有快捷键,节点可以包含叶子和元数据指针的混合物。 298 299索引键是以机器字的块状来读取的。每个块被细分为每层一个nibble(4比特),所以在32位CPU上这适合8层, 300在64位CPU上适合16层。除非散布情况真的很差,否则不太可能有超过一个字的任何特定索引键需要被使用。 301 302 303内部工作机制 304============ 305 306关联数组数据结构有一个内部树。这个树由两种类型的元数据块构成:节点和快捷键。 307 308一个节点是一个槽的数组。每个槽可以包含以下四种东西之一: 309 310* 一个NULL的指针,表示槽是空的。 311* 一个指向对象(叶子)的指针。 312* 一个指向下一级节点的指针。 313* 一个指向快捷键的指针。 314 315 316基本的内部树形布局 317------------------ 318 319暂时不考虑快捷键,节点形成一个多级树。索引键空间被树上的节点严格细分,节点出现在固定的层次上。例如:: 320 321 Level: 0 1 2 3 322 =============== =============== =============== =============== 323 NODE D 324 NODE B NODE C +------>+---+ 325 +------>+---+ +------>+---+ | | 0 | 326 NODE A | | 0 | | | 0 | | +---+ 327 +---+ | +---+ | +---+ | : : 328 | 0 | | : : | : : | +---+ 329 +---+ | +---+ | +---+ | | f | 330 | 1 |---+ | 3 |---+ | 7 |---+ +---+ 331 +---+ +---+ +---+ 332 : : : : | 8 |---+ 333 +---+ +---+ +---+ | NODE E 334 | e |---+ | f | : : +------>+---+ 335 +---+ | +---+ +---+ | 0 | 336 | f | | | f | +---+ 337 +---+ | +---+ : : 338 | NODE F +---+ 339 +------>+---+ | f | 340 | 0 | NODE G +---+ 341 +---+ +------>+---+ 342 : : | | 0 | 343 +---+ | +---+ 344 | 6 |---+ : : 345 +---+ +---+ 346 : : | f | 347 +---+ +---+ 348 | f | 349 +---+ 350 351在上述例子中,有7个节点(A-G),每个节点有16个槽(0-f)。假设树上没有其他元数据节点,那么密钥空间 352是这样划分的:: 353 354 KEY PREFIX NODE 355 ========== ==== 356 137* D 357 138* E 358 13[0-69-f]* C 359 1[0-24-f]* B 360 e6* G 361 e[0-57-f]* F 362 [02-df]* A 363 364因此,例如,具有以下示例索引键的键将在适当的节点中被找到:: 365 366 INDEX KEY PREFIX NODE 367 =============== ======= ==== 368 13694892892489 13 C 369 13795289025897 137 D 370 13889dde88793 138 E 371 138bbb89003093 138 E 372 1394879524789 12 C 373 1458952489 1 B 374 9431809de993ba - A 375 b4542910809cd - A 376 e5284310def98 e F 377 e68428974237 e6 G 378 e7fffcbd443 e F 379 f3842239082 - A 380 381为了节省内存,如果一个节点可以容纳它的那部分键空间中的所有叶子,那么这个节点将有所有这些叶子,而不 382会有任何元数据指针——即使其中一些叶子想在同一个槽中。 383 384一个节点可以包含叶子和元数据指针的异质性混合。元数据指针必须在与它们的关键空间的细分相匹配的槽中。 385叶子可以在任何没有被元数据指针占据的槽中。保证一个节点中没有一个叶子会与元数据指针占据的槽相匹配。 386如果元数据指针在那里,任何键与元数据键前缀相匹配的叶必须在元数据指针指向的子树中。 387 388在上面的索引键的例子列表中,节点A将包含:: 389 390 SLOT CONTENT INDEX KEY (PREFIX) 391 ==== =============== ================== 392 1 PTR TO NODE B 1* 393 any LEAF 9431809de993ba 394 any LEAF b4542910809cd 395 e PTR TO NODE F e* 396 any LEAF f3842239082 397 398和节点B:: 399 400 3 PTR TO NODE C 13* 401 any LEAF 1458952489 402 403 404快捷键 405--------- 406 407快捷键是跳过一块键空间的元数据记录。快捷键是一系列通过层次上升的单占节点的替代物。快捷键的存在是 408为了节省内存和加快遍历速度。 409 410树的根部有可能是一个快捷键——比如说,树至少包含17个节点,都有键前缀 ``1111`` 。插入算法将插入一个快捷键, 411以单次跳过 ``1111`` 的键位,并到达第四层,在这里,这些键位实际上变得不同。 412 413 414拆分和合并节点 415------------------------------ 416 417每个节点的最大容量为16个叶子和元数据指针。如果插入算法发现它正试图将一个第17个对象插入到一个节点中, 418该节点将被拆分,使得至少两个在该层有一个共同的关键段的叶子最终在一个单独的节点中,该共同的关键段的根 419在该槽上。 420 421如果一个完整的节点中的叶子和被插入的叶子足够相似,那么就会在树中插入一个快捷键。 422 423当根植于某个节点的子树中的对象数量下降到16个或更少时,那么该子树将被合并成一个单独的节点——如果可能的 424话,这将向根部扩散。 425 426 427非递归式迭代 428------------ 429 430每个节点和快捷键都包含一个指向其父节点的后置指针,以及该父节点中指向它的槽数。非递归迭代使用这些来 431通过树的根部进行,前往父节点,槽N+1,以确保在没有堆栈的情况下取得进展。 432 433然而,反向指针使得同时改变和迭代变得很棘手。 434 435 436同时改变和迭代 437-------------- 438 439有一些情况需要考虑: 440 4411. 简单的插入/替换。这涉及到简单地将一个NULL或旧的匹配叶子的指针替换为屏障后的新叶子的指针。否则元数 442 据块不会改变。一个旧的叶子直到RCU宽限期过后才会被释放。 443 4442. 简单删除。这只是涉及到清除一个旧的匹配叶子。元数据块不会有其他变化。旧的叶子直到RCU宽限期之后才会 445 被释放。 446 4473. 插入,替换我们还没有进入的子树的一部分。这可能涉及到替换该子树的一部分——但这不会影响迭代,因为我们 448 还没有到达它的指针,而且祖先块也不会被替换(这些块的布局不会改变)。 449 4504. 插入替换了我们正在处理的节点。这不是一个问题,因为我们已经通过了锚定指针,直到我们跟随后面的指针才 451 会切换到新的布局上——这时我们已经检查了被替换节点的叶子(在跟随任何元数据指针之前,我们会迭代一个节 452 点的所有叶子)。 453 454 然而,我们可能会重新看到一些叶子,这些叶子已经被分割成一个新的分支,而这个分支的位置比我们之前的位 455 置更远。 456 4575. 插入替换了我们正在处理的依赖分支的节点。这不会影响到我们,直到我们跟随后面的指针。与(4)类似。 458 4596. 删掉我们下面的一个分支。这不会影响我们,因为在我们看到新节点之前,回溯指针会让我们回到新节点的父节 460 点。整个崩溃的子树被扔掉了,没有任何变化——而且仍然会在同一个槽上生根,所以我们不应该第二次处理它, 461 因为我们会回到槽+1。 462 463.. note:: 464 465 在某些情况下,我们需要同时改变一个节点的父指针和父槽指针(比如说,我们在它之前插入了另一个节点, 466 并把它往上移了一层)。我们不能在不锁定读取的情况下这样做——所以我们必须同时替换该节点。 467 468 然而,当我们把一个快捷键改成一个节点时,这不是一个问题,因为快捷键只有一个槽,所以当向后遍 469 历一个槽时,不会使用父槽号。这意味着先改变槽位号是可以的——只要使用适当的屏障来确保父槽位号在后 470 退指针之后被读取。 471 472过时的块和叶子在RCU宽限期过后会被释放,所以只要任何进行遍历或迭代的人持有RCU读锁,旧的上层建筑就不 473应该在他们身上消失。 474