xarray-1-理论和xarray.rst翻译
一、xarray实现原理简介
XArray 本质上是 Linux 内核里对 radix tree/IDR 一类结构的统一升级版。它表面上提供的是“超大稀疏数组”语义,底层实现则是一棵按位分层的多叉树,加上 RCU、标记位、内部条目和范围条目等机制。
实现本质:XArray 是一棵基数为 64 的稀疏分层树,用索引的位段逐层寻址,读路径依赖 RCU 无锁遍历,写路径通过 xa_lock 修改树结构,并利用内部条目、标记摘要和范围条目来提升功能和性能。
再压缩成 4 个关键词:位段寻址、稀疏多叉树、RCU 读、mark/range/internal-entry 增强。
1. XArray 解决什么问题
(1) 用一个整数索引查对象,像数组一样快。
(2) 允许索引很大,但不能真的分配一个巨大连续数组。
(3) 支持稀疏存储,空洞成本低。
(4) 支持高并发查找,尤其是读多写少场景。
(5) 能做范围扫描、按标记扫描,而不是只能精确查一个 key。
所以它既不像普通数组那样要求连续内存,也不像哈希表那样不擅长“找下一个”,更不像链表那样缓存局部性差。
2. 核心思想:按索引位切分的分层树
XArray 底层不是线性数组,而是一棵多叉树。可以把一个索引 index 看成一串二进制位。XArray 每一层取其中固定几位,决定走到哪个子槽。抽象上类似这样:
index bits: [高位 ...][6 bits][6 bits][6 bits] | | | 第1层 第2层 叶子槽
在 64 位机器上,XArray 每层通常用 6 bit 选择一个槽,也就是每个节点有 64 个 slot。所以它本质上是:一棵基数为 64 的稀疏树, 每层消费 6 个索引位, 深度随索引大小增长, 小索引路径短,大索引路径长,但都不需要搬迁已有数据。
这就是它和“可扩容数组”最大的区别:扩容时不是重新分配一个更大的连续数组并拷贝,而是按需长出更高层节点。
3. 顶层结构:xa_head
一个 XArray 对外看是一个结构体,里面最重要的是一个 head 指针。这个 head 可能指向三种东西:
(1) NULL;
(2) 直接存放的条目;
(3) 一个内部节点 xa_node;
如果数组里只有一个很小索引的条目,head 甚至可以直接放那个条目,不一定立刻建树。这是一个常见的小优化。
4. 内部节点 xa_node 是什么
内部节点可以理解成“树的一层”。每个 xa_node 大致包含这些逻辑信息:
slots[]: 每个槽位指向下一层节点,或者直接指向用户条目。
marks: 每个节点会维护整层的标记位摘要,用于快速跳过“不可能命中”的子树.
shift: 表示当前节点负责索引的哪一段位.
count: 记录该节点里当前有多少有效槽,便于删除时回收空节点.
你可以把它想成:
xa_node ├─ shift = 12 ├─ offset = ... ├─ count = ... ├─ slots[64] ├─ marks[3][...]
其中最核心的是 slots[64]。
5. 查找原理
查找 xa_load(xa, index) 的过程本质上就是“按位下钻”。例如:从 xa_head 开始,看当前层的 shift,取 index 对应的那 6 位,选择 slots[offset],继续往下,直到到达叶子条目或空,伪代码可以写成:
entry = xa_head; while (entry is node) { offset = (index >> node->shift) & 0x3f; //6bit entry = node->slots[offset]; } return entry;
复杂度近似是:O(log64N), 因为分支因子很大,树通常很浅,所以查找非常快。
6. 为什么它适合页缓存
页缓存的 key 本质上就是文件页偏移 pgoff_t,这是天然的整数索引。页缓存很适合 XArray 的原因:
(1) 索引天然是整数.
(2) 索引通常局部密集.
(3) 需要找“下一个存在的页”.
(4) 需要按标记扫描,比如 dirty/writeback/reclaim 相关页.
(5) 读远多于写,适合 RCU 查找.
所以现在页缓存的 mapping->i_pages 就是 XArray。
7. 存储原理
插入一个条目时,如果路径上某层节点不存在,就按需分配 xa_node(插入时可能有内存分配)。过程大致是:
确定当前树高度是否足够覆盖目标索引; 不够就扩展根; 从顶层一路向下; 缺哪一层就分配哪一层; 到叶子槽后放入 entry; 回溯更新 count、mark 等元信息;
这也是为什么 XArray 擅长稀疏数组:只为实际用到的路径分配节点,空洞完全不占线性空间。
8. 删除原理
删除时做相反的事:找到目标索引对应的叶子槽; 清空该槽; 向上回溯; 如果某个节点 count == 0,就把该节点释放; 一直清理到第一个仍非空的祖先节点为止; 所以树会自动收缩,不会无限残留空层。
9. 标记位 marks 的实现思想
XArray 每个非空条目可以附带 3 个 mark。这不是直接只存在叶子上,而是“叶子 + 中间节点摘要”一起维护。例如某个叶子条目被设置了 mark 1,那么它的祖先节点也会在 mark 位图里记录:这个子树中存在至少一个 mark 1 条目。
这样做的好处是扫描时可以快速剪枝。比如要找“下一个被标记的条目”:从高层节点先看摘要位图; 没有标记的整棵子树直接跳过; 只进入可能含有标记条目的分支; 这比逐个索引试探高效得多。
10. RCU 查找为什么快
XArray 的一个核心设计点是:读路径尽量无锁、写路径加 xa_lock、读写并发靠 RCU 协调。
读者做 xa_load() 时,一般只需要:进入 RCU read-side critical section,沿树指针向下读,返回结果,不需要抢自旋锁。
这是因为:节点释放延迟到 RCU grace period 之后; 读者即使看到旧节点,也不会立刻踩到已释放内存; 写者通过替换指针和内部条目控制并发更新.
所以 XArray 特别适合“查得很多、改得不算太多”的场景。
11. 写路径为什么仍需要 xa_lock
RCU 只能解决“读旧数据不崩”,不能解决“多个写者同时改树结构”的一致性问题。所以修改操作,比如:store/erase/cmpxchg/set/clear mark/alloc ID, 通常都要持有 xa_lock。
这个锁主要保护:树结构变化, 节点分配/回收, count 更新, mark 摘要更新, 多索引条目的拆分/合并状态.
所以 XArray 的并发模型可以概括成:读:RCU, 写:xa_lock, 读写共存:依赖 RCU + 内部状态协议.
12. 内部条目是什么
XArray 里并不是所有 slot 都是“用户真正存进去的指针”。有一部分编码值被保留给内部用途,叫 internal entries。比如:
node: 表示这是一个内部节点.
retry: 表示该位置正在被修改,读者应重试.
sibling: 多索引条目使用的辅助条目.
zero: 对普通 API 表现为 NULL,但内部表示“这个位置被保留了”
这些内部条目通过低位编码区分。因为普通指针要求 4 字节对齐,低几位本来就是空的,XArray 利用这些低位来编码特殊含义。这也是为什么它不能直接存任意函数指针或 IS_ERR() 指针, 因为编码空间会冲突。
13. value entry 的原理
XArray 不仅能存指针,还能存小整数。做法不是“真的把整数当指针”,而是用特殊编码把整数包装成 entry:
xa_mk_value(v)
xa_is_value(entry)
xa_to_value(entry)
底层同样利用的是指针对齐留下的低位空间。所以从实现上看,XArray entry 本质是一个“带标签的 machine word”。
14. 多索引条目为什么能省内存
XArray 支持一个条目占据一段索引范围。例如一个 order-9 的范围可覆盖 512 个索引。如果这 512 个索引逻辑上都对应同一个对象,就没必要真的插入 512 次。实现上通常是:只有一个 canonical entry, 其他相关槽位用 sibling/internal encoding 表示“我属于那个主条目”, 这样节省的不只是用户条目数,还包括中间层节点和 mark 维护成本。
这在页缓存的大页、shadow entry 等场景里很有价值。
15. allocating XArray 为什么像 IDR
如果带上 XA_FLAGS_ALLOC,XArray 就不仅是“按索引存值”,还会追踪“哪些索引空闲”。这使它能实现类似 ID 分配器的语义:
找到最小可用索引, 占用它, 返回给调用者。
这里会用 XA_MARK_0 跟踪 free/used 状态,所以 allocating XArray 不能把 mark 0 留给用户。
这也是为什么 XArray 取代了很多以前 IDR/IDA 的内部实现角色。
16. 为什么说它比 radix tree 更统一
XArray 可以看作 radix tree 的现代化重构版,但它比旧 radix tree 更统一,主要体现在:
(1) API 更完整: 普通 API 和高级 API 分层清晰.
(2) 并发语义更明确: RCU、内部锁、retry 条目等机制更系统.
(3) 同时覆盖多种需求: 普通映射、ID 分配、标记扫描、多索引范围. ####
(4) 更强调类型编码规范: value entry、tagged pointer、internal entry 都统一到 entry 编码模型里.
所以它不是单纯“一棵树”,而是一整套“稀疏索引容器框架”。
17. mapping->i_pages 这个xarray
mapping->i_pages 不是普通数组,它是一个“索引到 page/folio/shadow entry”的稀疏树,查页是按页偏移一路下钻,扫 dirty/writeback/reclaim 候选页时会大量利用 marks,高并发下读路径不想抢锁,所以依赖 RCU。
二、xarray.rst翻译
注:本文翻译自 msm-5.4/Documentation/core-api/xarray.rst
.. SPDX-License-Identifier: GPL-2.0+
======
XArray
======
:作者: Matthew Wilcox
1. 概述
XArray 是一种抽象数据类型,其行为类似于一个超大型的指针数组。它能满足哈希表或传统可调整大小数组的许多使用需求。与哈希表不同,XArray 允许你以缓存友好的方式有序地遍历前一个或后一个条目。与可调整大小的数组相比,XArray 无需复制数据或修改 MMU 映射即可扩展。与双向链表相比,它具有更高的内存效率、更好的并行性和缓存友好性。XArray 利用 RCU 机制实现无锁查找。
当使用的索引比较密集时,XArray 的实现效率最高;如果对对象做哈希并将哈希值用作索引,性能将不佳。XArray 对小索引进行了优化,但在大索引场景下也有良好表现。如果你的索引可能超过 ``ULONG_MAX``,则 XArray 不适合该用途。XArray 最重要的使用场景是页缓存(page cache)。
数组中每个非 ``NULL`` 的条目都关联有三个位(bits),称为"标记"(marks)。每个标记可以独立设置或清除。你可以遍历被标记的条目。
普通指针可以直接存储在 XArray 中。这些指针必须是 4 字节对齐的,kmalloc() 和 alloc_page() 返回的指针都满足此要求。任意用户空间指针和函数指针则不满足。你可以存储指向静态分配对象的指针,只要这些对象的对齐方式至少为 4 字节。
你也可以将 0 到 ``LONG_MAX`` 之间的整数存储在 XArray 中。首先需要使用 xa_mk_value() 将其转换为条目。从 XArray 中取回条目后,可以调用 xa_is_value() 检查它是否为值类型条目,并通过 xa_to_value() 将其转换回整数。
//include/linux/xarray.h static inline void *xa_mk_value(unsigned long v) { WARN_ON((long)v < 0); return (void *)((v << 1) | 1); } static inline bool xa_is_value(const void *entry) { return (unsigned long)entry & 1; } static inline unsigned long xa_to_value(const void *entry) { return (unsigned long)entry >> 1; }
部分用户希望存储带标签的指针,而不是使用上述标记机制。他们可以调用 xa_tag_pointer() 创建带标签的条目,调用 xa_untag_pointer() 将带标签的条目转回无标签指针,以及调用 xa_pointer_tag() 获取条目的标签。带标签的指针与值类型条目使用相同的位,因此每个用户必须决定在特定的 XArray 中存储值类型条目还是带标签的指针。
//include/linux/xarray.h static inline void *xa_tag_pointer(void *p, unsigned long tag) { return (void *)((unsigned long)p | tag); } static inline void *xa_untag_pointer(void *entry) { return (void *)((unsigned long)entry & ~3UL); } static inline unsigned int xa_pointer_tag(void *entry) { return (unsigned long)entry & 3UL; }
XArray 不支持存储 IS_ERR() 指针,因为这些指针与值类型条目或内部条目存在冲突。
XArray 有一个特殊功能:可以创建占据一段索引范围的条目。存入后,对该范围内任意索引的查找都会返回相同的条目。对某个索引设置标记,将对范围内所有索引生效。向任意索引写入数据,将对所有索引生效。多索引条目可以显式拆分为更小的条目,或者向任意条目存入 ``NULL`` 将使 XArray 忘记该范围。
2. 普通 API
首先初始化一个 XArray(struct xarray):对于静态分配的 XArray 使用 DEFINE_XARRAY(),对于动态分配的使用 xa_init()。一个刚初始化的 XArray 在每个索引处都包含 ``NULL`` 指针。
//include/linux/xarray.h struct xarray { spinlock_t xa_lock; gfp_t xa_flags; void __rcu * xa_head; }; #define DEFINE_XARRAY(name) DEFINE_XARRAY_FLAGS(name, 0) #define DEFINE_XARRAY_FLAGS(name, flags) \ struct xarray name = XARRAY_INIT(name, flags) #define XARRAY_INIT(name, flags) { \ .xa_lock = __SPIN_LOCK_UNLOCKED(name.xa_lock), \ .xa_flags = flags, \ .xa_head = NULL, \ } //等效于: struct xarray name = { .xa_lock = __SPIN_LOCK_UNLOCKED(name.xa_lock), .xa_flags = 0, .xa_head = NULL, }
xa_init() / xa_init_flags():
static inline void xa_init(struct xarray *xa) { xa_init_flags(xa, 0); } static inline void xa_init_flags(struct xarray *xa, gfp_t flags) { spin_lock_init(&xa->xa_lock); xa->xa_flags = flags; xa->xa_head = NULL; }
之后可使用 xa_store() 设置条目,使用 xa_load() 获取条目。xa_store() 会用新条目覆盖原有条目,并返回之前存储在该索引处的条目。你可以使用 xa_erase() 代替以 ``NULL`` 为参数调用 xa_store()。从未写入过的条目、已被擦除的条目以及最近存入了 ``NULL`` 的条目,三者之间没有任何区别。
void *xa_store(struct xarray *xa, unsigned long index, void *entry, gfp_t gfp) void *xa_load(struct xarray *xa, unsigned long index) void *xa_erase(struct xarray *xa, unsigned long index)
可以使用 xa_cmpxchg() 有条件地替换某个索引处的条目。与 cmpxchg() 类似,仅当该索引处的条目与 'old' 值匹配时才会成功。它同样会返回该索引处原有的条目;如果返回值与传入的 'old' 值相同,则说明 xa_cmpxchg() 操作成功。
void *xa_cmpxchg(struct xarray *xa, unsigned long index, void *old, void *entry, gfp_t gfp)
如果只想在某个索引处的当前条目为 ``NULL`` 时才存入新条目,可以使用 xa_insert(),当该条目不为空时它会返回 ``-EBUSY``。
int __must_check xa_insert(struct xarray *xa, unsigned long index, void *entry, gfp_t gfp)
可以使用 xa_get_mark() 查询某个条目是否被标记。如果条目不为 ``NULL``,可以使用 xa_set_mark() 对其设置标记,使用 xa_clear_mark() 清除标记。可以调用 xa_marked() 查询 XArray 中是否有任意条目设置了特定标记。
bool xa_get_mark(struct xarray *xa, unsigned long index, xa_mark_t mark) void xa_set_mark(struct xarray *xa, unsigned long index, xa_mark_t mark) void xa_clear_mark(struct xarray *xa, unsigned long index, xa_mark_t mark) bool xa_marked(const struct xarray *xa, xa_mark_t mark)
可以调用 xa_extract() 将 XArray 中的条目复制到普通数组中。或者调用 xa_for_each() 遍历 XArray 中的现有条目。也可使用 xa_find() 或 xa_find_after() 移动到 XArray 中的下一个现有条目。
调用 xa_store_range() 可以在一段索引范围内存储相同的条目。执行此操作后,部分其他操作的行为会略有不同。例如,对某个索引设置标记,可能导致部分(而非全部)其他索引处的条目也被标记。向某个索引写入数据,可能导致部分(而非全部)其他索引处取回的条目发生变化。
void *xa_store_range(struct xarray *xa, unsigned long first, unsigned long last, void *entry, gfp_t gfp)
有时需要确保后续对 xa_store() 的调用不需要分配内存。xa_reserve() 函数会在指定索引处存储一个"保留"条目。普通 API 的用户看到的该条目为 ``NULL``。如果不需要使用该保留条目,可以调用 xa_release() 移除它。如果在此期间已有其他用户向该条目写入了内容,xa_release() 将不做任何事;如果你希望该条目变为 ``NULL``,则应使用 xa_erase()。对保留条目调用 xa_insert() 将失败。
int xa_reserve(struct xarray *xa, unsigned long index, gfp_t gfp) void xa_release(struct xarray *xa, unsigned long index)
当数组中的所有条目均为 ``NULL`` 时,xa_empty() 将返回 ``true``。
static inline bool xa_empty(const struct xarray *xa) { return xa->xa_head == NULL; }
最后,可以调用 xa_destroy() 移除 XArray 中的所有条目。如果 XArray 中存储的是指针,则可能需要先释放这些条目。可以通过 xa_for_each() 迭代器遍历所有现有条目来完成此操作。
void xa_destroy(struct xarray *xa) #define xa_for_each(xa, index, entry) xa_for_each_start(xa, index, entry, 0)
3. 分配 XArray
如果使用 DEFINE_XARRAY_ALLOC() 定义 XArray,或通过向 xa_init_flags() 传入``XA_FLAGS_ALLOC`` 来初始化,则 XArray 会改变行为以追踪条目是否已被使用。
#define DEFINE_XARRAY_ALLOC(name) DEFINE_XARRAY_FLAGS(name, XA_FLAGS_ALLOC) //展开后为: struct xarray name = { .xa_lock = __SPIN_LOCK_UNLOCKED(name.xa_lock), \ .xa_flags = XA_FLAGS_ALLOC, \ .xa_head = NULL, \ };
可以调用 xa_alloc() 在 XArray 的未使用索引处存储条目。如果需要从中断上下文修改数组,可以使用 xa_alloc_bh() 或 xa_alloc_irq() 在分配 ID 期间禁用中断。
int xa_alloc(struct xarray *xa, u32 *id, void *entry, struct xa_limit limit, gfp_t gfp) int xa_alloc_bh(struct xarray *xa, u32 *id, void *entry, struct xa_limit limit, gfp_t gfp) int xa_alloc_irq(struct xarray *xa, u32 *id, void *entry, struct xa_limit limit, gfp_t gfp)
使用 xa_store()、xa_cmpxchg() 或 xa_insert() 也会将条目标记为已分配。与普通 XArray 不同,存入 ``NULL`` 将把条目标记为已使用(类似 xa_reserve())。要释放条目,使用 xa_erase()(或者如果只想在条目为 ``NULL`` 时释放它,使用xa_release())。
默认情况下,从 0 开始分配最低编号的空闲条目。如果希望从 1 开始分配,使用 DEFINE_XARRAY_ALLOC1() 或 ``XA_FLAGS_ALLOC1`` 效率更高。如果想要分配 ID 至某个最大值后再回绕到最低的空闲 ID,可以使用 xa_alloc_cyclic()。
#define DEFINE_XARRAY_ALLOC1(name) DEFINE_XARRAY_FLAGS(name, XA_FLAGS_ALLOC1) int xa_alloc_cyclic(struct xarray *xa, u32 *id, void *entry, struct xa_limit limit, u32 *next, gfp_t gfp)
不能将 ``XA_MARK_0`` 与分配型 XArray 结合使用,因为该标记已用于追踪条目是否空闲。其他标记可供正常使用。
4. 内存分配
xa_store()、xa_cmpxchg()、xa_alloc()、xa_reserve() 和 xa_insert() 函数都接受一个 gfp_t 参数,以备 XArray 在存储条目时需要分配内存####。如果正在删除条目,则无需执行内存分配,指定的 GFP 标志将被忽略。
可能出现无法分配到内存的情况,尤其是在传入限制性 GFP 标志时。这种情况下,函数将返回一个特殊值,可通过 xa_err() 将其转换为 errno。如果不需要知道具体是什么错误,使用 xa_is_err() 会略微更高效。
int xa_err(void *entry) bool xa_is_err(const void *entry)
5. 锁
使用普通 API 时,无需关心锁的问题。XArray 使用 RCU 和内部自旋锁来同步访问:
(1) 无需加锁:
* xa_empty()
* xa_marked()
(2) 获取 RCU 读锁:
* xa_load()
* xa_for_each()
* xa_find()
* xa_find_after()
* xa_extract()
* xa_get_mark()
(3) 内部获取 xa_lock:
* xa_store()
* xa_store_bh()
* xa_store_irq()
* xa_insert()
* xa_insert_bh()
* xa_insert_irq()
* xa_erase()
* xa_erase_bh()
* xa_erase_irq()
* xa_cmpxchg()
* xa_cmpxchg_bh()
* xa_cmpxchg_irq()
* xa_store_range()
* xa_alloc()
* xa_alloc_bh()
* xa_alloc_irq()
* xa_reserve()
* xa_reserve_bh()
* xa_reserve_irq()
* xa_destroy()
* xa_set_mark()
* xa_clear_mark()
(4) 要求调用时已持有 xa_lock:
* __xa_store()
* __xa_insert()
* __xa_erase()
* __xa_cmpxchg()
* __xa_alloc()
* __xa_set_mark()
* __xa_clear_mark()
如果希望借助锁来保护存储在 XArray 中的数据结构,可以在调用 xa_load() 之前先调用 xa_lock(),在获取到目标对象后对其增加引用计数,然后再调用 xa_unlock()。这样可以防止在查找对象到增加引用计数之间,store 操作将该对象从数组中移除。也可以使用 RCU 来避免访问已释放的内存,但这方面的说明超出了本文档的范围。
XArray 在修改数组时不会禁用中断或软中断。从中断或软中断上下文读取 XArray 是安全的,因为 RCU 锁提供了足够的保护。
例如,如果需要在进程上下文中向 XArray 存入条目,再在软中断上下文中将其擦除,可以按如下方式实现::
void foo_init(struct foo *foo) { xa_init_flags(&foo->array, XA_FLAGS_LOCK_BH); } int foo_store(struct foo *foo, unsigned long index, void *entry) { int err; xa_lock_bh(&foo->array); err = xa_err(__xa_store(&foo->array, index, entry, GFP_KERNEL)); //GFP标志对吗? if (!err) foo->count++; xa_unlock_bh(&foo->array); return err; } /* foo_erase() 只在软中断上下文中调用 */ void foo_erase(struct foo *foo, unsigned long index) { xa_lock(&foo->array); __xa_erase(&foo->array, index); foo->count--; xa_unlock(&foo->array); }
如果需要从中断或软中断上下文修改 XArray,需要使用 xa_init_flags() 初始化数组,传入 ``XA_FLAGS_LOCK_IRQ`` 或 ``XA_FLAGS_LOCK_BH``。
上述示例也展示了一种常见的模式:在 store 侧扩展 xa_lock 的覆盖范围,以保护与数组相关的某些统计信息。
也可以与中断上下文共享 XArray:在中断处理程序和进程上下文中都使用 xa_lock_irqsave(),或者在进程上下文中使用 xa_lock_irq(),在中断处理程序中使用 xa_lock()。一些更常见的模式有辅助函数,例如 xa_store_bh()、xa_store_irq()、xa_erase_bh()、xa_erase_irq()、xa_cmpxchg_bh() 和 xa_cmpxchg_irq()。
有时需要用互斥锁来保护对 XArray 的访问,因为该锁在锁层级中位于另一个互斥锁之上。但这并不意味着你可以在不持有 xa_lock 的情况下使用 __xa_erase() 等函数;xa_lock 用于 lockdep 验证,将来也会用于其他目的。
__xa_set_mark() 和 __xa_clear_mark() 函数也适用于这样的场景:在查找到条目后希望原子地设置或清除标记。此时使用高级 API 可能更高效,因为它可以避免对树进行两次遍历。
void __xa_set_mark(struct xarray *xa, unsigned long index, xa_mark_t mark) void __xa_clear_mark(struct xarray *xa, unsigned long index, xa_mark_t mark)
6. 高级 API
高级 API 提供了更大的灵活性和更好的性能,但代价是接口更难使用且安全防护较少。高级 API 不会自动进行任何加锁,你需要在修改数组时自己使用 xa_lock。在对数组进行只读操作时,可以选择使用 xa_lock 或 RCU 锁。可以在同一个数组上混用高级操作和普通操作;实际上普通 API 就是基于高级 API 实现的。高级 API 仅对具有GPL 兼容许可证的模块开放。
高级 API 以 xa_state 为核心。这是一个不透明的数据结构,使用 XA_STATE() 宏在栈上声明。该宏会将 xa_state 初始化为可以开始遍历 XArray 的状态。它作为游标使用,维护在 XArray 中的当前位置,使你可以将多种操作组合在一起,而无需每次都从头开始。
struct xa_state { struct xarray *xa; unsigned long xa_index; unsigned char xa_shift; unsigned char xa_sibs; unsigned char xa_offset; unsigned char xa_pad; struct xa_node *xa_node; struct xa_node *xa_alloc; xa_update_node_t xa_update; };
xa_state 也用于存储错误信息。可以调用 xas_error() 获取错误。所有操作在继续之前都会检查 xa_state 是否处于错误状态,因此无需在每次调用后检查错误;可以连续进行多次调用,只在方便的时候统一检查。XArray 代码本身目前只会产生 ``ENOMEM`` 和 ``EINVAL`` 错误,但如果你想自行调用 xas_set_err(),它支持任意错误。
int xas_error(const struct xa_state *xas) { return xa_err(xas->xa_node); } static inline void xas_set_err(struct xa_state *xas, long err) { xas->xa_node = XA_ERROR(err); }
如果 xa_state 持有 ``ENOMEM`` 错误,调用 xas_nomem() 会尝试使用指定的 gfp 标志分配更多内存并将其缓存在 xa_state 中供下次尝试使用。其设计思路是:获取 xa_lock,尝试操作,释放锁。在持锁状态下分配内存可能失败的概率更高。释放锁后,xas_nomem() 可以更积极地尝试分配内存。如果值得重试操作(即存在内存错误 *且*成功分配了更多内存),它将返回 ``true``。如果之前已分配了内存,但该内存未被使用,且没有错误(或存在非 ``ENOMEM`` 的其他错误),它将释放之前分配的内存。
bool xas_nomem(struct xa_state *xas, gfp_t gfp)
7. 内部条目
XArray 为自身用途保留了一些条目。这些条目通过普通 API 不可见,但使用高级 API 时可能会看到。通常最好的处理方式是将它们传给 xas_retry(),如果其返回 ``true``则重试操作。
bool xas_retry(struct xa_state *xas, const void *entry)
.. flat-table::
:widths: 1 1 6
名称 测试函数 用途说明
(1) Node xa_is_node() XArray 节点。在使用多索引 xa_state 时可能可见。
(2) Sibling xa_is_sibling() 多索引条目的非规范副本。其值指示此节点中哪个槽位持有规范条目。
(3) Retry xa_is_retry() 此条目正在被持有 xa_lock 的线程修改。包含该条目的节点可能在本 RCU 周期 结束时被释放。应从数组头部重新开始查找。
(4) Zero xa_is_zero() 零条目通过普通 API 呈现为 ``NULL``,但在 XArray 中占用一个条目,可用于为将来使用而保留索引。分配型 XArray 使用此机制表示值为 ``NULL`` 的已分配条目。
未来可能会添加其他内部条目。尽可能情况下,它们将由 xas_retry() 统一处理。
8. 附加功能
xas_create_range() 函数会分配所有必要的内存,以便存储某个范围内的每一个条目。如果无法分配内存,它将在 xa_state 中设置 ENOMEM。
void xas_create_range(struct xa_state *xas)
可以使用 xas_init_marks() 将条目上的标记重置为其默认状态。通常情况下所有标记都会被清除,除非 XArray 标记了 ``XA_FLAGS_TRACK_FREE``,此时标记 0 会被设置,其余标记会被清除。通过 xas_store() 将一个条目替换为另一个条目时,不会重置该条目上的标记;如果需要重置标记,应显式执行此操作。
void xas_init_marks(const struct xa_state *xas) void *xas_store(struct xa_state *xas, void *entry)
xas_load() 会将 xa_state 游标尽可能接近地移动到目标条目处。如果你知道 xa_state 已经遍历到了该条目,只需要检查条目是否发生了变化,可以使用 xas_reload() 来节省一次函数调用。
void *xas_load(struct xa_state *xas) void *xas_reload(struct xa_state *xas)
如果需要移动到 XArray 中的不同索引,调用 xas_set()。这会将游标重置到树的顶部,通常下次操作会将游标移到树中所需的位置。如果想移动到下一个或上一个索引,调用 xas_next() 或 xas_prev()。设置索引不会在数组中移动游标,因此不需要持有锁;而移动到下一个或上一个索引则需要。
void xas_set(struct xa_state *xas, unsigned long index) void *xas_next(struct xa_state *xas) void *xas_prev(struct xa_state *xas)
可以使用 xas_find() 搜索下一个现有条目。它等价于 xa_find() 和 xa_find_after() 两者的功能:如果游标已遍历到某个条目,将会查找当前引用条目之后的下一个条目;否则,将返回 xa_state 索引处的条目。使用 xas_next_entry() 代替 xas_find() 来移动到下一个现有条目,在大多数情况下会节省一次函数调用,但代价是生成更多内联代码。
void *xas_find(struct xa_state *xas, unsigned long max) void *xa_find(struct xarray *xa, unsigned long *indexp, unsigned long max, xa_mark_t filter) void *xa_find_after(struct xarray *xa, unsigned long *indexp, unsigned long max, xa_mark_t filter) void *xas_next_entry(struct xa_state *xas, unsigned long max)
xas_find_marked() 函数的行为类似。如果 xa_state 尚未遍历,它将返回 xa_state 索引处的条目(如果该条目已被标记)。否则,将返回由 xa_state 引用的条目之后的第一个被标记的条目。xas_next_marked() 函数等价于 xas_next_entry()。
void *xas_find_marked(struct xa_state *xas, unsigned long max, xa_mark_t mark) void *xas_next_marked(struct xa_state *xas, unsigned long max, xa_mark_t mark)
在使用 xas_for_each() 或 xas_for_each_marked() 遍历 XArray 的某个范围时,有时可能需要临时暂停迭代。xas_pause() 函数即用于此目的。完成必要的工作并希望继续时,xa_state 处于适当的状态,可以从上次处理的条目之后继续迭代。如果在迭代时禁用了中断,按礼貌约定应该每处理 ``XA_CHECK_SCHED`` 个条目就暂停迭代并重新启用中断。
void xas_pause(struct xa_state *xas) #define xas_for_each(xas, entry, max) \ for (entry = xas_find(xas, max); entry; entry = xas_next_entry(xas, max)) #define xas_for_each_marked(xas, entry, max, mark) \ for (entry = xas_find_marked(xas, max, mark); entry; entry = xas_next_marked(xas, max, mark))
xas_get_mark()、xas_set_mark() 和 xas_clear_mark() 函数要求 xa_state 游标已移动到 xarray 中的合适位置;如果在此之前刚调用了 xas_pause() 或 xas_set(),这些函数将不执行任何操作。
bool xas_get_mark(const struct xa_state *xas, xa_mark_t mark) void xas_set_mark(const struct xa_state *xas, xa_mark_t mark) void xas_clear_mark(const struct xa_state *xas, xa_mark_t mark) void xas_set(struct xa_state *xas, unsigned long index)
可以调用 xas_set_update() 来注册一个回调函数,每当 XArray 更新节点时都会调用该函数。页缓存工作集(workingset)代码使用此机制来维护只包含 shadow 条目的节点列表。
9. 多索引条目
XArray 具有将多个索引绑定在一起的能力,使得对一个索引的操作会影响所有绑定的索引。例如,向任意索引写入数据都会更改从任意索引取回的条目值。在任意索引上设置或清除标记,将对所有绑定在一起的索引上的标记生效。当前实现只允许将对齐的 2 的幂次方范围绑定在一起;例如,索引 64-127 可以绑定在一起,但 2-6 则不可以。这可以节省大量内存;例如,将 512 个条目绑定在一起,可以节省超过 4KB 的内存。
可以使用 XA_STATE_ORDER() 或 xas_set_order() 再调用 xas_store() 来创建多索引条目。使用多索引 xa_state 调用 xas_load() 将把 xa_state 游标移动到树中正确的位置,但返回值没有实际意义——即使范围内已存储了条目,返回值也可能是内部条目或 ``NULL``。调用 xas_find_conflict() 将返回范围内的第一个条目,如果范围内没有条目则返回 ``NULL``。xas_for_each_conflict() 迭代器会遍历与指定范围重叠的每一个条目。
#define XA_CHUNK_SHIFT 6 #define XA_STATE_ORDER(name, array, index, order) \ struct xa_state name = { .xa = array, \ .xa_index = (index >> order) << order, \ .xa_shift = order - (order % XA_CHUNK_SHIFT), \ .xa_sibs = (1U << (order % XA_CHUNK_SHIFT)) - 1, \ .xa_offset = 0, \ .xa_pad = 0, \ .xa_node = XAS_RESTART, \ .xa_alloc = NULL, \ .xa_update = NULL \ } void xas_set_order(struct xa_state *xas, unsigned long index, unsigned int order) void *xas_find_conflict(struct xa_state *xas) #define xas_for_each_conflict(xas, entry) while ((entry = xas_find_conflict(xas)))
如果 xas_load() 遇到多索引条目,xa_state 中的 xa_index 不会被改变。在遍历 XArray 或调用 xas_find() 时,如果初始索引位于某个多索引条目的中间,该索引不会被修改。后续的调用或迭代将把索引移动到该范围的第一个索引处。无论一个条目占据多少个索引,它只会被返回一次。
不支持在多索引 xa_state 上使用 xas_next() 或 xas_prev()。在多索引条目上使用这两个函数会暴露出 sibling 条目,调用方应跳过这些条目。
向多索引条目的任意索引存入 ``NULL``,将把每个索引处的条目都设为 ``NULL``,并解除绑定关系。可以通过在不持有 xa_lock 的情况下调用 xas_split_alloc(),然后在持锁状态下调用 xas_split(),将多索引条目拆分为占据更小范围的条目。
void xas_split_alloc(struct xa_state *xas, void *entry, unsigned int order, gfp_t gfp) void xas_split(struct xa_state *xas, void *entry, unsigned int order)
10. 函数与数据结构
.. kernel-doc:: include/linux/xarray.h
.. kernel-doc:: lib/xarray.c
posted on 2026-04-07 14:48 Hello-World3 阅读(1) 评论(0) 收藏 举报
浙公网安备 33010602011771号