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)    收藏  举报

导航