요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0+
======
XArray
======
:Author: Matthew Wilcox
Overview
========
The XArray is an abstract data type which behaves like a very large array
of pointers. It meets many of the same needs as a hash or a conventional
resizable array. Unlike a hash, it allows you to sensibly go to the
next or previous entry in a cache-efficient manner. In contrast to a
resizable array, there is no need to copy data or change MMU mappings in
order to grow the array. It is more memory-efficient, parallelisable
and cache friendly than a doubly-linked list. It takes advantage of
RCU to perform lookups without locking.
The XArray implementation is efficient when the indices used are densely
clustered; hashing the object and using the hash as the index will not
perform well. The XArray is optimised for small indices, but still has
good performance with large indices. If your index can be larger than
``ULONG_MAX`` then the XArray is not the data type for you. The most
important user of the XArray is the page cache.
Normal pointers may be stored in the XArray directly. They must be 4-byte
aligned, which is true for any pointer returned from kmalloc() and
alloc_page(). It isn't true for arbitrary user-space pointers,
nor for function pointers. You can store pointers to statically allocated
objects, as long as those objects have an alignment of at least 4.
You can also store integers between 0 and ``LONG_MAX`` in the XArray.
You must first convert it into an entry using xa_mk_value().
When you retrieve an entry from the XArray, you can check whether it is
a value entry by calling xa_is_value(), and convert it back to
an integer by calling xa_to_value().
Some users want to tag the pointers they store in the XArray. You can
call xa_tag_pointer() to create an entry with a tag, xa_untag_pointer()
to turn a tagged entry back into an untagged pointer and xa_pointer_tag()
to retrieve the tag of an entry. Tagged pointers use the same bits that
are used to distinguish value entries from normal pointers, so you must
decide whether you want to store value entries or tagged pointers in any
particular XArray.
The XArray does not support storing IS_ERR() pointers as some
conflict with value entries or internal entries.
An unusual feature of the XArray is the ability to create entries which
occupy a range of indices. Once stored to, looking up any index in
the range will return the same entry as looking up any other index in
the range. Storing to any index will store to all of them. Multi-index
entries can be explicitly split into smaller entries. Unsetting (using
xa_erase() or xa_store() with ``NULL``) any entry will cause the XArray
to forget about the range.
Normal API
==========
Start by initialising an XArray, either with DEFINE_XARRAY()
for statically allocated XArrays or xa_init() for dynamically
allocated ones. A freshly-initialised XArray contains a ``NULL``
pointer at every index.
You can then set entries using xa_store() and get entries using
xa_load(). xa_store() will overwrite any entry with the new entry and
return the previous entry stored at that index. You can unset entries
using xa_erase() or by setting the entry to ``NULL`` using xa_store().
There is no difference between an entry that has never been stored to
and one that has been erased with xa_erase(); an entry that has most
recently had ``NULL`` stored to it is also equivalent except if the
XArray was initialized with ``XA_FLAGS_ALLOC``.
You can conditionally replace an entry at an index by using
xa_cmpxchg(). Like cmpxchg(), it will only succeed if
the entry at that index has the 'old' value. It also returns the entry
which was at that index; if it returns the same entry which was passed as
'old', then xa_cmpxchg() succeeded.
If you want to only store a new entry to an index if the current entry
at that index is ``NULL``, you can use xa_insert() which
returns ``-EBUSY`` if the entry is not empty.
You can copy entries out of the XArray into a plain array by calling
xa_extract(). Or you can iterate over the present entries in the XArray
by calling xa_for_each(), xa_for_each_start() or xa_for_each_range().
You may prefer to use xa_find() or xa_find_after() to move to the next
present entry in the XArray.
Calling xa_store_range() stores the same entry in a range
of indices. If you do this, some of the other operations will behave
in a slightly odd way. For example, marking the entry at one index
may result in the entry being marked at some, but not all of the other
indices. Storing into one index may result in the entry retrieved by
some, but not all of the other indices changing.
Sometimes you need to ensure that a subsequent call to xa_store()
will not need to allocate memory. The xa_reserve() function
will store a reserved entry at the indicated index. Users of the
normal API will see this entry as containing ``NULL``. If you do
not need to use the reserved entry, you can call xa_release()
to remove the unused entry. If another user has stored to the entry
in the meantime, xa_release() will do nothing; if instead you
want the entry to become ``NULL``, you should use xa_erase().
Using xa_insert() on a reserved entry will fail.
If all entries in the array are ``NULL``, the xa_empty() function
will return ``true``.
Finally, you can remove all entries from an XArray by calling
xa_destroy(). If the XArray entries are pointers, you may wish
to free the entries first. You can do this by iterating over all present
entries in the XArray using the xa_for_each() iterator.
Search Marks
------------
Each entry in the array has three bits associated with it called marks.
Each mark may be set or cleared independently of the others. You can
iterate over marked entries by using the xa_for_each_marked() iterator.
You can enquire whether a mark is set on an entry by using
xa_get_mark(). If the entry is not ``NULL``, you can set a mark on it
by using xa_set_mark() and remove the mark from an entry by calling
xa_clear_mark(). You can ask whether any entry in the XArray has a
particular mark set by calling xa_marked(). Erasing an entry from the
XArray causes all marks associated with that entry to be cleared.
Setting or clearing a mark on any index of a multi-index entry will
affect all indices covered by that entry. Querying the mark on any
index will return the same result.
There is no way to iterate over entries which are not marked; the data
structure does not allow this to be implemented efficiently. There are
not currently iterators to search for logical combinations of bits (eg
iterate over all entries which have both ``XA_MARK_1`` and ``XA_MARK_2``
set, or iterate over all entries which have ``XA_MARK_0`` or ``XA_MARK_2``
set). It would be possible to add these if a user arises.
Allocating XArrays
------------------
If you use DEFINE_XARRAY_ALLOC() to define the XArray, or
initialise it by passing ``XA_FLAGS_ALLOC`` to xa_init_flags(),
the XArray changes to track whether entries are in use or not.
You can call xa_alloc() to store the entry at an unused index
in the XArray. If you need to modify the array from interrupt context,
you can use xa_alloc_bh() or xa_alloc_irq() to disable
interrupts while allocating the ID.
Using xa_store(), xa_cmpxchg() or xa_insert() will
also mark the entry as being allocated. Unlike a normal XArray, storing
``NULL`` will mark the entry as being in use, like xa_reserve().
To free an entry, use xa_erase() (or xa_release() if
you only want to free the entry if it's ``NULL``).
By default, the lowest free entry is allocated starting from 0. If you
want to allocate entries starting at 1, it is more efficient to use
DEFINE_XARRAY_ALLOC1() or ``XA_FLAGS_ALLOC1``. If you want to
allocate IDs up to a maximum, then wrap back around to the lowest free
ID, you can use xa_alloc_cyclic().
You cannot use ``XA_MARK_0`` with an allocating XArray as this mark
is used to track whether an entry is free or not. The other marks are
available for your use.
Memory allocation
-----------------
The xa_store(), xa_cmpxchg(), xa_alloc(),
xa_reserve() and xa_insert() functions take a gfp_t
parameter in case the XArray needs to allocate memory to store this entry.
If the entry is being deleted, no memory allocation needs to be performed,
and the GFP flags specified will be ignored.
It is possible for no memory to be allocatable, particularly if you pass
a restrictive set of GFP flags. In that case, the functions return a
special value which can be turned into an errno using xa_err().
If you don't need to know exactly which error occurred, using
xa_is_err() is slightly more efficient.
Locking
-------
When using the Normal API, you do not have to worry about locking.
The XArray uses RCU and an internal spinlock to synchronise access:
No lock needed:
* xa_empty()
* xa_marked()
Takes RCU read lock:
* xa_load()
* xa_for_each()
* xa_for_each_start()
* xa_for_each_range()
* xa_find()
* xa_find_after()
* xa_extract()
* xa_get_mark()
Takes xa_lock internally:
* 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()
Assumes xa_lock held on entry:
* __xa_store()
* __xa_insert()
* __xa_erase()
* __xa_cmpxchg()
* __xa_alloc()
* __xa_set_mark()
* __xa_clear_mark()
If you want to take advantage of the lock to protect the data structures
that you are storing in the XArray, you can call xa_lock()
before calling xa_load(), then take a reference count on the
object you have found before calling xa_unlock(). This will
prevent stores from removing the object from the array between looking
up the object and incrementing the refcount. You can also use RCU to
avoid dereferencing freed memory, but an explanation of that is beyond
the scope of this document.
The XArray does not disable interrupts or softirqs while modifying
the array. It is safe to read the XArray from interrupt or softirq
context as the RCU lock provides enough protection.
If, for example, you want to store entries in the XArray in process
context and then erase them in softirq context, you can do that this way::
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));
if (!err)
foo->count++;
xa_unlock_bh(&foo->array);
return err;
}
/* foo_erase() is only called from softirq context */
void foo_erase(struct foo *foo, unsigned long index)
{
xa_lock(&foo->array);
__xa_erase(&foo->array, index);
foo->count--;
xa_unlock(&foo->array);
}
If you are going to modify the XArray from interrupt or softirq context,
you need to initialise the array using xa_init_flags(), passing
``XA_FLAGS_LOCK_IRQ`` or ``XA_FLAGS_LOCK_BH``.
The above example also shows a common pattern of wanting to extend the
coverage of the xa_lock on the store side to protect some statistics
associated with the array.
Sharing the XArray with interrupt context is also possible, either
using xa_lock_irqsave() in both the interrupt handler and process
context, or xa_lock_irq() in process context and xa_lock()
in the interrupt handler. Some of the more common patterns have helper
functions such as xa_store_bh(), xa_store_irq(),
xa_erase_bh(), xa_erase_irq(), xa_cmpxchg_bh()
and xa_cmpxchg_irq().
Sometimes you need to protect access to the XArray with a mutex because
that lock sits above another mutex in the locking hierarchy. That does
not entitle you to use functions like __xa_erase() without taking
the xa_lock; the xa_lock is used for lockdep validation and will be used
for other purposes in the future.
The __xa_set_mark() and __xa_clear_mark() functions are also
available for situations where you look up an entry and want to atomically
set or clear a mark. It may be more efficient to use the advanced API
in this case, as it will save you from walking the tree twice.
Advanced API
============
The advanced API offers more flexibility and better performance at the
cost of an interface which can be harder to use and has fewer safeguards.
No locking is done for you by the advanced API, and you are required
to use the xa_lock while modifying the array. You can choose whether
to use the xa_lock or the RCU lock while doing read-only operations on
the array. You can mix advanced and normal operations on the same array;
indeed the normal API is implemented in terms of the advanced API. The
advanced API is only available to modules with a GPL-compatible license.
The advanced API is based around the xa_state. This is an opaque data
structure which you declare on the stack using the XA_STATE() macro.
This macro initialises the xa_state ready to start walking around the
XArray. It is used as a cursor to maintain the position in the XArray
and let you compose various operations together without having to restart
from the top every time. The contents of the xa_state are protected by
the rcu_read_lock() or the xas_lock(). If you need to drop whichever of
those locks is protecting your state and tree, you must call xas_pause()
so that future calls do not rely on the parts of the state which were
left unprotected.
The xa_state is also used to store errors. You can call
xas_error() to retrieve the error. All operations check whether
the xa_state is in an error state before proceeding, so there's no need
for you to check for an error after each call; you can make multiple
calls in succession and only check at a convenient point. The only
errors currently generated by the XArray code itself are ``ENOMEM`` and
``EINVAL``, but it supports arbitrary errors in case you want to call
xas_set_err() yourself.
If the xa_state is holding an ``ENOMEM`` error, calling xas_nomem()
will attempt to allocate more memory using the specified gfp flags and
cache it in the xa_state for the next attempt. The idea is that you take
the xa_lock, attempt the operation and drop the lock. The operation
attempts to allocate memory while holding the lock, but it is more
likely to fail. Once you have dropped the lock, xas_nomem()
can try harder to allocate more memory. It will return ``true`` if it
is worth retrying the operation (i.e. that there was a memory error *and*
more memory was allocated). If it has previously allocated memory, and
that memory wasn't used, and there is no error (or some error that isn't
``ENOMEM``), then it will free the memory previously allocated.
Internal Entries
----------------
The XArray reserves some entries for its own purposes. These are never
exposed through the normal API, but when using the advanced API, it's
possible to see them. Usually the best way to handle them is to pass them
to xas_retry(), and retry the operation if it returns ``true``.
.. flat-table::
:widths: 1 1 6
* - Name
- Test
- Usage
* - Node
- xa_is_node()
- An XArray node. May be visible when using a multi-index xa_state.
* - Sibling
- xa_is_sibling()
- A non-canonical entry for a multi-index entry. The value indicates
which slot in this node has the canonical entry.
* - Retry
- xa_is_retry()
- This entry is currently being modified by a thread which has the
xa_lock. The node containing this entry may be freed at the end
of this RCU period. You should restart the lookup from the head
of the array.
* - Zero
- xa_is_zero()
- Zero entries appear as ``NULL`` through the Normal API, but occupy
an entry in the XArray which can be used to reserve the index for
future use. This is used by allocating XArrays for allocated entries
which are ``NULL``.
Other internal entries may be added in the future. As far as possible, they
will be handled by xas_retry().
Additional functionality
------------------------
The xas_create_range() function allocates all the necessary memory
to store every entry in a range. It will set ENOMEM in the xa_state if
it cannot allocate memory.
You can use xas_init_marks() to reset the marks on an entry
to their default state. This is usually all marks clear, unless the
XArray is marked with ``XA_FLAGS_TRACK_FREE``, in which case mark 0 is set
and all other marks are clear. Replacing one entry with another using
xas_store() will not reset the marks on that entry; if you want
the marks reset, you should do that explicitly.
The xas_load() will walk the xa_state as close to the entry
as it can. If you know the xa_state has already been walked to the
entry and need to check that the entry hasn't changed, you can use
xas_reload() to save a function call.
If you need to move to a different index in the XArray, call
xas_set(). This resets the cursor to the top of the tree, which
will generally make the next operation walk the cursor to the desired
spot in the tree. If you want to move to the next or previous index,
call xas_next() or xas_prev(). Setting the index does
not walk the cursor around the array so does not require a lock to be
held, while moving to the next or previous index does.
You can search for the next present entry using xas_find(). This
is the equivalent of both xa_find() and xa_find_after();
if the cursor has been walked to an entry, then it will find the next
entry after the one currently referenced. If not, it will return the
entry at the index of the xa_state. Using xas_next_entry() to
move to the next present entry instead of xas_find() will save
a function call in the majority of cases at the expense of emitting more
inline code.
The xas_find_marked() function is similar. If the xa_state has
not been walked, it will return the entry at the index of the xa_state,
if it is marked. Otherwise, it will return the first marked entry after
the entry referenced by the xa_state. The xas_next_marked()
function is the equivalent of xas_next_entry().
When iterating over a range of the XArray using xas_for_each()
or xas_for_each_marked(), it may be necessary to temporarily stop
the iteration. The xas_pause() function exists for this purpose.
After you have done the necessary work and wish to resume, the xa_state
is in an appropriate state to continue the iteration after the entry
you last processed. If you have interrupts disabled while iterating,
then it is good manners to pause the iteration and reenable interrupts
every ``XA_CHECK_SCHED`` entries.
The xas_get_mark(), xas_set_mark() and xas_clear_mark() functions require
the xa_state cursor to have been moved to the appropriate location in the
XArray; they will do nothing if you have called xas_pause() or xas_set()
immediately before.
You can call xas_set_update() to have a callback function
called each time the XArray updates a node. This is used by the page
cache workingset code to maintain its list of nodes which contain only
shadow entries.
Multi-Index Entries
-------------------
The XArray has the ability to tie multiple indices together so that
operations on one index affect all indices. For example, storing into
any index will change the value of the entry retrieved from any index.
Setting or clearing a mark on any index will set or clear the mark
on every index that is tied together. The current implementation
only allows tying ranges which are aligned powers of two together;
eg indices 64-127 may be tied together, but 2-6 may not be. This may
save substantial quantities of memory; for example tying 512 entries
together will save over 4kB.
You can create a multi-index entry by using XA_STATE_ORDER()
or xas_set_order() followed by a call to xas_store().
Calling xas_load() with a multi-index xa_state will walk the
xa_state to the right location in the tree, but the return value is not
meaningful, potentially being an internal entry or ``NULL`` even when there
is an entry stored within the range. Calling xas_find_conflict()
will return the first entry within the range or ``NULL`` if there are no
entries in the range. The xas_for_each_conflict() iterator will
iterate over every entry which overlaps the specified range.
If xas_load() encounters a multi-index entry, the xa_index
in the xa_state will not be changed. When iterating over an XArray
or calling xas_find(), if the initial index is in the middle
of a multi-index entry, it will not be altered. Subsequent calls
or iterations will move the index to the first index in the range.
Each entry will only be returned once, no matter how many indices it
occupies.
Using xas_next() or xas_prev() with a multi-index xa_state is not
supported. Using either of these functions on a multi-index entry will
reveal sibling entries; these should be skipped over by the caller.
Storing ``NULL`` into any index of a multi-index entry will set the
entry at every index to ``NULL`` and dissolve the tie. A multi-index
entry can be split into entries occupying smaller ranges by calling
xas_split_alloc() without the xa_lock held, followed by taking the lock
and calling xas_split() or calling xas_try_split() with xa_lock. The
difference between xas_split_alloc()+xas_split() and xas_try_alloc() is
that xas_split_alloc() + xas_split() split the entry from the original
order to the new order in one shot uniformly, whereas xas_try_split()
iteratively splits the entry containing the index non-uniformly.
For example, to split an order-9 entry, which takes 2^(9-6)=8 slots,
assuming ``XA_CHUNK_SHIFT`` is 6, xas_split_alloc() + xas_split() need
8 xa_node. xas_try_split() splits the order-9 entry into
2 order-8 entries, then split one order-8 entry, based on the given index,
to 2 order-7 entries, ..., and split one order-1 entry to 2 order-0 entries.
When splitting the order-6 entry and a new xa_node is needed, xas_try_split()
will try to allocate one if possible. As a result, xas_try_split() would only
need 1 xa_node instead of 8.
Functions and structures
========================
.. kernel-doc:: include/linux/xarray.h
.. kernel-doc:: lib/xarray.c
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
XArray
1-8.. SPDX-License-Identifier: GPL-2.0+
XArray
저자: Matthew Wilcox
개요
9-58개요
XArray는 매우 큰 pointer 배열처럼 동작하는 추상 자료형입니다. Hash나 일반적인 크기 조절 가능 배열과 같은 요구를 많이 충족합니다. Hash와 달리 cache 효율적인 방식으로 다음 또는 이전 entry로 이동할 수 있습니다. 크기 조절 가능 배열과 달리 배열을 확장하기 위해 data를 복사하거나 MMU mapping을 변경할 필요가 없습니다. Doubly-linked list보다 memory 효율적이고 병렬화하기 쉬우며 cache 친화적입니다. RCU를 활용하여 lock 없이 lookup을 수행합니다.
XArray 구현은 사용하는 index가 조밀하게 모여 있을 때 효율적입니다. Object를 hashing하고 그 hash를 index로 사용하면 성능이 좋지 않습니다. XArray는 작은 index에 최적화되어 있지만 큰 index에서도 좋은 성능을 냅니다. Index가 `ULONG_MAX`보다 클 수 있다면 XArray는 적합한 자료형이 아닙니다. XArray의 가장 중요한 사용자는 page cache입니다.
일반 pointer는 XArray에 직접 저장할 수 있습니다. Pointer는 4-byte alignment를 만족해야 하며, `kmalloc()`과 `alloc_page()`가 반환하는 모든 pointer가 이에 해당합니다. 임의의 user-space pointer나 function pointer에는 이 조건이 성립하지 않습니다. 정적으로 할당한 object도 alignment가 최소 4라면 pointer를 저장할 수 있습니다.
0부터 `LONG_MAX` 사이의 integer도 XArray에 저장할 수 있습니다. 먼저 `xa_mk_value()`를 사용해 entry로 변환해야 합니다. XArray에서 entry를 가져온 뒤 `xa_is_value()`로 value entry인지 검사하고 `xa_to_value()`로 다시 integer로 변환할 수 있습니다.
XArray에 저장하는 pointer에 tag를 붙여야 하는 사용자도 있습니다. `xa_tag_pointer()`는 tag가 있는 entry를 만들고, `xa_untag_pointer()`는 tagged entry를 tag가 없는 pointer로 되돌리며, `xa_pointer_tag()`는 entry의 tag를 가져옵니다. Tagged pointer는 value entry와 일반 pointer를 구분하는 bit를 함께 사용하므로, 특정 XArray에는 value entry와 tagged pointer 중 어느 것을 저장할지 결정해야 합니다.
일부 `IS_ERR()` pointer는 value entry 또는 internal entry와 충돌하므로 XArray는 `IS_ERR()` pointer 저장을 지원하지 않습니다.
XArray의 특이한 기능 중 하나는 일정한 index 범위를 차지하는 entry를 만들 수 있다는 점입니다. 저장 후에는 범위 안의 어느 index를 lookup해도 다른 index와 같은 entry를 반환하며, 어느 index에 저장하더라도 전체 범위에 저장됩니다. Multi-index entry는 명시적으로 더 작은 entry로 분할할 수 있습니다. `xa_erase()` 또는 `NULL`을 전달한 `xa_store()`로 어느 entry든 해제하면 XArray는 그 범위 정보를 잊습니다.
일반 API
59-116일반 API
정적으로 할당한 XArray는 `DEFINE_XARRAY()`로, 동적으로 할당한 XArray는 `xa_init()`으로 초기화합니다. 새로 초기화한 XArray의 모든 index에는 `NULL` pointer가 들어 있습니다.
`xa_store()`로 entry를 설정하고 `xa_load()`로 가져옵니다. `xa_store()`는 index의 기존 entry를 새 entry로 덮어쓰고 이전 entry를 반환합니다. `xa_erase()`를 사용하거나 `xa_store()`로 `NULL`을 저장하여 entry를 해제할 수 있습니다. 한 번도 저장하지 않은 entry, `xa_erase()`로 지운 entry, 가장 최근에 `NULL`을 저장한 entry는 동일합니다. 단, XArray를 `XA_FLAGS_ALLOC`으로 초기화한 경우는 예외입니다.
`xa_cmpxchg()`를 사용하면 index의 entry를 조건부로 교체할 수 있습니다. `cmpxchg()`와 마찬가지로 해당 entry가 `old` 값일 때만 성공합니다. 이 함수는 그 index에 있던 entry도 반환하며, 전달한 `old`와 같은 entry가 반환되면 `xa_cmpxchg()`가 성공한 것입니다.
현재 index의 entry가 `NULL`일 때만 새 entry를 저장하려면 `xa_insert()`를 사용합니다. Entry가 비어 있지 않으면 `-EBUSY`를 반환합니다.
`xa_extract()`를 호출하면 XArray의 entry를 일반 배열로 복사할 수 있습니다. `xa_for_each()`, `xa_for_each_start()`, `xa_for_each_range()`로 현재 존재하는 entry를 순회할 수도 있습니다. 다음 present entry로 이동할 때는 `xa_find()` 또는 `xa_find_after()`가 더 알맞을 수 있습니다.
`xa_store_range()`는 일정한 index 범위에 같은 entry를 저장합니다. 이때 일부 다른 operation은 다소 특이하게 동작합니다. 예를 들어 한 index의 entry에 mark를 설정하면 다른 index 중 일부에만 mark가 설정될 수 있고, 한 index에 저장하면 다른 index 중 일부에서 가져오는 entry만 바뀔 수 있습니다.
이후의 `xa_store()`가 memory를 할당하지 않도록 보장해야 할 때가 있습니다. `xa_reserve()`는 지정한 index에 reserved entry를 저장하며 일반 API 사용자는 이를 `NULL`로 봅니다. Reserved entry가 필요 없어지면 `xa_release()`로 사용하지 않은 entry를 제거합니다. 그 사이 다른 사용자가 entry에 저장했다면 `xa_release()`는 아무것도 하지 않습니다. Entry를 반드시 `NULL`로 만들려면 `xa_erase()`를 사용해야 합니다. Reserved entry에 대한 `xa_insert()`는 실패합니다.
배열의 모든 entry가 `NULL`이면 `xa_empty()`가 `true`를 반환합니다.
마지막으로 `xa_destroy()`를 호출하면 XArray의 모든 entry를 제거할 수 있습니다. Entry가 pointer라면 먼저 해제해야 할 수 있으며, `xa_for_each()` iterator로 모든 present entry를 순회하여 처리할 수 있습니다.
검색 mark
117-141검색 mark
배열의 각 entry에는 mark라고 부르는 bit 세 개가 연결됩니다. 각 mark는 서로 독립적으로 설정하거나 지울 수 있습니다. `xa_for_each_marked()` iterator로 mark가 설정된 entry를 순회할 수 있습니다.
`xa_get_mark()`로 entry에 mark가 설정되었는지 확인합니다. Entry가 `NULL`이 아니면 `xa_set_mark()`로 mark를 설정하고 `xa_clear_mark()`로 제거할 수 있습니다. `xa_marked()`는 특정 mark가 설정된 entry가 XArray에 하나라도 있는지 확인합니다. XArray에서 entry를 지우면 그 entry와 연결된 모든 mark도 지워집니다.
Multi-index entry의 어느 index에서 mark를 설정하거나 지우더라도 그 entry가 포함하는 모든 index에 영향을 줍니다. 어느 index에서 mark를 조회해도 같은 결과가 반환됩니다.
Mark가 없는 entry를 순회하는 방법은 없습니다. 이 자료 구조로는 이를 효율적으로 구현할 수 없기 때문입니다. 현재는 `XA_MARK_1`과 `XA_MARK_2`가 모두 설정된 entry 또는 `XA_MARK_0`이나 `XA_MARK_2`가 설정된 entry처럼 bit의 논리 조합을 검색하는 iterator도 없습니다. 사용 사례가 생기면 추가할 수 있습니다.
할당형 XArray
142-169할당형 XArray
`DEFINE_XARRAY_ALLOC()`으로 XArray를 정의하거나 `XA_FLAGS_ALLOC`을 `xa_init_flags()`에 전달해 초기화하면, XArray는 entry가 사용 중인지 추적하는 방식으로 바뀝니다.
`xa_alloc()`은 XArray의 사용하지 않는 index에 entry를 저장합니다. Interrupt context에서 배열을 수정해야 한다면 ID를 할당하는 동안 interrupt를 비활성화하는 `xa_alloc_bh()` 또는 `xa_alloc_irq()`를 사용할 수 있습니다.
`xa_store()`, `xa_cmpxchg()`, `xa_insert()`도 entry를 allocated 상태로 표시합니다. 일반 XArray와 달리 `NULL` 저장도 `xa_reserve()`처럼 entry를 사용 중으로 표시합니다. Entry를 해제하려면 `xa_erase()`를 사용하고, entry가 `NULL`인 경우에만 해제하려면 `xa_release()`를 사용합니다.
기본적으로 가장 낮은 free entry를 0부터 할당합니다. 1부터 할당하려면 `DEFINE_XARRAY_ALLOC1()` 또는 `XA_FLAGS_ALLOC1`을 사용하는 편이 효율적입니다. 최대값까지 ID를 할당한 뒤 가장 낮은 free ID로 되돌아가려면 `xa_alloc_cyclic()`을 사용합니다.
할당형 XArray에서는 `XA_MARK_0`이 entry의 free 여부를 추적하는 데 사용되므로 사용자가 쓸 수 없습니다. 나머지 mark는 사용할 수 있습니다.
Memory 할당
170-184Memory 할당
`xa_store()`, `xa_cmpxchg()`, `xa_alloc()`, `xa_reserve()`, `xa_insert()`는 entry 저장에 memory가 필요할 때를 위해 `gfp_t` parameter를 받습니다. Entry를 삭제하는 경우에는 memory를 할당할 필요가 없으므로 지정한 GFP flag를 무시합니다.
특히 제한적인 GFP flag를 전달하면 memory를 전혀 할당할 수 없을 수 있습니다. 이때 함수들은 `xa_err()`로 errno로 변환할 수 있는 특별한 값을 반환합니다. 정확히 어떤 error인지 알 필요가 없다면 `xa_is_err()`가 조금 더 효율적입니다.
Locking 분류
185-237Locking
일반 API를 사용할 때는 locking을 직접 처리할 필요가 없습니다. XArray는 RCU와 내부 spinlock으로 access를 동기화합니다.
Lock이 필요 없는 함수:
- xa_empty()
- xa_marked()
RCU read lock을 획득하는 함수와 iterator:
- xa_load()
- xa_for_each()
- xa_for_each_start()
- xa_for_each_range()
- xa_find()
- xa_find_after()
- xa_extract()
- xa_get_mark()
내부에서 `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()
진입 시 `xa_lock`이 이미 잡혀 있다고 가정하는 함수:
- __xa_store()
- __xa_insert()
- __xa_erase()
- __xa_cmpxchg()
- __xa_alloc()
- __xa_set_mark()
- __xa_clear_mark()
Locking 사용 방법
238-306XArray에 저장한 자료 구조까지 lock으로 보호하려면 `xa_load()` 전에 `xa_lock()`을 호출하고, 찾은 object의 reference count를 획득한 뒤 `xa_unlock()`을 호출할 수 있습니다. 그러면 object lookup과 refcount 증가 사이에 store가 배열에서 object를 제거하지 못합니다. RCU로 해제된 memory의 dereference를 피할 수도 있지만 이는 이 문서의 범위를 벗어납니다.
XArray는 배열을 수정하는 동안 interrupt나 softirq를 비활성화하지 않습니다. RCU lock이 충분한 보호를 제공하므로 interrupt 또는 softirq context에서 XArray를 읽어도 안전합니다.
예를 들어 process context에서 XArray에 entry를 저장하고 softirq context에서 지우려면 다음과 같이 구현할 수 있습니다.
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));
if (!err)
foo->count++;
xa_unlock_bh(&foo->array);
return err;
}
/* foo_erase() is only called from softirq context */
void foo_erase(struct foo *foo, unsigned long index)
{
xa_lock(&foo->array);
__xa_erase(&foo->array, index);
foo->count--;
xa_unlock(&foo->array);
}
Interrupt 또는 softirq context에서 XArray를 수정한다면 `xa_init_flags()`에 `XA_FLAGS_LOCK_IRQ` 또는 `XA_FLAGS_LOCK_BH`를 전달하여 배열을 초기화해야 합니다.
위 예제는 배열과 연결된 통계를 보호하기 위해 store 쪽에서 `xa_lock`의 보호 범위를 확장하는 일반적인 pattern도 보여 줍니다.
XArray를 interrupt context와 공유할 수도 있습니다. Interrupt handler와 process context 양쪽에서 `xa_lock_irqsave()`를 사용하거나, process context에서 `xa_lock_irq()`를 사용하고 interrupt handler에서 `xa_lock()`을 사용합니다. 흔한 pattern에는 `xa_store_bh()`, `xa_store_irq()`, `xa_erase_bh()`, `xa_erase_irq()`, `xa_cmpxchg_bh()`, `xa_cmpxchg_irq()` 같은 helper가 제공됩니다.
Locking hierarchy에서 다른 mutex 위에 있는 lock 때문에 mutex로 XArray access를 보호해야 할 때도 있습니다. 그렇더라도 `xa_lock` 없이 `__xa_erase()` 같은 함수를 사용할 수 있는 것은 아닙니다. `xa_lock`은 lockdep validation에 쓰이며 앞으로 다른 목적으로도 사용될 예정입니다.
`__xa_set_mark()`와 `__xa_clear_mark()`는 entry를 lookup한 뒤 mark를 원자적으로 설정하거나 지울 때도 사용할 수 있습니다. 이 경우 advanced API를 사용하면 tree를 두 번 순회하지 않아도 되어 더 효율적일 수 있습니다.
고급 API
307-350고급 API
고급 API는 사용하기 더 어렵고 보호 장치가 적은 대신 더 높은 유연성과 성능을 제공합니다. 고급 API는 locking을 대신 수행하지 않으므로 배열을 수정할 때 `xa_lock`을 사용해야 합니다. Read-only operation에는 `xa_lock` 또는 RCU lock 중 하나를 선택할 수 있습니다. 같은 배열에서 고급 operation과 일반 operation을 섞어 쓸 수 있으며 실제로 일반 API는 고급 API로 구현됩니다. 고급 API는 GPL-compatible license를 가진 module에만 제공됩니다.
고급 API는 `xa_state`를 중심으로 구성됩니다. 이는 `XA_STATE()` macro를 사용해 stack에 선언하는 opaque 자료 구조입니다. Macro는 XArray 순회를 시작할 준비가 된 `xa_state`를 초기화합니다. `xa_state`는 XArray 안의 위치를 유지하는 cursor로 사용되므로 매번 root부터 다시 시작하지 않고 여러 operation을 조합할 수 있습니다. 내용은 `rcu_read_lock()` 또는 `xas_lock()`으로 보호합니다. State와 tree를 보호하는 lock을 내려놓아야 한다면, 이후 호출이 보호되지 않은 state 부분에 의존하지 않도록 `xas_pause()`를 호출해야 합니다.
`xa_state`는 error 저장에도 사용됩니다. `xas_error()`로 error를 가져옵니다. 모든 operation은 진행 전에 `xa_state`의 error 상태를 검사하므로 호출마다 error를 확인할 필요 없이 여러 호출을 연속 수행한 뒤 편리한 지점에서 한 번 검사할 수 있습니다. 현재 XArray code가 직접 생성하는 error는 `ENOMEM`과 `EINVAL`뿐이지만, 사용자가 `xas_set_err()`를 호출하는 경우를 위해 임의의 error를 지원합니다.
`xa_state`가 `ENOMEM` error를 보유하고 있을 때 `xas_nomem()`을 호출하면 지정한 GFP flag로 memory를 더 할당하여 다음 시도를 위해 `xa_state`에 cache합니다. 의도한 흐름은 `xa_lock`을 잡고 operation을 시도한 뒤 lock을 내려놓는 것입니다. Lock을 잡은 채 memory 할당을 시도하면 실패하기 쉽지만, lock을 내려놓은 뒤 `xas_nomem()`은 더 적극적으로 할당할 수 있습니다. Memory error가 있었고 추가 memory를 할당하여 재시도할 가치가 있으면 `true`를 반환합니다. 이전에 할당한 memory가 사용되지 않았고 error가 없거나 `ENOMEM`이 아닌 error라면 그 memory를 해제합니다.
내부 entry
351-391내부 entry
XArray는 자체 용도를 위해 일부 entry를 예약합니다. 일반 API에는 노출되지 않지만 고급 API를 사용하면 볼 수 있습니다. 보통은 이를 `xas_retry()`에 전달하고, 함수가 `true`를 반환하면 operation을 재시도하는 방식이 가장 좋습니다.
| 이름 | 검사 | 용도 |
|---|---|---|
| Node | xa_is_node() | XArray node입니다. Multi-index xa_state를 사용할 때 보일 수 있습니다. |
| Sibling | xa_is_sibling() | Multi-index entry의 non-canonical entry입니다. 값은 이 node에서 canonical entry가 있는 slot을 나타냅니다. |
| Retry | xa_is_retry() | `xa_lock`을 보유한 thread가 현재 수정 중인 entry입니다. 이 entry를 포함한 node는 현재 RCU period가 끝날 때 해제될 수 있으므로 array head부터 lookup을 다시 시작해야 합니다. |
| Zero | xa_is_zero() | 일반 API에서는 `NULL`로 보이지만 XArray 안의 entry를 차지하여 이후 사용을 위해 index를 예약합니다. 할당형 XArray에서 값이 `NULL`인 allocated entry에 사용됩니다. |
앞으로 다른 internal entry가 추가될 수 있습니다. 가능한 한 `xas_retry()`가 이를 처리할 것입니다.
추가 기능
392-452추가 기능
`xas_create_range()`는 범위의 모든 entry를 저장하는 데 필요한 memory를 할당합니다. Memory를 할당할 수 없으면 `xa_state`에 `ENOMEM`을 설정합니다.
`xas_init_marks()`는 entry의 mark를 기본 상태로 reset합니다. 보통 모든 mark가 clear된 상태이지만 XArray에 `XA_FLAGS_TRACK_FREE`가 설정되어 있으면 mark 0은 set되고 나머지는 clear됩니다. `xas_store()`로 entry를 교체해도 mark가 reset되지 않으므로 필요하다면 명시적으로 reset해야 합니다.
`xas_load()`는 가능한 한 entry에 가까운 위치까지 `xa_state`를 순회합니다. 이미 해당 entry까지 순회했고 entry가 바뀌지 않았는지 확인해야 한다면 `xas_reload()`로 function call 하나를 줄일 수 있습니다.
XArray의 다른 index로 이동하려면 `xas_set()`을 호출합니다. 이 함수는 cursor를 tree root로 reset하며 보통 다음 operation이 원하는 위치까지 cursor를 순회하게 합니다. 다음 또는 이전 index로 이동하려면 `xas_next()` 또는 `xas_prev()`를 호출합니다. Index 설정 자체는 cursor를 순회하지 않아 lock이 필요 없지만 다음이나 이전 index로 이동할 때는 lock이 필요합니다.
`xas_find()`로 다음 present entry를 검색할 수 있습니다. 이는 `xa_find()`와 `xa_find_after()`의 역할을 모두 수행합니다. Cursor가 이미 entry까지 순회했다면 현재 참조하는 entry 다음의 entry를 찾고, 그렇지 않으면 `xa_state`의 index에 있는 entry를 반환합니다. `xas_find()` 대신 `xas_next_entry()`로 다음 present entry로 이동하면 inline code가 늘어나는 대신 대부분의 경우 function call 하나를 줄일 수 있습니다.
`xas_find_marked()`도 비슷합니다. `xa_state`가 아직 순회되지 않았고 해당 index의 entry에 mark가 있으면 그 entry를 반환합니다. 이미 순회했다면 현재 참조하는 entry 다음의 첫 marked entry를 반환합니다. `xas_next_marked()`는 `xas_next_entry()`에 대응합니다.
`xas_for_each()` 또는 `xas_for_each_marked()`로 XArray 범위를 순회할 때 일시적으로 순회를 멈춰야 할 수 있습니다. 이를 위해 `xas_pause()`를 사용합니다. 필요한 작업을 마치고 재개하면 `xa_state`는 마지막으로 처리한 entry 다음부터 계속할 수 있는 상태입니다. Interrupt를 비활성화한 채 순회한다면 `XA_CHECK_SCHED`개 entry마다 순회를 멈추고 interrupt를 다시 활성화하는 것이 좋습니다.
`xas_get_mark()`, `xas_set_mark()`, `xas_clear_mark()`는 `xa_state` cursor가 XArray의 적절한 위치로 이동해 있어야 합니다. 바로 전에 `xas_pause()` 또는 `xas_set()`을 호출했다면 아무 작업도 하지 않습니다.
`xas_set_update()`를 호출하면 XArray가 node를 갱신할 때마다 callback function을 호출하게 할 수 있습니다. Page cache workingset code는 이를 사용해 shadow entry만 포함한 node 목록을 유지합니다.
Multi-index entry
453-505Multi-index entry
XArray는 여러 index를 묶어 한 index의 operation이 모든 index에 영향을 주게 할 수 있습니다. 어느 index에 저장해도 모든 index에서 가져오는 entry 값이 바뀌며, 어느 index에서 mark를 설정하거나 지워도 묶인 모든 index의 mark가 함께 바뀝니다. 현재 구현은 alignment가 맞는 power-of-two 범위만 묶을 수 있습니다. 예를 들어 64-127은 묶을 수 있지만 2-6은 묶을 수 없습니다. 이렇게 하면 상당한 memory를 절약할 수 있으며 512개 entry를 묶으면 4kB 넘게 줄일 수 있습니다.
`XA_STATE_ORDER()`를 사용하거나 `xas_set_order()` 뒤에 `xas_store()`를 호출하여 multi-index entry를 만듭니다. Multi-index `xa_state`로 `xas_load()`를 호출하면 tree의 올바른 위치까지 이동하지만 반환값은 의미가 없으며, 범위 안에 entry가 있어도 internal entry 또는 `NULL`일 수 있습니다. `xas_find_conflict()`는 범위 안의 첫 entry를 반환하고 entry가 없으면 `NULL`을 반환합니다. `xas_for_each_conflict()` iterator는 지정 범위와 겹치는 모든 entry를 순회합니다.
`xas_load()`가 multi-index entry를 만나도 `xa_state`의 `xa_index`는 바뀌지 않습니다. XArray를 순회하거나 `xas_find()`를 호출할 때 초기 index가 multi-index entry 중간에 있으면 그대로 유지됩니다. 이후 호출이나 순회에서는 index가 범위의 첫 index로 이동합니다. Entry가 차지하는 index 수와 관계없이 각 entry는 한 번만 반환됩니다.
Multi-index `xa_state`에 `xas_next()` 또는 `xas_prev()`를 사용하는 것은 지원되지 않습니다. Multi-index entry에 이 함수를 사용하면 sibling entry가 노출되므로 호출자가 건너뛰어야 합니다.
Multi-index entry의 어느 index에든 `NULL`을 저장하면 모든 index의 entry가 `NULL`이 되고 묶음이 해제됩니다. 더 작은 범위의 entry로 나누려면 `xa_lock` 없이 `xas_split_alloc()`을 호출한 뒤 lock을 잡고 `xas_split()`을 호출하거나, `xa_lock`을 잡은 채 `xas_try_split()`을 호출합니다. 원문의 비교 대상인 `xas_split_alloc()+xas_split()`과 `xas_try_alloc()` 가운데 전자는 원래 order에서 새 order로 한 번에 균일하게 분할하고, `xas_try_split()`은 지정 index를 포함한 entry를 반복적으로 비균일 분할합니다. `XA_CHUNK_SHIFT`가 6일 때 order-9 entry는 2^(9-6)=8 slot을 차지하므로 전자는 xa_node 8개가 필요합니다. `xas_try_split()`은 order-9 entry를 order-8 두 개로 나눈 다음 지정 index에 따라 그중 하나를 order-7 두 개로 나누는 과정을 order-0까지 반복합니다. Order-6 entry 분할에 새 xa_node가 필요하면 가능한 경우 하나를 할당하므로, 결과적으로 xa_node 8개 대신 1개만 필요합니다.
함수와 구조체
506-510함수와 구조체
XArray API와 구현의 kernel-doc reference는 다음 source에 있습니다.
.. kernel-doc:: include/linux/xarray.h
.. kernel-doc:: lib/xarray.c
요약과 해설
xarray.rst:1-510XArray는 큰 sparse pointer 배열을 radix-tree 계열 구조로 구현하여 cache 효율적인 순회, lockless RCU lookup, 동적 확장을 제공합니다. Index가 조밀할수록 효율적이며 page cache가 대표 사용자입니다.
일반 API는 내부 locking을 제공하며 `xa_store()`, `xa_load()`, `xa_erase()`, `xa_find()`와 mark 및 ID allocation helper를 사용합니다. Interrupt나 softirq와 공유할 때는 `XA_FLAGS_LOCK_IRQ` 또는 `XA_FLAGS_LOCK_BH`와 대응 helper를 선택해야 합니다.
고급 API는 `XA_STATE()`로 만든 `xa_state` cursor를 사용합니다. 호출자가 `xa_lock` 또는 RCU 보호를 책임지는 대신 반복 순회와 조합 operation에서 더 높은 성능과 유연성을 얻습니다.
Multi-index entry는 alignment가 맞는 power-of-two index 범위를 하나의 entry로 묶어 memory를 절약합니다. Split, mark, lookup의 범위 semantics와 internal entry 처리에서 `xas_retry()` 규칙을 지키는 것이 중요합니다.