Back to home page

OSCL-LXR

 
 

    


0001 .. include:: ../disclaimer-zh_CN.rst
0002 
0003 :Original: Documentation/core-api/assoc_array.rst
0004 
0005 :翻译:
0006 
0007  司延腾 Yanteng Si <siyanteng@loongson.cn>
0008 
0009 :校译:
0010 
0011 
0012 
0013 .. _cn_core-api_assoc_array:
0014 
0015 ==================
0016 通用关联数组的实现
0017 ==================
0018 
0019 简介
0020 ====
0021 
0022 这个关联数组的实现是一个具有以下属性的对象容器:
0023 
0024 1. 对象是不透明的指针。该实现不关心它们指向哪里(如果有的话)或它们指向什么(如果有的
0025    话)。
0026 
0027    .. note::
0028 
0029       指向对象的指针 *必须* 在最小有效位为零。
0030 
0031 2. 对象不需要包含供数组使用的链接块。这允许一个对象同时位于多个数组中。相反,数组是
0032    由指向对象的元数据块组成的。
0033 
0034 3. 对象需要索引键来定位它们在阵列中的位置。
0035 
0036 4. 索引键必须是唯一的。插入一个与已经在数组中的且具有相同键值的对象将取代旧的对象。
0037 
0038 5. 索引键可以是任何长度,也可以是不同的长度。
0039 
0040 6. 索引键应该在早期就对长度进行编码,即在任何由于长度引起的变化出现之前。
0041 
0042 7. 索引键可以包括一个哈希值,以便将对象分散到整个数组中。
0043 
0044 8. 该数组可以迭代。对象不一定会按索引键的顺序出现。
0045 
0046 9.  数组可以在被修改的时候进行迭代,只要RCU的读锁被迭代器持有。然而,请注意,在这种情
0047     况下,一些对象可能会被看到不止一次。如果这是个问题,迭代器应该锁定以防止修改。然
0048     而,除非删除,否则对象不会被错过。
0049 
0050 10. 数组中的对象可以通过其索引键进行查询。
0051 
0052 11. 当数组被修改时,对象可以被查询,前提是进行查询的线程持有RCU的读锁。
0053 
0054 该实现在内部使用了一棵由16个指针节点组成的树,这些节点在每一层都由索引键的小数点进行索
0055 引,其方式与基数树相同。为了提高内存效率,可以放置快捷键,以跳过本来是一系列单占节点的地
0056 方。此外,节点将叶子对象指针打包到节点的空闲空间中,而不是做一个额外的分支,直到有对象
0057 需要被添加到一个完整的节点中。
0058 
0059 公用API
0060 =======
0061 
0062 公用API可以在 ``<linux/assoc_array.h>`` 中找到。关联数组的根是以下结构::
0063 
0064     struct assoc_array {
0065             ...
0066     };
0067 
0068 该代码是通过启用 ``CONFIG_ASSOCIATIVE_ARRAY`` 来选择的,以::
0069 
0070     ./script/config -e ASSOCIATIVE_ARRAY
0071 
0072 
0073 编辑脚本
0074 --------
0075 
0076 插入和删除功能会产生一个“编辑脚本”,以后可以应用这个脚本来实现更改,而不会造成 ``ENOMEM``
0077 风险。这保留了将被安装在内部树中的预分配的元数据块,并跟踪应用脚本时将从树中删除的元数
0078 据块。
0079 
0080 在脚本应用后,这也被用来跟踪死块和死对象,以便以后可以释放它们。释放是在RCU宽限期过后
0081 进行的--因此允许访问功能在RCU读锁下进行。
0082 
0083 脚本在API之外显示为一个类型为::
0084 
0085     struct assoc_array_edit;
0086 
0087 有两个处理脚本的功能:
0088 
0089 1. 应用一个编辑脚本::
0090 
0091     void assoc_array_apply_edit(struct assoc_array_edit *edit);
0092 
0093 这将执行编辑功能,插值各种写屏障,以允许在RCU读锁下的访问继续进行。然后,编辑脚本将被
0094 传递给 ``call_rcu()`` ,以释放它和它所指向的任何死的东西。
0095 
0096 2. Cancel an edit script::
0097 
0098     void assoc_array_cancel_edit(struct assoc_array_edit *edit);
0099 
0100 这将立即释放编辑脚本和所有预分配的内存。如果这是为了插入,新的对象不会被这个函数释放,
0101 而是必须由调用者释放。
0102 
0103 这些函数保证不会失败。
0104 
0105 
0106 操作表
0107 ------
0108 
0109 各种功能采用了一个操作表::
0110 
0111     struct assoc_array_ops {
0112             ...
0113     };
0114 
0115 这指出了一些方法,所有这些方法都需要提供:
0116 
0117 1. 从调用者数据中获取索引键的一个块::
0118 
0119     unsigned long (*get_key_chunk)(const void *index_key, int level);
0120 
0121 这应该返回一个由调用者提供的索引键的块,从level参数给出的 *比特* 位置开始。level参数将
0122 是 ``ASSOC_ARRAY_KEY_CHUNK_SIZE`` 的倍数,该函数应返回 ``ASSOC_ARRAY_KEY_CHUNK_SIZE``
0123 位。不可能出现错误。
0124 
0125 
0126 2. 获取一个对象的索引键的一个块::
0127 
0128     unsigned long (*get_object_key_chunk)(const void *object, int level);
0129 
0130 和前面的函数一样,但是从数组中的一个对象而不是从调用者提供的索引键中获取数据。
0131 
0132 
0133 3. 看看这是否是我们要找的对象::
0134 
0135     bool (*compare_object)(const void *object, const void *index_key);
0136 
0137 将对象与一个索引键进行比较,如果匹配则返回 ``true`` ,不匹配则返回 ``false`` 。
0138 
0139 
0140 4. 对两个对象的索引键进行比较::
0141 
0142     int (*diff_objects)(const void *object, const void *index_key);
0143 
0144 返回指定对象的索引键与给定索引键不同的比特位置,如果它们相同,则返回-1。
0145 
0146 
0147 5. 释放一个对象::
0148 
0149     void (*free_object)(void *object);
0150 
0151 释放指定的对象。注意,这可能是在调用 ``assoc_array_apply_edit()`` 后的一个RCU宽限期内
0152 调用的,所以在模块卸载时可能需要 ``synchronize_rcu()`` 。
0153 
0154 
0155 操控函数
0156 --------
0157 
0158 有一些函数用于操控关联数组:
0159 
0160 1. 初始化一个关联数组::
0161 
0162     void assoc_array_init(struct assoc_array *array);
0163 
0164 这将初始化一个关联数组的基础结构。它不会失败。
0165 
0166 
0167 2. 在一个关联数组中插入/替换一个对象::
0168 
0169     struct assoc_array_edit *
0170     assoc_array_insert(struct assoc_array *array,
0171                        const struct assoc_array_ops *ops,
0172                        const void *index_key,
0173                        void *object);
0174 
0175 这将把给定的对象插入数组中。注意,指针的最小有效位必须是0,因为它被用来在内部标记指针的类
0176 型。
0177 
0178 如果该键已经存在一个对象,那么它将被新的对象所取代,旧的对象将被自动释放。
0179 
0180 ``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。
0181 
0182 这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足的错误,会
0183 返回 ``-ENOMEM`` 。
0184 
0185 调用者应专门锁定数组的其他修改器。
0186 
0187 
0188 3. 从一个关联数组中删除一个对象::
0189 
0190     struct assoc_array_edit *
0191     assoc_array_delete(struct assoc_array *array,
0192                        const struct assoc_array_ops *ops,
0193                        const void *index_key);
0194 
0195 这将从数组中删除一个符合指定数据的对象。
0196 
0197 ``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。
0198 
0199 这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。 ``-ENOMEM`` 在出现内存不足
0200 的错误时返回。如果在数组中没有找到指定的对象,将返回 ``NULL`` 。
0201 
0202 调用者应该对数组的其他修改者进行专门锁定。
0203 
0204 
0205 4. 从一个关联数组中删除所有对象::
0206 
0207     struct assoc_array_edit *
0208     assoc_array_clear(struct assoc_array *array,
0209                       const struct assoc_array_ops *ops);
0210 
0211 这个函数删除了一个关联数组中的所有对象,使其完全为空。
0212 
0213 这个函数没有对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足
0214 的错误,则返回 ``-ENOMEM`` 。
0215 
0216 调用者应专门锁定数组的其他修改者。
0217 
0218 
0219 5. 销毁一个关联数组,删除所有对象::
0220 
0221     void assoc_array_destroy(struct assoc_array *array,
0222                              const struct assoc_array_ops *ops);
0223 
0224 这将破坏关联数组的内容,使其完全为空。在这个函数销毁数组的同时,不允许另一个线程在RCU读锁
0225 下遍历数组,因为在内存释放时不执行RCU延迟,这需要分配内存。
0226 
0227 调用者应该专门针对数组的其他修改者和访问者进行锁定。
0228 
0229 
0230 6. 垃圾回收一个关联数组::
0231 
0232     int assoc_array_gc(struct assoc_array *array,
0233                        const struct assoc_array_ops *ops,
0234                        bool (*iterator)(void *object, void *iterator_data),
0235                        void *iterator_data);
0236 
0237 这是对一个关联数组中的对象进行迭代,并将每个对象传递给 ``iterator()`` 。如果 ``iterator()`` 返回
0238 true,该对象被保留。如果它返回 ``false`` ,该对象将被释放。如果 ``iterator()`` 函数返回 ``true`` ,它必须
0239 在返回之前对该对象进行适当的 ``refcount`` 递增。
0240 
0241 如果可能的话,内部树将被打包下来,作为迭代的一部分,以减少其中的节点数量。
0242 
0243 ``iterator_data`` 被直接传递给 ``iterator()`` ,否则会被函数忽略。
0244 
0245 如果成功,该函数将返回 ``0`` ,如果没有足够的内存,则返回 ``-ENOMEM`` 。
0246 
0247 在这个函数执行过程中,其他线程有可能在RCU读锁下迭代或搜索阵列。调用者应该专门针对数组的其他
0248 修改者进行锁定。
0249 
0250 
0251 访问函数
0252 --------
0253 
0254 有两个函数用于访问一个关联数组:
0255 
0256 1. 遍历一个关联数组中的所有对象::
0257 
0258     int assoc_array_iterate(const struct assoc_array *array,
0259                             int (*iterator)(const void *object,
0260                                             void *iterator_data),
0261                             void *iterator_data);
0262 
0263 这将数组中的每个对象传递给迭代器回调函数。 ``iterator_data`` 是该函数的私有数据。
0264 
0265 在数组被修改的同时,可以在数组上使用这个方法,前提是RCU读锁被持有。在这种情况下,迭代函数有
0266 可能两次看到某些对象。如果这是个问题,那么修改应该被锁定。然而,迭代算法不应该错过任何对象。
0267 
0268 如果数组中没有对象,该函数将返回 ``0`` ,否则将返回最后一次调用的迭代器函数的结果。如果对迭代函数
0269 的任何调用导致非零返回,迭代立即停止。
0270 
0271 
0272 2. 在一个关联数组中寻找一个对象::
0273 
0274     void *assoc_array_find(const struct assoc_array *array,
0275                            const struct assoc_array_ops *ops,
0276                            const void *index_key);
0277 
0278 这将直接穿过数组的内部树,到达索引键所指定的对象。
0279 
0280 这个函数可以在修改数组的同时用在数组上,前提是RCU读锁被持有。
0281 
0282 如果找到对象,该函数将返回对象(并将 ``*_type`` 设置为对象的类型),如果没有找到对象,将返回 ``NULL`` 。
0283 
0284 
0285 索引键形式
0286 ----------
0287 
0288 索引键可以是任何形式的,但是由于算法没有被告知键有多长,所以强烈建议在任何由于长度而产生的变化
0289 对比较产生影响之前,索引键应该很早就包括其长度。
0290 
0291 这将导致具有不同长度键的叶子相互分散,而具有相同长度键的叶子则聚集在一起。
0292 
0293 我们还建议索引键以键的其余部分的哈希值开始,以最大限度地提高整个键空间的散布情况。
0294 
0295 分散性越好,内部树就越宽,越低。
0296 
0297 分散性差并不是一个太大的问题,因为有快捷键,节点可以包含叶子和元数据指针的混合物。
0298 
0299 索引键是以机器字的块状来读取的。每个块被细分为每层一个nibble(4比特),所以在32位CPU上这适合8层,
0300 在64位CPU上适合16层。除非散布情况真的很差,否则不太可能有超过一个字的任何特定索引键需要被使用。
0301 
0302 
0303 内部工作机制
0304 ============
0305 
0306 关联数组数据结构有一个内部树。这个树由两种类型的元数据块构成:节点和快捷键。
0307 
0308 一个节点是一个槽的数组。每个槽可以包含以下四种东西之一:
0309 
0310 * 一个NULL的指针,表示槽是空的。
0311 * 一个指向对象(叶子)的指针。
0312 * 一个指向下一级节点的指针。
0313 * 一个指向快捷键的指针。
0314 
0315 
0316 基本的内部树形布局
0317 ------------------
0318 
0319 暂时不考虑快捷键,节点形成一个多级树。索引键空间被树上的节点严格细分,节点出现在固定的层次上。例如::
0320 
0321  Level: 0               1               2               3
0322         =============== =============== =============== ===============
0323                                                         NODE D
0324                         NODE B          NODE C  +------>+---+
0325                 +------>+---+   +------>+---+   |       | 0 |
0326         NODE A  |       | 0 |   |       | 0 |   |       +---+
0327         +---+   |       +---+   |       +---+   |       :   :
0328         | 0 |   |       :   :   |       :   :   |       +---+
0329         +---+   |       +---+   |       +---+   |       | f |
0330         | 1 |---+       | 3 |---+       | 7 |---+       +---+
0331         +---+           +---+           +---+
0332         :   :           :   :           | 8 |---+
0333         +---+           +---+           +---+   |       NODE E
0334         | e |---+       | f |           :   :   +------>+---+
0335         +---+   |       +---+           +---+           | 0 |
0336         | f |   |                       | f |           +---+
0337         +---+   |                       +---+           :   :
0338                 |       NODE F                          +---+
0339                 +------>+---+                           | f |
0340                         | 0 |           NODE G          +---+
0341                         +---+   +------>+---+
0342                         :   :   |       | 0 |
0343                         +---+   |       +---+
0344                         | 6 |---+       :   :
0345                         +---+           +---+
0346                         :   :           | f |
0347                         +---+           +---+
0348                         | f |
0349                         +---+
0350 
0351 在上述例子中,有7个节点(A-G),每个节点有16个槽(0-f)。假设树上没有其他元数据节点,那么密钥空间
0352 是这样划分的::
0353 
0354     KEY PREFIX      NODE
0355     ==========      ====
0356     137*            D
0357     138*            E
0358     13[0-69-f]*     C
0359     1[0-24-f]*      B
0360     e6*             G
0361     e[0-57-f]*      F
0362     [02-df]*        A
0363 
0364 因此,例如,具有以下示例索引键的键将在适当的节点中被找到::
0365 
0366     INDEX KEY       PREFIX  NODE
0367     =============== ======= ====
0368     13694892892489  13      C
0369     13795289025897  137     D
0370     13889dde88793   138     E
0371     138bbb89003093  138     E
0372     1394879524789   12      C
0373     1458952489      1       B
0374     9431809de993ba  -       A
0375     b4542910809cd   -       A
0376     e5284310def98   e       F
0377     e68428974237    e6      G
0378     e7fffcbd443     e       F
0379     f3842239082     -       A
0380 
0381 为了节省内存,如果一个节点可以容纳它的那部分键空间中的所有叶子,那么这个节点将有所有这些叶子,而不
0382 会有任何元数据指针——即使其中一些叶子想在同一个槽中。
0383 
0384 一个节点可以包含叶子和元数据指针的异质性混合。元数据指针必须在与它们的关键空间的细分相匹配的槽中。
0385 叶子可以在任何没有被元数据指针占据的槽中。保证一个节点中没有一个叶子会与元数据指针占据的槽相匹配。
0386 如果元数据指针在那里,任何键与元数据键前缀相匹配的叶必须在元数据指针指向的子树中。
0387 
0388 在上面的索引键的例子列表中,节点A将包含::
0389 
0390     SLOT    CONTENT         INDEX KEY (PREFIX)
0391     ====    =============== ==================
0392     1       PTR TO NODE B   1*
0393     any     LEAF            9431809de993ba
0394     any     LEAF            b4542910809cd
0395     e       PTR TO NODE F   e*
0396     any     LEAF            f3842239082
0397 
0398 和节点B::
0399 
0400     3   PTR TO NODE C   13*
0401     any LEAF            1458952489
0402 
0403 
0404 快捷键
0405 ---------
0406 
0407 快捷键是跳过一块键空间的元数据记录。快捷键是一系列通过层次上升的单占节点的替代物。快捷键的存在是
0408 为了节省内存和加快遍历速度。
0409 
0410 树的根部有可能是一个快捷键——比如说,树至少包含17个节点,都有键前缀 ``1111`` 。插入算法将插入一个快捷键,
0411 以单次跳过 ``1111`` 的键位,并到达第四层,在这里,这些键位实际上变得不同。
0412 
0413 
0414 拆分和合并节点
0415 ------------------------------
0416 
0417 每个节点的最大容量为16个叶子和元数据指针。如果插入算法发现它正试图将一个第17个对象插入到一个节点中,
0418 该节点将被拆分,使得至少两个在该层有一个共同的关键段的叶子最终在一个单独的节点中,该共同的关键段的根
0419 在该槽上。
0420 
0421 如果一个完整的节点中的叶子和被插入的叶子足够相似,那么就会在树中插入一个快捷键。
0422 
0423 当根植于某个节点的子树中的对象数量下降到16个或更少时,那么该子树将被合并成一个单独的节点——如果可能的
0424 话,这将向根部扩散。
0425 
0426 
0427 非递归式迭代
0428 ------------
0429 
0430 每个节点和快捷键都包含一个指向其父节点的后置指针,以及该父节点中指向它的槽数。非递归迭代使用这些来
0431 通过树的根部进行,前往父节点,槽N+1,以确保在没有堆栈的情况下取得进展。
0432 
0433 然而,反向指针使得同时改变和迭代变得很棘手。
0434 
0435 
0436 同时改变和迭代
0437 --------------
0438 
0439 有一些情况需要考虑:
0440 
0441 1. 简单的插入/替换。这涉及到简单地将一个NULL或旧的匹配叶子的指针替换为屏障后的新叶子的指针。否则元数
0442    据块不会改变。一个旧的叶子直到RCU宽限期过后才会被释放。
0443 
0444 2. 简单删除。这只是涉及到清除一个旧的匹配叶子。元数据块不会有其他变化。旧的叶子直到RCU宽限期之后才会
0445    被释放。
0446 
0447 3. 插入,替换我们还没有进入的子树的一部分。这可能涉及到替换该子树的一部分——但这不会影响迭代,因为我们
0448    还没有到达它的指针,而且祖先块也不会被替换(这些块的布局不会改变)。
0449 
0450 4. 插入替换了我们正在处理的节点。这不是一个问题,因为我们已经通过了锚定指针,直到我们跟随后面的指针才
0451    会切换到新的布局上——这时我们已经检查了被替换节点的叶子(在跟随任何元数据指针之前,我们会迭代一个节
0452    点的所有叶子)。
0453 
0454    然而,我们可能会重新看到一些叶子,这些叶子已经被分割成一个新的分支,而这个分支的位置比我们之前的位
0455    置更远。
0456 
0457 5. 插入替换了我们正在处理的依赖分支的节点。这不会影响到我们,直到我们跟随后面的指针。与(4)类似。
0458 
0459 6. 删掉我们下面的一个分支。这不会影响我们,因为在我们看到新节点之前,回溯指针会让我们回到新节点的父节
0460    点。整个崩溃的子树被扔掉了,没有任何变化——而且仍然会在同一个槽上生根,所以我们不应该第二次处理它,
0461    因为我们会回到槽+1。
0462 
0463 .. note::
0464 
0465     在某些情况下,我们需要同时改变一个节点的父指针和父槽指针(比如说,我们在它之前插入了另一个节点,
0466     并把它往上移了一层)。我们不能在不锁定读取的情况下这样做——所以我们必须同时替换该节点。
0467 
0468     然而,当我们把一个快捷键改成一个节点时,这不是一个问题,因为快捷键只有一个槽,所以当向后遍
0469     历一个槽时,不会使用父槽号。这意味着先改变槽位号是可以的——只要使用适当的屏障来确保父槽位号在后
0470     退指针之后被读取。
0471 
0472 过时的块和叶子在RCU宽限期过后会被释放,所以只要任何进行遍历或迭代的人持有RCU读锁,旧的上层建筑就不
0473 应该在他们身上消失。