Generic Radix Tree (lib/generic-radix-tree)

리눅스 커널의 Generic Radix Tree를 심층 분석합니다. include/linux/generic-radix-tree.hlib/generic-radix-tree.c에 구현된 페이지(Page) 크기 노드 기반의 확장 가능 배열(Expandable Array)로, 기존 Radix Tree/XArray와는 완전히 별개의 자료구조입니다. genradix_ptr/genradix_ptr_alloc 등 핵심 API, 트리 깊이 동적 증가 메커니즘, bcachefs와 io_uring에서의 실제 활용 사례, 성능 특성과 디버깅 기법까지 커널 소스 기반으로 분석합니다.

전제 조건: 메모리 관리(Memory Management) 개요Radix Tree (레거시), XArray 문서를 먼저 읽으면 도움이 됩니다. Generic Radix Tree는 이들과 독립적이지만, 비교를 위해 기본 개념을 알아두면 좋습니다.
일상 비유: Generic Radix Tree는 확장 가능한 서류함과 비슷합니다. 처음에는 서랍 하나(페이지 1개)에 번호순으로 서류를 넣습니다. 서랍이 가득 차면 서랍장(내부 노드)을 추가해서 여러 서랍을 연결합니다. 서류 번호만 알면 "몇 번째 서랍장 → 몇 번째 서랍 → 몇 번째 칸"으로 즉시 찾을 수 있습니다. 각 서랍의 크기가 정확히 페이지(4KB) 크기이므로 메모리 할당이 매우 효율적입니다.

핵심 요약

  • 확장 가능 배열 -- 정수 인덱스로 임의 크기 객체에 접근하는 단순한 자료구조입니다. 해시(Hash)나 검색 키가 아닌, 배열처럼 인덱스 기반으로 동작합니다.
  • 페이지 크기 노드 -- 모든 노드(리프와 내부 노드 모두)가 정확히 PAGE_SIZE(보통 4KB)이며, 단일 페이지 할당(__get_free_page)으로 관리됩니다.
  • 동적 깊이 증가 -- 인덱스 범위가 커지면 루트(Root) 위에 새 내부 노드를 삽입하여 트리 깊이를 늘립니다. 최대 깊이는 보통 4~5레벨이면 충분합니다.
  • Radix Tree/XArray와 별개 -- include/linux/radix-tree.hinclude/linux/xarray.h와는 전혀 다른 구현입니다. Kent Overstreet가 bcachefs를 위해 작성했습니다.
  • 주요 사용처 -- bcachefs(파일시스템 내부 배열), io_uring(요청 슬롯 관리), 기타 커널 서브시스템에서 단순한 확장 가능 배열이 필요할 때 사용됩니다.
  • 타입 안전 매크로 -- GENRADIX(type) 매크로로 타입 안전한 래퍼를 생성하며, 내부적으로 struct __genradix를 감싼 구조체를 만듭니다.

단계별 이해

  1. 필요성 파악
    커널에서 "인덱스로 접근하는 동적 배열"이 필요한 상황을 이해합니다. 일반 배열은 크기가 고정이고, Radix Tree/XArray는 포인터 저장에 최적화되어 있어 임의 크기 객체 저장에는 오버킬(Overkill)입니다.
  2. 노드 구조 이해
    리프 노드에는 실제 데이터 객체들이, 내부 노드에는 자식 포인터들이 저장됩니다. 모든 노드가 PAGE_SIZE라는 것이 핵심입니다.
  3. 인덱스 → 경로 변환
    인덱스를 트리 깊이에 따라 분할하여 각 레벨에서 어떤 슬롯을 선택할지 결정하는 과정을 이해합니다.
  4. API 학습
    genradix_ptr()로 읽기, genradix_ptr_alloc()로 쓰기(필요시 할당), genradix_for_each()로 순회하는 패턴을 익힙니다.
  5. 깊이 증가 메커니즘
    현재 트리 깊이로 표현 불가능한 인덱스가 들어오면 루트 위에 새 노드를 삽입하여 깊이를 늘리는 과정을 추적합니다.
  6. 실제 활용 사례
    bcachefs의 키 캐시, io_uring의 요청 관리 등 실제 커널 코드에서의 사용 패턴을 확인합니다.
관련 커널 소스: include/linux/generic-radix-tree.h (API 선언, 타입 안전 매크로), lib/generic-radix-tree.c (핵심 구현), fs/bcachefs/ (주요 사용처), io_uring/ (요청 슬롯 관리). Kent Overstreet가 bcachefs 개발 과정에서 작성하여 v5.0(2019년 3월) 커널에 lib/로 통합되었습니다.

개요: 왜 Generic Radix Tree인가

커널에는 이미 Radix Tree(include/linux/radix-tree.h)와 XArray(include/linux/xarray.h)라는 강력한 기수 트리(Radix Tree) 구현이 있습니다. 그런데 왜 별도의 Generic Radix Tree가 필요할까요?

설계 목적: 단순한 확장 가능 배열

기존 Radix Tree와 XArray는 키(Key)→포인터(Pointer) 매핑에 최적화되어 있습니다. 페이지 캐시(Page Cache)에서 오프셋(Offset)→페이지(Page) 매핑, IDR에서 ID→객체 매핑 등이 대표적입니다. 이들은 태그(Tag) 관리, 동시성 제어(RCU, spinlock), 예외 엔트리(Exception Entry) 등 풍부한 기능을 제공하지만, "단순히 인덱스로 접근하는 동적 배열"에는 과도한 복잡성입니다.

Generic Radix Tree는 이 틈새를 채웁니다:

특성Radix Tree / XArrayGeneric Radix Tree
저장 단위포인터(void *)임의 크기 객체(바이트 배열)
노드 크기가변 (kmalloc)고정 PAGE_SIZE
동시성RCU + spinlock 내장외부 잠금 필요
태그/마크지원미지원
API 복잡도높음 (~50+ 함수)낮음 (~10 함수)
메모리 할당kmalloc (슬랩 할당)__get_free_page (페이지 할당)
주 용도키→포인터 매핑인덱스→객체 배열
Generic Radix Tree: 확장 가능 배열 개념 일반 배열 (고정 크기) [0] [1] [2] ... [N-1] X 확장 불가 크기 N 고정, 초과 시 재할당 필요 Generic Radix Tree (동적 확장) Root (내부 노드) 리프 [0..N] 리프 [N+1..2N] NULL (미할당) 모든 노드 = PAGE_SIZE (4KB) 필요 시 깊이 증가 → 무제한 확장 __get_free_page()로 할당
Generic Radix Tree는 고정 크기 배열과 달리 필요에 따라 동적으로 확장됩니다

Kent Overstreet는 bcachefs 파일시스템 개발 과정에서 "인덱스로 임의 크기 구조체(Struct)에 접근하는 단순한 자료구조"가 필요했습니다. 기존 flex_array(이후 제거됨)나 자체 동적 배열 대신, 기수 트리 구조를 활용하되 모든 노드를 페이지 크기로 통일한 깔끔한 설계를 선택했습니다.

구조체 분석

Generic Radix Tree의 핵심 자료구조는 놀라울 정도로 간결합니다. 전체 구현이 헤더 파일과 소스 파일 합쳐서 약 300줄에 불과합니다.

struct __genradix (루트 구조체)

/* include/linux/generic-radix-tree.h */
struct __genradix {
    struct genradix_root *root;
};

루트 구조체는 단일 포인터 하나뿐입니다. 이 포인터의 하위 비트(Bit)에 트리의 깊이(Depth) 정보가 인코딩(Encoding)됩니다.

genradix_root 포인터 인코딩

/* include/linux/generic-radix-tree.h */
#define GENRADIX_DEPTH_MASK  ((unsigned long) 3)  /* 하위 2비트 */

static inline struct genradix_node *genradix_root_to_node(
    struct genradix_root *r)
{
    return (struct genradix_node *)
        ((unsigned long) r & ~GENRADIX_DEPTH_MASK);
}

static inline unsigned genradix_root_to_depth(
    struct genradix_root *r)
{
    return (unsigned long) r & GENRADIX_DEPTH_MASK;
}
설명 root 포인터의 하위 2비트에 트리 깊이가 저장됩니다. 페이지 할당은 항상 PAGE_SIZE 정렬이므로 하위 12비트가 0이며, 2비트를 깊이 정보로 활용해도 안전합니다. 깊이 0은 루트가 곧 리프 노드(단일 페이지)임을, 깊이 1은 루트가 내부 노드이고 자식이 리프임을 의미합니다.

struct genradix_node (노드 구조체)

/* lib/generic-radix-tree.c */
struct genradix_node {
    union {
        /* 리프 노드: 실제 데이터가 저장되는 바이트 배열 */
        u8    data[PAGE_SIZE];

        /* 내부 노드: 자식 노드 포인터 배열 */
        struct genradix_node *children[PAGE_SIZE / sizeof(struct genradix_node *)];
    };
};
설명

노드는 정확히 PAGE_SIZE 바이트입니다. union으로 두 가지 역할을 합니다:

  • 리프 노드: data[PAGE_SIZE] -- 실제 객체들이 연속으로 저장됩니다
  • 내부 노드: children[] -- 자식 노드 포인터 배열입니다. 64비트 시스템에서 포인터가 8바이트이므로, 4KB 페이지에 512개의 자식 포인터가 들어갑니다

별도의 플래그(Flag)로 리프/내부를 구분하지 않으며, 트리 깊이 정보로 어떤 레벨의 노드가 리프인지 결정합니다.

genradix_node: 페이지 크기 노드 (PAGE_SIZE = 4096 바이트) 리프 노드: u8 data[4096] obj[0] obj[1] obj[2] ... obj[N-1] N = PAGE_SIZE / obj_size 예: obj_size=64 → N=64개, obj_size=8 → N=512개 __get_free_page(GFP_KERNEL|__GFP_ZERO) 내부 노드: genradix_node *children[512] (64비트: PAGE_SIZE / sizeof(ptr) = 4096 / 8 = 512) child[0] child[1] child[2] ... 리프 노드 (4KB) 리프 노드 (4KB) NULL (미할당) 팬 아웃(Fan-out) = 512 (64비트) 깊이 1: 512 리프 = 512 x 4KB = 2MB 커버 깊이 2: 512 x 512 리프 = 1GB 커버 깊이 3: 512^3 리프 = 512GB 커버 대부분의 사용 사례에서 깊이 1~2로 충분
genradix_node는 union으로 리프 데이터와 내부 자식 포인터를 공유합니다

페이지당 객체 수 계산

/* include/linux/generic-radix-tree.h */
#define GENRADIX_NODE_SHIFT   PAGE_SHIFT  /* 12 (4KB) */
#define GENRADIX_NODE_SIZE    PAGE_SIZE   /* 4096 */

/* 한 리프 노드(페이지)에 들어가는 객체 수 */
#define GENRADIX_OBJS_PER_PAGE(type) \
    (PAGE_SIZE / sizeof(type))

/* 예시 계산 */
/* sizeof(u64) = 8  → 4096/8  = 512개/페이지 */
/* sizeof(u32) = 4  → 4096/4  = 1024개/페이지 */
/* struct bkey_cached (128B) → 4096/128 = 32개/페이지 */

핵심 API

Generic Radix Tree의 API는 매우 간결합니다. 타입 안전 래퍼 매크로를 통해 사용하며, 내부적으로 __genradix_* 함수를 호출합니다.

GENRADIX() 매크로: 타입 안전 선언

/* include/linux/generic-radix-tree.h */
#define GENRADIX(_type)                   \
struct {                                    \
    struct __genradix tree;                \
}

/* 사용 예 */
GENRADIX(struct my_entry) my_array;

/* 위 매크로가 확장되면: */
struct {
    struct __genradix tree;
} my_array;
설명 GENRADIX(type)는 익명 구조체를 만들어 타입 정보를 보존합니다. 이를 통해 genradix_ptr() 등의 매크로가 올바른 타입의 포인터를 반환할 수 있습니다. C에서 제네릭(Generic)을 흉내 내는 전형적인 패턴입니다.

genradix_init() / genradix_free()

/* 초기화: root를 NULL로 설정 */
#define genradix_init(_radix)              \
    __genradix_init(&(_radix)->tree)

static inline void __genradix_init(
    struct __genradix *radix)
{
    radix->root = NULL;
}

/* 해제: 모든 노드를 재귀적으로 free */
#define genradix_free(_radix)              \
    __genradix_free(&(_radix)->tree)

void __genradix_free(struct __genradix *radix);
설명 초기화는 root 포인터를 NULL로 설정하는 것이 전부입니다. 해제 시에는 트리의 모든 노드를 깊이 우선 탐색(DFS)으로 순회하며 free_page()로 반환합니다. genradix_free() 호출 후 root는 NULL로 리셋되므로 재사용 가능합니다.

genradix_ptr(): 읽기 전용 접근

/* 인덱스로 객체 포인터 반환 (할당하지 않음) */
#define genradix_ptr(_radix, _idx)         \
    ((__typeof__((_radix))0) *)          \
    __genradix_ptr(&(_radix)->tree,       \
        _idx, sizeof(*(_radix)0))

/* 내부 구현 */
void *__genradix_ptr(struct __genradix *radix,
                     size_t offset)
{
    struct genradix_root *r = radix->root;
    struct genradix_node *n;
    unsigned level;

    if (!r)
        return NULL;

    n     = genradix_root_to_node(r);
    level = genradix_root_to_depth(r);

    while (level) {
        unsigned idx = (offset >> (level * GENRADIX_NODE_SHIFT))
                     & (GENRADIX_NODE_SIZE / sizeof(void *) - 1);
        n = n->children[idx];
        if (!n)
            return NULL;
        level--;
    }

    return &n->data[offset & (GENRADIX_NODE_SIZE - 1)];
}
설명

genradix_ptr()는 인덱스에 해당하는 객체의 포인터를 반환합니다. 노드가 할당되지 않은 경우 NULL을 반환합니다.

동작 과정:

  1. root 포인터에서 노드 주소와 깊이를 추출합니다
  2. 각 레벨에서 오프셋의 해당 비트 범위를 슬롯 인덱스로 변환합니다
  3. 리프 레벨에 도달하면 오프셋의 하위 비트로 data[] 내 바이트 위치를 결정합니다

이 함수는 읽기 전용이며 어떤 메모리 할당도 수행하지 않습니다.

genradix_ptr_alloc(): 할당 포함 접근

/* 인덱스로 객체 포인터 반환 (필요시 노드 할당) */
#define genradix_ptr_alloc(_radix, _idx, _gfp) \
    ((__typeof__((_radix))0) *)               \
    __genradix_ptr_alloc(&(_radix)->tree,      \
        _idx, sizeof(*(_radix)0), _gfp)

void *__genradix_ptr_alloc(
    struct __genradix *radix,
    size_t offset,
    gfp_t gfp)
{
    struct genradix_root *r, *new_root;
    struct genradix_node *node, *new_node;
    unsigned level;

    /* 기존 트리에서 찾기 시도 */
restart:
    r = radix->root;

    if (r) {
        node  = genradix_root_to_node(r);
        level = genradix_root_to_depth(r);

        /* 현재 깊이로 인덱스 표현 가능한지 확인 */
        if (offset < genradix_depth_size(level)) {
            /* 경로를 따라 내려가며 필요한 노드 할당 */
            while (level) {
                unsigned idx = ...;
                if (!node->children[idx]) {
                    new_node = genradix_alloc_node(gfp);
                    if (!new_node)
                        return NULL;
                    node->children[idx] = new_node;
                }
                node = node->children[idx];
                level--;
            }
            return &node->data[offset & ...];
        }
    }

    /* 깊이 증가 필요: 새 루트 노드 삽입 */
    new_node = genradix_alloc_node(gfp);
    if (!new_node)
        return NULL;

    new_node->children[0] = node;
    new_root = (struct genradix_root *)
        ((unsigned long) new_node | (level + 1));

    if (cmpxchg(&radix->root, r, new_root) != r) {
        free_page((unsigned long) new_node);
        goto restart;
    }

    goto restart;
}
설명

genradix_ptr_alloc()genradix_ptr()의 확장 버전으로, 경로상에 없는 노드를 자동으로 할당합니다.

핵심 동작:

  1. 현재 깊이로 인덱스를 표현할 수 있으면, 경로를 따라 내려가며 NULL인 자식 슬롯에 새 노드를 할당합니다
  2. 현재 깊이로 인덱스를 표현할 수 없으면, 새 루트 노드를 만들어 기존 트리를 자식[0]에 연결하고 깊이를 1 증가시킵니다
  3. cmpxchg()로 루트 교체를 원자적(Atomic)으로 수행하여 레이스 컨디션(Race Condition)을 방지합니다

genradix_iter_peek() / genradix_for_each()

/* 반복자 구조체 */
struct genradix_iter {
    size_t offset;
    size_t pos;
};

#define genradix_iter_init(_radix, _idx)   \
    ((struct genradix_iter) {               \
        .offset = (_idx) * sizeof(...),    \
        .pos    = _idx,                       \
    })

/* 현재 위치의 객체를 반환하고, 없으면 다음 유효 객체로 이동 */
#define genradix_iter_peek(_radix, _iter)  \
    (__typeof__((_radix))0) *)              \
    __genradix_iter_peek(&(_radix)->tree, _iter, sizeof(...))

/* 전체 순회 매크로 */
#define genradix_for_each(_radix, _iter, _p) \
    for (_iter = genradix_iter_init(_radix, 0); \
         (_p = genradix_iter_peek(_radix, &_iter)) != NULL; \
         genradix_iter_advance(&_iter, sizeof(...)))
설명

genradix_for_each()는 트리의 모든 할당된 객체를 순차적으로 순회합니다.

genradix_iter_peek()은 NULL 자식을 만나면 해당 서브트리를 통째로 건너뛰므로, 희소(Sparse) 배열에서도 효율적입니다.

API 요약표

API기능할당반환값
GENRADIX(type)타입 안전 구조체 선언-구조체 타입
genradix_init(r)초기화 (root = NULL)없음void
genradix_free(r)전체 트리 해제없음void
genradix_ptr(r, idx)읽기 접근없음type * 또는 NULL
genradix_ptr_alloc(r, idx, gfp)쓰기 접근 (필요시 할당)페이지type * 또는 NULL
genradix_iter_peek(r, iter)반복자 현재 객체없음type * 또는 NULL
genradix_for_each(r, iter, p)전체 순회없음-

내부 구현

Generic Radix Tree의 내부 구현은 간결하면서도 정교합니다. 핵심은 인덱스를 트리 경로로 변환하는 방식과, 트리 깊이를 동적으로 늘리는 메커니즘입니다.

인덱스 → 경로 변환

바이트 오프셋(Byte Offset)을 트리 경로로 변환하는 과정을 살펴봅니다. 객체 인덱스는 먼저 바이트 오프셋으로 변환됩니다: offset = index * obj_size.

인덱스 → 트리 경로 변환 (64비트, 깊이 2) 바이트 오프셋 비트 분해: 레벨 2 인덱스 (비트 24~) 레벨 1 인덱스 (비트 12~23) = 9비트 리프 내 오프셋 (비트 0~11) = 12비트 예시: obj_size=64, index=1000 offset = 1000 * 64 = 64000 = 0xFA00 Root (depth=2) children[0] (0xFA00 >> 21) & 0x1FF = 0 내부 노드 children[15] (0xFA00 >> 12) & 0x1FF = 15 리프 노드 data[0xA00] = data[2560] 0xFA00 & 0xFFF = 0xA00 비트 분해 규칙 SHIFT = PAGE_SHIFT = 12 FANOUT = PAGE_SIZE/sizeof(ptr) = 512 레벨 n 인덱스: (offset >> (n*12)) & 511 리프 내 오프셋: offset & 0xFFF
바이트 오프셋의 각 12비트(또는 9비트) 구간이 트리의 각 레벨에서의 슬롯 인덱스가 됩니다
/* 깊이별 커버 가능한 최대 바이트 오프셋 */
static inline size_t genradix_depth_size(unsigned depth)
{
    return 1UL << (GENRADIX_NODE_SHIFT +
                   depth * (GENRADIX_NODE_SHIFT - ilog2(sizeof(void *))));
}

/* 64비트 시스템 (ptr=8B, PAGE_SIZE=4KB):
 * depth 0: 1 << 12         = 4KB     (리프 1개)
 * depth 1: 1 << (12+9)     = 2MB     (리프 512개)
 * depth 2: 1 << (12+18)    = 1GB     (리프 262144개)
 * depth 3: 1 << (12+27)    = 512GB   (리프 ~134M개)
 */

트리 깊이 동적 증가

인덱스 범위가 현재 트리 깊이로 표현할 수 없을 때, 루트 위에 새 내부 노드를 삽입하여 깊이를 늘립니다.

트리 깊이 증가: 루트 위에 새 노드 삽입 BEFORE: depth=0 리프 (root|0) 커버: 4KB (64개 obj, 64B 기준) idx=65 접근 AFTER: depth=1 새 Root (root|1) [0] [1] 기존 리프 새 리프 (할당) 커버: 2MB (512 리프 가능) 깊이 증가 과정 (원자적 교체) 1. new_node = genradix_alloc_node(gfp) -- 새 내부 노드 할당 2. new_node->children[0] = old_root_node -- 기존 트리를 첫 번째 자식으로 연결 3. new_root = new_node | (depth + 1) -- 깊이 +1 인코딩 4. cmpxchg(&radix->root, old, new_root) -- 원자적 교체
기존 트리를 새 루트의 children[0]에 연결하여 기존 데이터를 보존하면서 확장합니다
/* 깊이 증가 핵심 로직 (lib/generic-radix-tree.c) */
do {
    new_node = genradix_alloc_node(gfp);
    if (!new_node)
        return NULL;

    /* 기존 트리를 새 노드의 첫 번째 자식에 연결 */
    new_node->children[0] = node;

    /* 새 루트 = 새 노드 주소 | (깊이+1) */
    new_root = (struct genradix_root *)
        ((unsigned long) new_node | (level + 1));

    /* 원자적 교체: 다른 스레드와의 경합 방지 */
    if (cmpxchg_release(&radix->root, r, new_root) == r) {
        /* 성공: 깊이 증가 완료 */
        node  = new_node;
        level = level + 1;
        r     = new_root;
    } else {
        /* 실패: 다른 스레드가 먼저 변경함 → 새 노드 해제 후 재시도 */
        free_page((unsigned long) new_node);
        goto restart;
    }
} while (offset >= genradix_depth_size(level));
설명

깊이 증가는 다음과 같이 동작합니다:

  1. 새 내부 노드를 할당합니다
  2. 기존 루트 노드를 새 노드의 children[0]에 배치합니다. 이렇게 하면 기존 인덱스 0~N의 데이터는 새 트리에서도 동일한 위치에 있습니다
  3. cmpxchg_release()로 루트 포인터를 원자적으로 교체합니다. 다른 스레드가 동시에 깊이를 늘렸다면 실패하고 재시도합니다
  4. 필요한 깊이에 도달할 때까지 반복합니다

노드 할당과 해제

/* lib/generic-radix-tree.c */
static inline struct genradix_node *genradix_alloc_node(gfp_t gfp)
{
    return (struct genradix_node *)
        __get_free_page(gfp | __GFP_ZERO);
}

static void genradix_free_recurse(
    struct genradix_node *n, unsigned level)
{
    if (level) {
        unsigned i;
        for (i = 0; i < GENRADIX_NODE_SIZE / sizeof(void *); i++)
            if (n->children[i])
                genradix_free_recurse(n->children[i], level - 1);
    }
    free_page((unsigned long) n);
}

void __genradix_free(struct __genradix *radix)
{
    struct genradix_root *r = radix->root;

    if (r) {
        genradix_free_recurse(
            genradix_root_to_node(r),
            genradix_root_to_depth(r));
        radix->root = NULL;
    }
}
설명

노드 할당은 __get_free_page(GFP_KERNEL|__GFP_ZERO)로 단일 페이지를 할당합니다. __GFP_ZERO 플래그로 0 초기화되므로, 리프의 data[]는 0으로, 내부 노드의 children[]은 NULL로 초기화됩니다.

해제는 재귀적 DFS로 모든 노드를 free_page()합니다. 깊이가 최대 3~4 레벨이므로 재귀 깊이(Stack Depth)는 매우 얕습니다.

bcachefs 활용

Generic Radix Tree의 가장 중요한 사용처는 bcachefs 파일시스템입니다. Kent Overstreet가 bcachefs 개발을 위해 이 자료구조를 직접 작성했습니다.

키 캐시(Key Cache) 관리

/* fs/bcachefs/btree_key_cache_types.h */
struct bch_fs {
    /* ... */
    GENRADIX(struct btree_key_cache_freelist_entry)
        key_cache_freelist;
    /* ... */
};

bcachefs는 B-트리(B-Tree) 키 캐시의 프리리스트(Freelist)를 Generic Radix Tree로 관리합니다. 캐시 엔트리 수가 동적으로 변하므로 확장 가능 배열이 적합합니다.

스트라이프(Stripe) 관리

/* fs/bcachefs/ec_types.h */
struct ec_stripe_head;

struct bch_fs {
    /* 스트라이프 데이터를 인덱스로 관리 */
    GENRADIX(struct stripe) stripes;
    /* ... */
};

/* 스트라이프 접근 예 */
struct stripe *s = genradix_ptr(&c->stripes, idx);
if (s && s->alive) {
    /* 스트라이프 데이터 사용 */
}

오픈 버킷(Open Bucket) 추적

/* fs/bcachefs/alloc_types.h */
struct bch_fs {
    GENRADIX(struct bucket_gens) bucket_gens;
    /* 버킷 세대(Generation) 번호를 인덱스로 추적 */
};

/* 버킷 세대 조회 */
static inline u8 *bucket_gen(
    struct bch_dev *ca, size_t b)
{
    return genradix_ptr(&ca->bucket_gens, b);
}
bcachefs에서의 Generic Radix Tree 사용처 struct bch_fs GENRADIX(struct stripe) Erasure Coding 스트라이프 인덱스 = 스트라이프 번호 GENRADIX(freelist_entry) B-Tree 키 캐시 프리리스트 인덱스 = 프리리스트 슬롯 GENRADIX(struct bucket_gens) 버킷 세대 번호 추적 인덱스 = 버킷 번호 왜 Generic Radix Tree인가? 동적 크기 + 임의 구조체 저장 + 단순 API + 페이지 단위 할당(단편화 방지) + 개별 요소 삭제 불필요
bcachefs는 내부 데이터 구조 관리에 Generic Radix Tree를 광범위하게 활용합니다

기타 커널 활용

io_uring 활용

io_uring은 고성능 비동기 I/O 프레임워크(Framework)로, 요청(Request) 슬롯 관리에 Generic Radix Tree를 사용합니다.

/* io_uring/io_uring.c */
struct io_ring_ctx {
    /* ... */
    GENRADIX(struct io_rsrc_node *) rsrc_nodes;
    /* 리소스 노드를 인덱스로 관리 */
};

/* 리소스 노드 접근 */
struct io_rsrc_node **node_p;
node_p = genradix_ptr(&ctx->rsrc_nodes, index);
if (node_p && *node_p) {
    /* 리소스 노드 사용 */
}
설명 io_uring은 등록된 파일 디스크립터(File Descriptor), 버퍼(Buffer) 등의 리소스를 고정 테이블에서 관리합니다. 리소스 테이블 크기가 동적으로 변할 수 있으므로, Generic Radix Tree가 적합합니다. 인덱스 기반의 빠른 접근이 가능하고, 필요할 때만 페이지를 할당하여 메모리 효율적입니다.

기타 서브시스템

서브시스템사용 목적저장 타입
bcachefs스트라이프, 버킷 세대, 키 캐시struct stripe, u8, freelist_entry
io_uring리소스 노드 테이블struct io_rsrc_node *
kernel/pid.cPID 할당 비트맵 (일부 아키텍처)unsigned long

Generic Radix Tree의 사용처는 아직 많지 않습니다. 대부분의 커널 서브시스템은 기존 XArray나 Red-Black Tree, 해시 테이블(Hash Table) 등을 사용합니다. Generic Radix Tree는 "단순한 확장 가능 배열"이 필요한 특정 니치(Niche)에 적합합니다.

Radix Tree / XArray 비교

커널의 세 가지 기수 트리(Radix Tree) 계열 자료구조를 비교합니다.

특성Radix Tree (레거시)XArrayGeneric Radix Tree
헤더linux/radix-tree.hlinux/xarray.hlinux/generic-radix-tree.h
도입 시기2.5.x (2002)4.20 (2018)5.0 (2019)
작성자여러 기여자Matthew WilcoxKent Overstreet
키(Key) 타입unsigned longunsigned longsize_t (인덱스)
값(Value) 타입void *void *임의 크기 객체
노드 크기576B (64슬롯)576B (64슬롯)PAGE_SIZE (4096B)
노드 할당kmalloc (슬랩)kmalloc (슬랩)__get_free_page
동시성RCU + spinlockRCU + spinlock외부 잠금 (+ cmpxchg)
태그/마크3개 태그 비트마크(Mark) 비트없음
삭제개별 삭제 지원개별 삭제 지원전체 해제만
순회for_each 매크로xa_for_eachgenradix_for_each
코드량~2000줄~3500줄~300줄
주 용도레거시 (XArray로 대체)페이지 캐시, IDR확장 가능 배열
커널 기수 트리 계열 비교 Radix Tree (레거시) 키 → void* 매핑 슬롯 64개/노드 (576B) RCU + spinlock 내장 태그 3개 (dirty 등) 폐기 예정 → XArray로 마이그레이션 XArray 키 → void* 매핑 슬롯 64개/노드 (576B) RCU + spinlock 내장 마크 비트 + 예외 엔트리 현재 표준 페이지 캐시, IDR, 범용 Generic Radix Tree 인덱스 → 임의 객체 PAGE_SIZE 노드 (4096B) 외부 잠금 + cmpxchg 태그/마크 없음 니치 용도 bcachefs, io_uring 대체
Generic Radix Tree는 Radix Tree/XArray와 독립적인 별도 구현으로, 단순 확장 가능 배열에 특화되어 있습니다

언제 Generic Radix Tree를 선택하는가

  1. Q1: 키가 정수(인덱스)입니까?
    • 아니오 → 해시 테이블 또는 Red-Black Tree
    • 예 → Q2로 진행
  2. Q2: 저장하는 값이 포인터입니까?
    • 예 → XArray (가장 범용적)
    • 아니오 (임의 크기 구조체) → Q3로 진행
  3. Q3: 개별 삭제가 필요합니까?
    • 예 → XArray (포인터로 감싸서)
    • 아니오 → Q4로 진행
  4. Q4: RCU 보호 읽기가 필요합니까?
    • 예 → XArray
    • 아니오 → Q5로 진행
  5. Q5: 요소 수가 100개 이상입니까?
    • 아니오 → kmalloc 배열
    • 예 → Generic Radix Tree
설명

위 의사 결정 트리(Decision Tree)는 커널 개발자가 자료구조를 선택할 때의 가이드입니다. Generic Radix Tree는 "정수 인덱스 + 임의 크기 객체 + 개별 삭제 불필요 + RCU 불필요 + 대량 요소"라는 특정 조건을 모두 만족할 때 최적의 선택입니다.

조건 중 하나라도 맞지 않으면 XArray, 해시 테이블, Red-Black Tree 등 다른 자료구조가 더 적합합니다.

성능 특성

메모리 오버헤드

요소 수obj_size=64B페이지 수 (리프)내부 노드총 메모리오버헤드율
164B104KB6300%
644KB104KB0%
1006.4KB2112KB87%
100064KB16168KB6%
327682MB51212052KB0.1%
16M1GB262144513~1GB~0.0%

요소 수가 적을 때는 페이지 단위 할당의 낭비가 크지만, 수백 개 이상이면 오버헤드가 급격히 감소합니다. bcachefs에서처럼 수천~수만 개의 요소를 관리할 때 효율적입니다.

접근 시간 분석

깊이조건 (obj_size=64B)포인터 역참조캐시 라인 접근복잡도
depth=0요소 64개 이하1회 (root → leaf)1회O(1)
depth=1요소 ~32K개2회 (root → internal → leaf)2회O(1) (상수 2)
depth=2요소 ~16M개3회3회O(1) (상수 3)

일반적으로 O(log512(N))이며 이는 실질적으로 O(1)에 해당합니다. depth 3 이상은 거의 발생하지 않습니다.

설명

접근 시간은 트리 깊이에 비례하며, 깊이는 log512(N)입니다. 512의 높은 팬 아웃 덕분에:

  • 깊이 1: 최대 ~32K 요소 (64B 기준)
  • 깊이 2: 최대 ~16M 요소
  • 깊이 3: 최대 ~8G 요소

대부분의 실제 사용에서 깊이 1~2이므로, 포인터 역참조(Dereference) 2~3회로 접근 가능합니다. 이는 실질적으로 O(1) 상수 시간입니다.

캐시 효율

특성Generic Radix TreeXArray연결 리스트
순차 순회우수 (리프 내 연속)양호 (64슬롯 노드)열악 (노드 산재)
랜덤 접근양호 (2~3 dereference)양호 (2~3 dereference)O(N)
리프 활용률100% (빈 공간도 할당)가변 (희소 시 낭비)100% (노드당 1개)
프리페치 친화높음 (4KB 연속)중간 (576B 연속)낮음

Generic Radix Tree의 리프 노드는 4KB 페이지이므로, 순차 순회 시 하드웨어 프리페처(Prefetcher)가 효과적으로 동작합니다. 64B 객체 기준으로 한 리프 내 64개 객체가 연속 메모리에 위치합니다.

디버깅

구조 시각화

디버거(Debugger)에서 Generic Radix Tree의 상태를 확인하는 방법입니다.

/* crash/drgn 등의 디버거에서 구조 확인 */

/* 1. root 포인터에서 노드 주소와 깊이 추출 */
unsigned long root_val = (unsigned long)radix->root;
unsigned depth = root_val & 3;
struct genradix_node *node =
    (struct genradix_node *)(root_val & ~3UL);

pr_info("genradix: root=%px depth=%u node=%px\n",
        radix->root, depth, node);

/* 2. 내부 노드의 자식 포인터 덤프 */
if (depth > 0) {
    unsigned i;
    for (i = 0; i < PAGE_SIZE / sizeof(void *); i++) {
        if (node->children[i])
            pr_info("  children[%u] = %px\n",
                    i, node->children[i]);
    }
}

/* 3. 리프 노드의 데이터 덤프 */
if (depth == 0) {
    print_hex_dump(KERN_INFO, "genradix data: ",
        DUMP_PREFIX_OFFSET, 16, 1,
        node->data, 256, true);
}

일반적인 문제

문제증상원인해결
NULL 역참조genradix_ptr() 반환값 미검사미할당 인덱스 접근반환값 NULL 검사 필수
메모리 누수genradix_free() 미호출소멸자(Destructor) 누락리소스 해제 경로에 추가
동시성 버그데이터 손상외부 잠금 없이 동시 접근적절한 잠금(mutex 등) 추가
할당 실패genradix_ptr_alloc() NULL메모리 부족GFP 플래그 확인, 오류 처리
정렬 문제예상치 못한 데이터obj_size가 PAGE_SIZE 약수 아님패딩(Padding) 추가

디버깅 팁

/* 트리 통계 출력 헬퍼 */
static void genradix_dump_stats(
    struct __genradix *radix, size_t obj_size)
{
    struct genradix_root *r = radix->root;
    unsigned depth;
    size_t max_objs, objs_per_page;

    if (!r) {
        pr_info("genradix: empty (root=NULL)\n");
        return;
    }

    depth = genradix_root_to_depth(r);
    objs_per_page = PAGE_SIZE / obj_size;
    max_objs = genradix_depth_size(depth) / obj_size;

    pr_info("genradix: depth=%u obj_size=%zu "
            "objs_per_page=%zu max_objs=%zu\n",
            depth, obj_size, objs_per_page, max_objs);
}

소스 코드 워크스루

genradix_ptr_alloc()의 전체 실행 흐름을 추적합니다. 빈 트리에 인덱스 1000번째 요소(obj_size=64B)를 처음 삽입하는 시나리오입니다.

시나리오: 빈 트리에 인덱스 1000 쓰기

/* 사용자 코드 */
GENRADIX(struct my_obj) tree;  /* sizeof(my_obj) = 64 */
genradix_init(&tree);               /* root = NULL */

struct my_obj *p = genradix_ptr_alloc(
    &tree, 1000, GFP_KERNEL);
/* p는 이제 인덱스 1000의 my_obj를 가리킴 */

실행 흐름 분석

  1. 바이트 오프셋 계산: offset = 1000 * 64 = 64000 (0xFA00)
  2. root == NULL이므로 새 트리 생성 경로로 진입합니다
  3. 필요한 깊이 결정:
    • depth=0: genradix_depth_size(0) = 4096 → 64000 > 4096 (부족)
    • depth=1: genradix_depth_size(1) = 2097152 (2MB) → 64000 < 2MB (충분)
  4. 깊이 0으로 시작: 첫 리프 노드 할당 (__get_free_page)
    • root = leaf_node | 0 (depth=0)
  5. 깊이 증가: 64000 >= depth_size(0) 이므로
    • 새 내부 노드 할당
    • new_node->children[0] = 기존 리프
    • root = new_node | 1 (depth=1)
    • cmpxchg로 원자적 교체
  6. 경로 탐색: depth=1에서
    • 레벨 1 인덱스: (0xFA00 >> 12) & 0x1FF = 15
    • children[15] == NULL → 새 리프 노드 할당
    • children[15] = new_leaf
  7. 리프 내 위치: 0xFA00 & 0xFFF = 0xA00 = 2560
    • 반환: &leaf->data[2560]
    • 이 위치에 struct my_obj (64B)가 저장됩니다
genradix_ptr_alloc() 실행 흐름: 인덱스 1000, obj_size=64 1. root = NULL 빈 트리 2. 첫 리프 할당 root = leaf|0 3. 깊이 부족! 64000 >= 4096 (depth_size(0)) 4. 깊이 증가 root = new_node|1 restart → 재시도 depth=1 트리 구조 (restart 후) Root (내부 노드) | depth=1 children[0] 기존 리프 (4KB) children[1..14] NULL (미할당) children[15] ← 할당 필요! 새 리프 할당 5. 최종 결과 반환: &leaf->data[2560] offset 0xFA00 & 0xFFF = 0xA00 = 2560 할당 통계 __get_free_page 호출: 3회 (리프 1 + 내부 노드 1 + 리프 2) = 12KB 사용 cmpxchg: 1회 (깊이 증가 시)
빈 트리에서 인덱스 1000 접근 시 깊이 증가 + 리프 할당이 순차적으로 발생합니다

해제 경로 추적

genradix_free(&tree) 호출 시 내부 동작은 다음과 같습니다:

  1. root 포인터에서 depth=1, node 주소를 추출합니다
  2. genradix_free_recurse(root_node, 1)을 호출합니다
  3. level=1이므로 children[]을 순회합니다:
    • children[0] != NULLgenradix_free_recurse(children[0], 0) → level=0이므로 free_page(children[0])
    • children[1..14] == NULL → 건너뜁니다
    • children[15] != NULLgenradix_free_recurse(children[15], 0)free_page(children[15])
  4. free_page(root_node)로 내부 노드 자체를 해제합니다
  5. radix->root = NULL로 리셋합니다

free_page 호출: 3회 (할당과 동일)

사용 예제

커널 모듈에서 Generic Radix Tree를 사용하는 완전한 예제입니다.

#include <linux/module.h>
#include <linux/generic-radix-tree.h>

struct my_entry {
    u32 id;
    u32 flags;
    u64 timestamp;
    char name[48];
};  /* sizeof = 64 바이트 → PAGE_SIZE/64 = 64개/페이지 */

static GENRADIX(struct my_entry) entries;

static int __init my_init(void)
{
    struct my_entry *e;
    struct genradix_iter iter;
    int i;

    genradix_init(&entries);

    /* 100개 엔트리 추가 */
    for (i = 0; i < 100; i++) {
        e = genradix_ptr_alloc(&entries, i, GFP_KERNEL);
        if (!e) {
            pr_err("alloc failed at %d\n", i);
            goto err;
        }
        e->id = i;
        e->flags = 0;
        e->timestamp = ktime_get_ns();
        snprintf(e->name, sizeof(e->name),
                 "entry-%d", i);
    }

    /* 특정 엔트리 조회 */
    e = genradix_ptr(&entries, 42);
    if (e)
        pr_info("entry 42: id=%u name=%s\n",
                e->id, e->name);

    /* 전체 순회 */
    genradix_for_each(&entries, iter, e) {
        pr_info("[%zu] id=%u ts=%llu\n",
                iter.pos, e->id, e->timestamp);
    }

    return 0;
err:
    genradix_free(&entries);
    return -ENOMEM;
}

static void __exit my_exit(void)
{
    genradix_free(&entries);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
설명

이 예제는 Generic Radix Tree의 전형적인 사용 패턴을 보여줍니다:

  1. GENRADIX(type)로 타입 안전한 트리를 선언합니다
  2. genradix_init()으로 초기화합니다
  3. genradix_ptr_alloc()으로 요소를 추가합니다. GFP 플래그를 전달하여 할당 컨텍스트를 지정합니다
  4. genradix_ptr()로 특정 인덱스의 요소를 읽습니다
  5. genradix_for_each()로 모든 요소를 순회합니다
  6. genradix_free()로 전체 트리를 해제합니다

설계 세부사항

왜 PAGE_SIZE 노드인가

Generic Radix Tree가 슬랩(Slab) 할당 대신 페이지 할당을 사용하는 데는 명확한 이유가 있습니다.

할당 방식장점단점
kmalloc (슬랩)작은 객체에 효율적, 다양한 크기 지원외부 단편화, 슬랩 캐시 오버헤드
__get_free_page단편화 없음, 정렬 보장, 하위 비트 활용 가능최소 4KB 할당, 작은 트리에 낭비
/* 페이지 할당의 핵심 이점: 포인터 하위 비트 활용 */

/* __get_free_page()는 항상 PAGE_SIZE(4096) 정렬 반환
 * → 하위 12비트가 항상 0
 * → 하위 2비트를 깊이(depth) 저장에 활용 가능
 * → 별도의 깊이 필드 불필요 → struct __genradix가 포인터 1개로 충분 */

struct __genradix {
    struct genradix_root *root;
    /* 이것이 전부! 8바이트로 전체 트리를 관리 */
};

/* 만약 kmalloc을 사용했다면: */
struct __genradix_hypothetical {
    void *root;
    unsigned depth;        /* 별도 필드 필요 */
    size_t node_size;      /* 노드 크기도 저장 필요 */
};
설명

페이지 할당을 사용함으로써 얻는 이점:

  1. 포인터 태깅(Tagging): 페이지 정렬 보장으로 하위 비트를 메타데이터에 활용합니다. 루트 구조체가 포인터 1개(8B)로 충분해집니다.
  2. 단편화 방지: 모든 노드가 정확히 1페이지이므로 버디 할당자(Buddy Allocator)에서 외부 단편화가 없습니다.
  3. 높은 팬 아웃: 4KB/8B = 512개 자식 포인터로, 매우 얕은 트리로 큰 인덱스 범위를 커버합니다.
  4. 캐시 효율: 리프 노드 전체가 연속 4KB이므로 하드웨어 프리페치가 효과적입니다.

동시성 모델

Generic Radix Tree는 내부적으로 최소한의 원자적 연산만 사용하며, 전체 동시성 제어는 호출자에게 위임합니다.

/* 내부 원자적 연산: 깊이 증가 시만 cmpxchg 사용 */
if (cmpxchg_release(&radix->root, old_root, new_root) != old_root) {
    /* 다른 스레드가 먼저 깊이를 늘림 → 새 노드 해제 후 재시도 */
    free_page((unsigned long) new_node);
    goto restart;
}

/* 호출자 책임: 동시 접근 보호 */
struct my_data {
    struct mutex lock;
    GENRADIX(struct my_entry) entries;
};

/* 쓰기 경로 */
mutex_lock(&data->lock);
e = genradix_ptr_alloc(&data->entries, idx, GFP_KERNEL);
if (e)
    e->value = new_value;
mutex_unlock(&data->lock);

/* 읽기 경로: 쓰기와 동시에 읽으면 안 됨 (RCU 미지원) */
mutex_lock(&data->lock);
e = genradix_ptr(&data->entries, idx);
if (e)
    process(e);
mutex_unlock(&data->lock);
설명

XArray는 내부에 spinlock과 RCU를 내장하여 호출자가 동시성을 신경 쓰지 않아도 됩니다. 반면 Generic Radix Tree는:

  • cmpxchg: 깊이 증가 시 루트 교체에만 사용합니다. 이는 두 스레드가 동시에 깊이를 늘리려 할 때의 레이스만 방지합니다.
  • 노드 내 데이터 접근: 보호가 없습니다. 호출자가 mutex, spinlock, RCU 등으로 보호해야 합니다.
  • 설계 철학: "단순하게 유지하고, 복잡한 동시성은 호출자가 결정"입니다.

메모리 0 초기화의 중요성

/* 노드 할당 시 __GFP_ZERO 필수 */
static inline struct genradix_node *genradix_alloc_node(gfp_t gfp)
{
    return (struct genradix_node *)
        __get_free_page(gfp | __GFP_ZERO);
}

/* __GFP_ZERO가 보장하는 것:
 *
 * 내부 노드:
 *   children[0..511] = NULL
 *   → genradix_ptr()가 미할당 경로에서 안전하게 NULL 반환
 *   → genradix_free_recurse()가 NULL 자식을 건너뜀
 *
 * 리프 노드:
 *   data[0..4095] = 0
 *   → 새로 할당된 객체가 자동으로 0 초기화
 *   → 호출자가 memset() 불필요
 *   → genradix_ptr_alloc() 후 즉시 사용 가능 (0값이 안전한 경우)
 */

객체 크기 제약

/* 객체 크기 제약사항 */

/* 1. obj_size는 PAGE_SIZE 이하여야 함 */
BUILD_BUG_ON(sizeof(type) > PAGE_SIZE);

/* 2. PAGE_SIZE가 obj_size의 배수일 때 가장 효율적 */
/* PAGE_SIZE=4096, obj_size=64:  4096/64  = 64개 (낭비 0) */
/* PAGE_SIZE=4096, obj_size=100: 4096/100 = 40개 (낭비 96B) */
/* PAGE_SIZE=4096, obj_size=3000: 4096/3000 = 1개 (낭비 1096B) */

/* 3. 2의 거듭제곱 크기를 권장 */
/* 정렬이 자연스럽고, 나눗셈이 시프트로 최적화됨 */
struct good_entry {   /* 64B - 완벽 */
    u64 fields[8];
};

struct ok_entry {     /* 128B - 양호 */
    u64 fields[16];
};

struct wasteful_entry { /* 3000B - 비효율적 */
    char data[3000];
    /* 페이지당 1개만 저장, 1096B 낭비 */
};

반복자(Iterator) 내부 구현

Generic Radix Tree의 반복자는 희소(Sparse) 배열에서 효율적으로 동작하도록 설계되어 있습니다.

__genradix_iter_peek() 상세

/* lib/generic-radix-tree.c */
void *__genradix_iter_peek(
    struct genradix_iter *iter,
    struct __genradix *radix,
    size_t objs_per_page)
{
    struct genradix_root *r;
    struct genradix_node *n;
    unsigned level, i;

restart:
    r = radix->root;
    if (!r)
        return NULL;

    n     = genradix_root_to_node(r);
    level = genradix_root_to_depth(r);

    /* 현재 오프셋이 트리 범위를 초과하면 종료 */
    if (iter->offset >= genradix_depth_size(level))
        return NULL;

    /* 트리를 내려가며 리프 탐색 */
    while (level) {
        i = (iter->offset >> (level * GENRADIX_NODE_SHIFT))
            & (GENRADIX_ARY - 1);

        if (!n->children[i]) {
            /* NULL 서브트리 → 다음 서브트리로 건너뛰기 */
            iter->offset = round_up(iter->offset + 1,
                1UL << (level * GENRADIX_NODE_SHIFT));
            iter->pos = iter->offset / objs_per_page;
            goto restart;
        }

        n = n->children[i];
        level--;
    }

    return &n->data[iter->offset & (PAGE_SIZE - 1)];
}
설명

반복자의 핵심 최적화는 NULL 서브트리 건너뛰기입니다:

  • 트리를 내려가다 NULL 자식을 만나면, 해당 서브트리가 커버하는 전체 범위를 건너뜁니다
  • 예를 들어 깊이 1에서 children[5]가 NULL이면, 인덱스 5*512 ~ 6*512-1 범위를 한번에 건너뜁니다
  • 이 덕분에 희소 배열에서도 실제 데이터가 있는 영역만 효율적으로 순회합니다
  • round_up()으로 다음 서브트리의 시작 오프셋으로 점프한 뒤 restart로 루트부터 다시 탐색합니다
genradix_for_each: NULL 서브트리 건너뛰기 Root (depth=1) [0] 리프 [1] NULL [2] NULL [3] 리프 [4] NULL [5] 리프 ... 반복자 이동 경로 순회 건너뛰기 (1024개 객체) 순회 건너뛰기 (512개) 순회 NULL 서브트리를 O(1)로 건너뛰어, 희소 배열에서도 할당된 요소만 효율적으로 순회합니다
반복자는 NULL 서브트리를 감지하면 해당 범위 전체를 건너뛰고 다음 유효 서브트리로 점프합니다

순회 성능: 희소 vs 밀집

시나리오총 인덱스할당된 요소실제 방문 노드건너뛴 범위
밀집 배열10,00010,000리프 157개0
10% 밀도10,0001,000관련 리프만~9,000 요소
극히 희소1,000,00010리프 ~10개~999,990 요소

고급 사용 패턴

Lookup-or-Create 패턴

/* 일반적인 패턴: 먼저 읽기, 없으면 생성 */
struct my_entry *get_or_create_entry(
    GENRADIX(struct my_entry) *tree,
    size_t idx)
{
    struct my_entry *e;

    /* 빠른 경로: 이미 할당된 경우 */
    e = genradix_ptr(tree, idx);
    if (e && e->initialized)
        return e;

    /* 느린 경로: 할당 필요 */
    e = genradix_ptr_alloc(tree, idx, GFP_KERNEL);
    if (!e)
        return ERR_PTR(-ENOMEM);

    if (!e->initialized) {
        e->id = idx;
        e->initialized = true;
    }
    return e;
}

배치 초기화 패턴

/* 대량 요소 사전 할당 */
static int preallocate_entries(
    GENRADIX(struct my_entry) *tree,
    size_t count)
{
    size_t i;

    for (i = 0; i < count; i++) {
        struct my_entry *e;

        e = genradix_ptr_alloc(tree, i, GFP_KERNEL);
        if (!e)
            return -ENOMEM;

        /* 0 초기화는 __GFP_ZERO가 보장하므로
         * 기본값이 0인 필드는 별도 초기화 불필요 */
        e->id = i;
    }
    return 0;
}

다양한 타입 활용

/* 포인터 배열로 사용 */
GENRADIX(struct task_struct *) task_table;

/* 단순 정수 배열로 사용 */
GENRADIX(u64) counter_array;
/* PAGE_SIZE/8 = 512개/페이지 */

/* 비트맵으로 사용 */
GENRADIX(unsigned long) bitmap;
/* 각 unsigned long에 64비트 → 페이지당 512*64 = 32,768비트 */

/* 큰 구조체 */
struct big_entry {
    char data[1024];
};
GENRADIX(struct big_entry) big_table;
/* PAGE_SIZE/1024 = 4개/페이지 */

개별 삭제 불가: 대안

/* Generic Radix Tree는 개별 요소 삭제를 지원하지 않음!
 * genradix_free()로 전체 해제만 가능 */

/* 대안 1: 유효성 플래그 사용 */
struct my_entry {
    bool valid;     /* false면 "삭제됨"으로 취급 */
    u32 data;
};

/* "삭제" = valid 플래그를 false로 */
e = genradix_ptr(&tree, idx);
if (e)
    e->valid = false;

/* 대안 2: 세대 번호(generation) 사용 */
struct versioned_entry {
    u32 generation;  /* 전역 세대와 불일치하면 무효 */
    u32 data;
};

/* 대안 3: 0 값을 "비어있음"으로 간주 */
/* __GFP_ZERO 덕분에 미사용 슬롯은 자연스럽게 0 */
e = genradix_ptr(&tree, idx);
if (e && e->id != 0) {
    /* 유효한 엔트리 */
}
설명

Generic Radix Tree는 설계상 개별 요소의 삭제를 지원하지 않습니다. 이는 의도적인 단순화입니다:

  • 리프 노드에서 요소를 "삭제"해도, 빈 리프 노드를 자동 해제하는 메커니즘이 없습니다
  • 내부 노드의 자식 포인터를 NULL로 되돌리는 shrink 로직도 없습니다
  • 대신 genradix_free()로 전체 트리를 한번에 해제합니다

이 특성은 bcachefs처럼 "트랜잭션(Transaction) 단위로 전체 배열을 생성하고 사용 후 일괄 해제"하는 패턴에 적합합니다.

제한사항과 대안

알려진 제한사항

제한사항설명대안
개별 삭제 불가전체 해제(genradix_free)만 지원유효성 플래그 또는 XArray 사용
RCU 미지원읽기 경로에서도 외부 잠금 필요XArray (내장 RCU 지원)
태그/마크 없음dirty, writeback 등 상태 관리 불가XArray 마크 또는 별도 비트맵
키 검색 불가인덱스 기반만, 키→값 검색 불가해시 테이블, Red-Black Tree
축소 불가트리 깊이가 줄어들지 않음필요시 재생성
최소 4KB 할당소수 요소에 메모리 낭비작은 배열은 kmalloc 사용

사용하지 말아야 할 경우

깊이별 용량 분석

Generic Radix Tree의 용량은 객체 크기와 트리 깊이에 따라 결정됩니다. 아키텍처(Architecture)별 포인터 크기에 따라 팬 아웃이 달라집니다.

64비트 시스템 (포인터 8바이트)

깊이내부 노드 수최대 리프 수obj=8B 용량obj=64B 용량obj=128B 용량최대 바이트
00151264324KB
11512262,14432,76816,3842MB
2~513262,144134,217,72816,777,2168,388,6081GB
3~262,657~134M~68G~8G~4G512GB

32비트 시스템 (포인터 4바이트)

깊이팬 아웃최대 리프obj=64B 용량최대 바이트
0-1644KB
110241,02465,5364MB
210241,048,57667,108,8644GB
/* 팬 아웃 계산 */
#define GENRADIX_ARY  (PAGE_SIZE / sizeof(void *))

/* 64비트: 4096 / 8  = 512 */
/* 32비트: 4096 / 4  = 1024 */

/* 깊이별 최대 바이트 오프셋 */
/* depth_size(d) = PAGE_SIZE * GENRADIX_ARY^d */
/*
 * 64비트:
 *   d=0: 4096 * 1    = 4KB
 *   d=1: 4096 * 512  = 2MB
 *   d=2: 4096 * 512^2 = 1GB
 *   d=3: 4096 * 512^3 = 512GB
 *
 * 32비트:
 *   d=0: 4096 * 1     = 4KB
 *   d=1: 4096 * 1024  = 4MB
 *   d=2: 4096 * 1024^2 = 4GB (전체 주소 공간)
 */
설명

32비트 시스템에서는 팬 아웃이 1024로 더 크므로, 같은 깊이에서 더 많은 리프를 커버합니다. 그러나 주소 공간이 4GB로 제한되므로 깊이 2이면 전체 주소 공간을 커버할 수 있습니다.

64비트 시스템에서도 실제로 깊이 3 이상이 필요한 경우는 거의 없습니다. 깊이 2면 obj_size=64B 기준 약 1670만 개의 요소를 저장할 수 있으며, 이는 대부분의 커널 사용 사례에 충분합니다.

깊이별 용량 스케일 (64비트, obj_size=64B) d=0 64개 4KB x512 depth=1 32,768개 (32K) 2MB x512 depth=2 16,777,216개 (16M) 1GB x512 depth=3 ~8,589,934,592개 (8G) 512GB 대부분의 실제 사용 범위 (depth 0~2) 내부 노드 오버헤드 depth=1: 내부 노드 1개 (4KB) = 0.2% 오버헤드 depth=2: 내부 노드 ~513개 (2MB) = 0.2% 오버헤드
깊이가 1 증가할 때마다 용량이 512배(64비트)씩 늘어나, 매우 얕은 트리로 방대한 요소를 관리합니다

구현 역사

버전시기변경 사항
v5.02019-03초기 도입: Kent Overstreet가 lib/generic-radix-tree.c 추가
v5.1~5.x2019~2022bcachefs에서 내부 사용, API 안정화
v6.72023-12bcachefs가 mainline에 머지되면서 사용처 확대
v6.8+2024~io_uring 등 추가 서브시스템 채택, 버그 수정

Generic Radix Tree는 Kent Overstreet의 bcachefs 프로젝트에서 탄생했습니다. bcachefs는 10년 이상의 개발 끝에 v6.7에서 mainline Linux에 머지되었으며, 이와 함께 Generic Radix Tree의 중요성도 높아졌습니다. 코드 자체는 매우 안정적이며, 초기 도입 이후 큰 API 변경 없이 유지되고 있습니다.

flex_array와의 관계

Generic Radix Tree 도입 이전에는 lib/flex_array.c가 유사한 역할을 했습니다. flex_array는 가변 크기의 배열을 페이지 단위로 관리했지만, 다음과 같은 문제가 있었습니다:

특성flex_array (제거됨)Generic Radix Tree
최대 크기생성 시 고정동적 확장
축소미지원미지원 (전체 해제만)
사전 할당필수 또는 선택필요 시 자동
APIflex_array_get/putgenradix_ptr/ptr_alloc
타입 안전void * 반환매크로로 타입 유지
상태v5.x에서 제거됨현재 활성

flex_array가 제거된 후, 그 역할의 상당 부분을 Generic Radix Tree가 대체하고 있습니다. flex_array가 고정 최대 크기를 요구한 반면, Generic Radix Tree는 필요에 따라 트리 깊이를 늘려 사실상 무제한 확장이 가능합니다.

향후 방향

Generic Radix Tree의 현재 구현은 매우 안정적이며, 대규모 변경보다는 점진적 개선이 예상됩니다:

genradix_delete(radix, idx) — 개별 요소 삭제
현재 미지원입니다. 특정 인덱스의 원소만 제거하는 기능입니다.
genradix_count(radix) — 사용 중인 요소 수 카운트
현재 미지원입니다. 트리에 저장된 유효 원소의 수를 반환하는 기능입니다.
genradix_ptr_rcu(radix, idx) — RCU 보호 읽기
현재 미지원입니다. 잠금 없이 RCU 보호 하에 원소를 읽는 기능입니다.

현재는 이 기능들이 필요하면 XArray 사용을 권장합니다.

Generic Radix Tree 메모리 레이아웃 심층

Generic Radix Tree의 핵심은 모든 노드가 정확히 PAGE_SIZE(4096바이트)라는 점입니다. 이 절에서는 깊이별 노드 배치, 인덱스에서 경로로의 변환 과정, 실제 메모리 사용량을 수식과 함께 상세히 분석합니다.

팬아웃(Fanout) 계산

내부 노드는 자식 포인터 배열을 저장합니다. 64비트 시스템에서 포인터 크기는 8바이트이므로, 하나의 내부 노드가 가질 수 있는 자식 수(팬아웃)는 다음과 같습니다.

/* 팬아웃 계산 — include/linux/generic-radix-tree.h */

/* 내부 노드의 자식 포인터 수 */
#define GENRADIX_NODE_SHIFT   (PAGE_SHIFT)          /* 12 (4KB 페이지) */
#define GENRADIX_NODE_SIZE    (1UL << GENRADIX_NODE_SHIFT)  /* 4096 */

/* 포인터 크기 = sizeof(void *) = 8 (64-bit) */
/* 팬아웃 = PAGE_SIZE / sizeof(void *) = 4096 / 8 = 512 */
#define GENRADIX_ARY          (GENRADIX_NODE_SIZE / sizeof(void *))  /* 512 */

/* 리프 노드의 슬롯 수는 원소 크기에 따라 달라짐 */
/* objs_per_leaf = PAGE_SIZE / obj_size */
/*   obj_size =   8B → 512개 */
/*   obj_size =  64B →  64개 */
/*   obj_size = 256B →  16개 */
/*   obj_size = 512B →   8개 */

struct genradix_node {
    union {
        /* 리프 노드: 원시 데이터 바이트 */
        u8  data[GENRADIX_NODE_SIZE];

        /* 내부 노드: 자식 포인터 배열 (최대 512개) */
        struct genradix_node *children[GENRADIX_ARY];
    };
};
코드 설명
  • GENRADIX_NODE_SHIFT: 페이지 시프트 값(12)과 동일합니다. 모든 노드가 정확히 한 페이지를 차지하므로 __get_free_page()로 할당합니다.
  • GENRADIX_ARY (512): 내부 노드 하나가 가리킬 수 있는 자식 수입니다. 이 값이 트리의 분기 계수(Branching Factor)가 되며, 깊이당 주소 공간이 512배씩 확장됩니다.
  • genradix_nodeunion: 리프와 내부 노드가 동일한 메모리 레이아웃을 공유합니다. 리프일 때는 data[]로, 내부 노드일 때는 children[]으로 해석합니다. 노드 유형 구분은 트리 깊이 정보로 수행합니다.
  • 리프 노드의 슬롯 수는 저장할 객체 크기에 따라 달라집니다. 예를 들어 64바이트 구조체를 저장하면 한 리프에 64개를 넣을 수 있습니다.

깊이별 노드 배치

다음 다이어그램은 깊이 0부터 3까지의 트리 구조와 각 깊이에서 지원하는 최대 원소 수를 보여줍니다. 원소 크기가 64바이트인 경우를 기준으로 합니다.

Depth 0 최대 64개 원소 리프 (4KB) ×1 = 4KB Depth 1 최대 32,768개 내부 노드 (512 ptrs) 리프[0] 리프[1] 리프[511] ×513 = ~2MB Depth 2 최대 16,777,216개 루트 (512 ptrs) 내부[0] (512) 내부[1] (512) 내부[511] (512) 리프 리프 리프 ×262,657 = ~1GB Depth 3 최대 8,589,934,592개 루트 (512 ptrs) L2 내부[0..511] L1 내부[0..511] 리프[0..511] 깊이 3: 512³ × 64 = ~8.5B 원소 (이론적 최대, 실제로는 메모리 한계로 불가능) ×134M+ = ~512GB

인덱스 → 경로 변환

인덱스에서 트리 경로를 도출하는 과정은 바이트 오프셋 변환과 비트 분해로 이루어집니다. 다음은 원소 크기 64바이트에서 인덱스 1000을 조회하는 과정입니다.

/* 인덱스 → 경로 변환 과정 (obj_size = 64, depth = 1) */

/* 1단계: 바이트 오프셋 계산 */
size_t byte_offset = index * obj_size;
/* byte_offset = 1000 * 64 = 64000 = 0xFA00 */

/* 2단계: 필요한 깊이 결정 */
/* depth_size(d) = PAGE_SIZE << (PAGE_SHIFT * d) */
/* depth_size(0) = 4096 = 0x1000         → 64000 > 4096 (부족) */
/* depth_size(1) = 4096 << 12 = 16777216 → 64000 < 16MB  (충분) */
/* → 깊이 1이면 충분합니다 */

/* 3단계: 비트 분해로 경로 결정 */
/* PAGE_SHIFT = 12, 각 레벨은 PAGE_SHIFT(12) 비트씩 소비 */
/*
 * byte_offset = 0xFA00 = 0b 1111 1010 0000 0000
 *                              ↑↑↑↑          ↑↑↑↑
 *                         비트[20:12]     비트[11:0]
 *                         Level 1 idx    리프 내 오프셋
 */

/* Level 1 인덱스: (0xFA00 >> 12) & 0x1FF = 0xF = 15 */
unsigned level1_idx = (byte_offset >> PAGE_SHIFT) & (GENRADIX_ARY - 1);
/* level1_idx = 15 → root->children[15]로 이동 */

/* 리프 내 오프셋: 0xFA00 & 0xFFF = 0xA00 = 2560 */
unsigned leaf_offset = byte_offset & (PAGE_SIZE - 1);
/* leaf_offset = 2560 → &leaf->data[2560] 반환 */

/* 검증: 2560 / 64 = 40 → 리프 내 40번째 슬롯 */
/* 전체: children[15]의 40번째 = 15*64 + 40 = 1000번째 원소 ✓ */
코드 설명
  • 1단계: 인덱스에 객체 크기를 곱하여 바이트 오프셋을 구합니다. genradix는 내부적으로 바이트 단위로 주소를 관리합니다.
  • 2단계: genradix_depth_size(d)는 깊이 d에서 표현 가능한 최대 바이트 범위입니다. PAGE_SIZE << (PAGE_SHIFT * d)로 계산하며, 깊이가 1 늘어날 때마다 4096배 증가합니다.
  • 3단계: 바이트 오프셋을 PAGE_SHIFT(12비트)씩 나누어 각 레벨의 인덱스를 추출합니다. 이 방식은 기수(Radix) 트리의 핵심 원리이며, 나눗셈 대신 시프트/마스크 연산만으로 경로를 결정할 수 있어 매우 빠릅니다.
  • 마지막 0x1FF는 512-1 = 0b111111111로, 9비트 마스크입니다. 이를 통해 한 레벨의 인덱스가 0~511 범위로 제한됩니다.

실제 메모리 사용량 계산

원소 크기와 저장 개수에 따른 실제 메모리 사용량을 계산합니다. 내부 노드의 오버헤드를 포함한 정확한 값입니다.

메모리 사용량 계산 공식:

예제obj_size원소 수objs_per_leaf리프 수내부 노드총 메모리배열 대비 오버헤드
18B10,000512201 (루트)84KB+5% (배열 80KB)
264B10,000641571 (루트)632KB-1.3% (배열 640KB, 패딩 절약)
3256B100,000166,25014 (L1: 13 + L2 루트: 1)~24.5MB-2.1% (배열 25,600KB)

메모리 효율이 높은 이유:

  1. 노드가 정확히 PAGE_SIZE이므로 내부 단편화가 없습니다
  2. Slab 할당자 메타데이터 오버헤드가 없습니다
  3. 희소 배열(Sparse Array)일 때 미사용 영역에 메모리를 할당하지 않습니다
코드 설명
  • objs_per_leaf: 리프 노드 하나에 들어가는 객체 수입니다. PAGE_SIZE / obj_size로 계산하며, obj_sizePAGE_SIZE의 약수가 아니면 마지막 남는 바이트는 사용되지 않습니다.
  • 내부 노드 오버헤드: 깊이 1에서는 내부 노드가 1개뿐이므로 오버헤드가 4KB에 불과합니다. 깊이 2에서도 최대 513개(1 루트 + 512 L1)이므로 약 2MB입니다.
  • 배열 대비 오버헤드가 음수인 경우: PAGE_SIZE가 객체 크기의 정확한 배수이면, 일반 배열의 정렬 패딩보다 오히려 적은 메모리를 사용할 수 있습니다.
  • 희소 배열 장점: 인덱스 0과 999,999만 사용하더라도 2개의 리프 노드 + 내부 노드만 할당됩니다. 일반 배열은 전체 공간을 할당해야 합니다.

genradix_ptr_alloc 전체 경로 워크스루

__genradix_ptr_alloc은 Generic Radix Tree에서 가장 복잡한 함수입니다. 인덱스에 해당하는 슬롯의 포인터를 반환하되, 경로에 노드가 없으면 할당하고, 트리 깊이가 부족하면 루트 위에 새 노드를 삽입합니다. 이 절에서는 전체 실행 경로를 단계별로 추적합니다.

함수 전체 구조

/* lib/generic-radix-tree.c — __genradix_ptr_alloc 핵심 경로 */

void *__genradix_ptr_alloc(struct __genradix *radix,
                            size_t offset,
                            gfp_t gfp_mask)
{
    struct genradix_root *r, *new_root;
    struct genradix_node *node, *new_node;
    unsigned level;

    /* 1. 현재 루트와 깊이 읽기 */
    r = READ_ONCE(radix->root);
    /* root 포인터의 하위 비트에 깊이가 인코딩되어 있습니다 */

    /* 2. 트리가 비었거나 깊이가 부족하면 확장 */
    while (!r || offset >= genradix_depth_size(genradix_root_to_depth(r))) {
        new_node = (struct genradix_node *)
            __get_free_page(gfp_mask | __GFP_ZERO);
        if (!new_node)
            return NULL;

        if (!r) {
            /* 빈 트리: 새 노드를 depth=0 루트로 설정 */
            new_root = genradix_depth_to_root(0, new_node);
        } else {
            /* 깊이 확장: 새 노드를 루트로, 기존 루트를 자식[0]으로 */
            new_node->children[0] = genradix_root_to_node(r);
            new_root = genradix_depth_to_root(
                genradix_root_to_depth(r) + 1, new_node);
        }

        /* 3. cmpxchg로 원자적 루트 교체 시도 */
        if (cmpxchg(&radix->root, r, new_root) != r) {
            /* 다른 스레드가 먼저 교체 → 할당 해제 후 재시도 */
            free_page((unsigned long)new_node);
            r = READ_ONCE(radix->root);
            continue;
        }
        r = new_root;
    }

    /* 4. 루트에서 리프까지 경로 탐색 */
    node = genradix_root_to_node(r);
    level = genradix_root_to_depth(r);

    while (level--) {
        unsigned idx = (offset >> (PAGE_SHIFT * (level + 1)))
                       & (GENRADIX_ARY - 1);
        struct genradix_node **child = &node->children[idx];

        if (!*child) {
            /* 중간 노드 또는 리프 할당 */
            new_node = (struct genradix_node *)
                __get_free_page(gfp_mask | __GFP_ZERO);
            if (!new_node)
                return NULL;
            *child = new_node;
        }
        node = *child;
    }

    /* 5. 리프 노드 내 바이트 오프셋 위치 반환 */
    return &node->data[offset & (PAGE_SIZE - 1)];
}
코드 설명
  • 루트 포인터 인코딩: radix->root는 노드 포인터와 깊이 정보를 하나의 값에 담습니다. 페이지 정렬된 주소의 하위 비트(최소 12비트가 0)를 활용하여 깊이(0~4)를 인코딩합니다.
  • while 루프(단계 2): 트리가 비어있거나 현재 깊이로 offset을 수용할 수 없으면 반복합니다. 매 반복마다 깊이가 1씩 증가하고, 기존 트리 전체가 새 루트의 children[0]에 매달립니다.
  • cmpxchg(단계 3): 락을 사용하지 않는 대신, CAS(Compare-And-Swap) 연산으로 동시성을 보장합니다. 경쟁에 실패하면 할당한 노드를 해제하고 재시도합니다. 이 덕분에 읽기(genradix_ptr)는 완전히 락프리입니다.
  • 경로 탐색(단계 4): 깊이에서 리프까지 내려가며, 각 레벨에서 오프셋의 해당 비트 범위를 추출하여 자식 인덱스를 결정합니다. NULL 자식을 만나면 새 페이지를 할당합니다.
  • 반환값(단계 5): 리프 노드의 데이터 영역 내에서 정확한 바이트 위치를 포인터로 반환합니다. 호출자는 이를 원하는 타입으로 캐스팅하여 사용합니다.

깊이 확장 과정

트리 깊이가 부족할 때의 확장 과정을 3단계로 보여줍니다. 기존 트리의 데이터를 이동하지 않고, 루트 위에 새 노드를 추가하는 것이 핵심입니다.

① Depth 0 (초기) 리프 노드 A data[0..4095] root→ 인덱스 0~63 저장 가능 (obj_size=64 기준) 확장 ② Depth 1 (확장 후) 새 내부 노드 B root→ 노드 A (리프) children[1..511] = NULL [0] 인덱스 0~32,767 저장 가능 확장 ③ Depth 2 (재확장) 새 내부 노드 C root→ 노드 B (내부) [0] 노드 A (리프) [0] 인덱스 0~16,777,215 깊이 확장 핵심 원리 • 기존 노드를 이동하지 않습니다. 새 루트 노드의 children[0]에 기존 루트를 연결할 뿐입니다. • cmpxchg로 루트 포인터를 원자적으로 교체하므로 읽기 측(genradix_ptr)은 항상 일관된 트리를 봅니다. • 교체에 실패하면(다른 스레드가 먼저 확장) 새 노드를 해제하고 재시도합니다. 비관적 락 없이 동작합니다. • 깊이 인코딩: root 포인터 하위 비트에 깊이를 저장 → 추가 메모리 없이 깊이 정보를 유지합니다.

cmpxchg 동시성 상세

스레드 A와 B가 동시에 깊이 확장을 시도하는 시나리오:

초기 상태: root = (leaf_node | depth=0)

단계스레드 A스레드 B
1r = READ_ONCE(root) → r = (leaf | 0)r = READ_ONCE(root) → r = (leaf | 0)
2new_A = alloc_page()
new_A->children[0] = leaf
new_B = alloc_page()
new_B->children[0] = leaf
3cmpxchg(&root, r, (new_A|1))
→ 성공! root = (new_A | 1)
cmpxchg(&root, r, (new_B|1))
→ 실패! (root != r)
4free_page(new_B) (할당 해제)
r = READ_ONCE(root) → (new_A | 1)
깊이 1이 충분하면 탐색을 진행합니다

이 패턴의 장점:

  1. spinlock/mutex 없이 동작하므로 읽기 경로에 오버헤드가 없습니다
  2. 경쟁 실패 시 비용은 한 페이지 할당+해제뿐이며, 이는 드문 경우에만 발생합니다
  3. ABA 문제가 없습니다. 깊이는 단조 증가하므로 같은 값으로 돌아오지 않습니다
코드 설명
  • READ_ONCE: 컴파일러 최적화(레지스터 캐싱 등)를 방지하여 항상 메모리에서 최신 값을 읽습니다. volatile과 유사하지만 커널에서 권장하는 방식입니다.
  • cmpxchg: cmpxchg(&root, old, new)는 "root == old이면 root = new로 바꾸고 old 반환, 아니면 현재 root 값 반환"을 원자적으로 수행합니다. x86에서는 LOCK CMPXCHG 명령어로 구현됩니다.
  • ABA 문제 면역: 깊이가 0→1→2로만 변하고 절대 감소하지 않으므로, r이 같은 값으로 돌아오는 ABA 문제가 발생하지 않습니다. genradix_free는 별도 동기화가 필요합니다.
  • 비관적 vs 낙관적: 이 방식은 낙관적(Optimistic) 동시성 제어입니다. 대부분 경쟁이 없으므로 한 번에 성공하고, 드물게 경쟁이 발생하면 재시도 비용(페이지 할당+해제)을 감수합니다.

genradix_iter와 순회 내부

Generic Radix Tree의 반복자(Iterator)는 트리의 모든 유효한 원소를 순서대로 방문합니다. 이 절에서는 struct genradix_iter의 내부 구조, genradix_iter_peek의 노드 탐색 알고리즘, genradix_for_each 매크로의 확장 결과를 분석합니다.

struct genradix_iter 분석

/* include/linux/generic-radix-tree.h — 반복자 구조체 */

struct genradix_iter {
    size_t           offset;    /* 현재 바이트 오프셋 */
    size_t           pos;       /* 현재 논리 인덱스 (offset / obj_size) */
};

/* 초기화 매크로 */
#define genradix_iter_init(_radix, _idx)    \
    ((struct genradix_iter) {                \
        .offset = (_idx) * (_radix)->_type_size, \
        .pos    = (_idx),                        \
    })

/* 핵심 이해: offset과 pos의 관계 */
/* offset = pos * obj_size */
/* offset은 트리 내부 탐색에, pos는 사용자 콜백에 사용됩니다 */

/* 예: obj_size=64, pos=100 → offset=6400 */
/*     offset으로 트리 경로를 계산하고, */
/*     pos를 사용자에게 현재 인덱스로 전달합니다 */
코드 설명
  • offset: 트리 내부의 바이트 단위 위치입니다. 경로 계산(비트 분해)에 직접 사용됩니다.
  • pos: 사용자가 보는 논리적 인덱스입니다. genradix_for_each 콜백에서 현재 몇 번째 원소인지 알려줍니다.
  • 두 필드를 분리한 이유는 성능입니다. 매번 곱셈/나눗셈을 하는 대신, 순회 중에 offset += obj_sizepos++를 동시에 증가시킵니다.

genradix_iter_peek 내부 동작

/* lib/generic-radix-tree.c — __genradix_iter_peek 핵심 로직 */

void *__genradix_iter_peek(struct genradix_iter *iter,
                            struct __genradix *radix,
                            size_t objs_per_leaf)
{
    struct genradix_root *r;
    struct genradix_node *node;
    unsigned level, i;

restart:
    r = READ_ONCE(radix->root);
    if (!r)
        return NULL;

    /* 현재 오프셋이 트리 범위를 넘으면 종료 */
    if (iter->offset >= genradix_depth_size(genradix_root_to_depth(r)))
        return NULL;

    node = genradix_root_to_node(r);
    level = genradix_root_to_depth(r);

    while (level) {
        i = (iter->offset >> (PAGE_SHIFT * level)) & (GENRADIX_ARY - 1);

        if (!node->children[i]) {
            /* NULL 서브트리 발견: 다음 유효한 서브트리로 건너뛰기 */
            size_t skip_size = 1UL << (PAGE_SHIFT * level);
            iter->offset = (iter->offset | (skip_size - 1)) + 1;
            iter->pos    = iter->offset / (PAGE_SIZE / objs_per_leaf);
            goto restart;
        }

        node = node->children[i];
        level--;
    }

    /* 리프 도달: 데이터 포인터 반환 */
    return &node->data[iter->offset & (PAGE_SIZE - 1)];
}
코드 설명
  • restart 레이블: NULL 노드를 만나면 오프셋을 건너뛰고 처음부터 경로를 다시 탐색합니다. 이 방식은 단순하지만, 깊이가 최대 4~5이므로 성능 문제가 되지 않습니다.
  • NULL 서브트리 건너뛰기: 핵심 최적화입니다. 희소 배열에서 비어있는 거대 영역을 한 번에 건너뜁니다. skip_size는 해당 레벨의 서브트리가 커버하는 바이트 범위입니다.
  • 오프셋 라운딩: (offset | (skip_size - 1)) + 1은 현재 서브트리의 끝으로 이동한 뒤 다음 서브트리의 시작으로 진행합니다. 비트 OR 연산으로 해당 레벨의 모든 하위 비트를 1로 만든 뒤 +1합니다.
  • pos 재계산: 오프셋을 건너뛴 후 pos도 동기화해야 합니다. objs_per_leaf를 이용하여 바이트 오프셋에서 논리 인덱스를 역산합니다.

genradix_for_each 매크로 확장

/* include/linux/generic-radix-tree.h — genradix_for_each 매크로 */

#define genradix_for_each(_radix, _iter, _p)                    \
    for (_iter = genradix_iter_init(_radix, 0);                  \
         (_p = genradix_iter_peek(&_iter, _radix)) != NULL;     \
         genradix_iter_advance(&_iter, _radix))

/* 확장 결과 (obj_size = 64 기준): */
/*
 * for (_iter = (struct genradix_iter){ .offset = 0, .pos = 0 };
 *      (_p = __genradix_iter_peek(&_iter, &radix->tree, 64)) != NULL;
 *      _iter.offset += 64, _iter.pos++) {
 *     // _p는 현재 원소를 가리키는 포인터
 *     // _iter.pos는 현재 논리 인덱스
 * }
 */

/* genradix_for_each_from — 특정 인덱스부터 시작 */
#define genradix_for_each_from(_radix, _iter, _idx, _p)          \
    for (_iter = genradix_iter_init(_radix, _idx);               \
         (_p = genradix_iter_peek(&_iter, _radix)) != NULL;     \
         genradix_iter_advance(&_iter, _radix))

/* 사용 예: 인덱스 500부터 순회 */
struct my_obj *obj;
struct genradix_iter iter;

genradix_for_each_from(&my_radix, iter, 500, obj) {
    pr_info("index %zu: value = %d\n", iter.pos, obj->value);
    if (iter.pos >= 1000)
        break;  /* 범위 제한 순회 */
}
코드 설명
  • genradix_for_each는 표준적인 C 매크로 for 루프 패턴입니다. 초기화에서 iter를 설정하고, 조건에서 peek으로 다음 유효 원소를 찾고, 증가에서 오프셋과 인덱스를 전진시킵니다.
  • peek이 NULL을 반환하면 더 이상 유효한 원소가 없거나 트리 범위를 넘었다는 의미이므로 루프가 종료됩니다.
  • genradix_for_each_from은 시작 인덱스를 지정할 수 있어, 부분 순회나 이어하기(Resume)에 유용합니다.
  • 루프 중간에 break로 탈출하는 것은 안전합니다. 별도의 정리(Cleanup) 작업이 필요하지 않습니다.
genradix_for_each 순회 경로 (depth=1, 희소 배열) 내부 노드 (children[0..511]) 리프[0] ✓ [1] NULL [2] NULL 리프[3] ✓ [4] NULL 리프[5] ✓ 순회 순서: ① 리프[0] 64개 원소 순회 ② [1],[2] NULL → 건너뛰기! ③ 리프[3] 64개 원소 순회 ④ [4] 건너뛰기 • 실선 화살표: 유효 노드 순회 (리프 내 모든 슬롯을 순서대로 방문) • 점선 화살표: NULL 노드 건너뛰기 (offset을 다음 서브트리 시작으로 점프 → restart) • 희소 배열에서 NULL 서브트리를 한 번에 건너뛰므로, 실제 데이터가 있는 리프만 방문합니다. • 리프[0]에 64개, 리프[3]에 64개만 있으면 128번 반복 (512×6=3072번 아님)

bcachefs에서의 실전 활용 상세

Generic Radix Tree는 Kent Overstreet가 bcachefs 파일시스템을 위해 만든 자료구조입니다. 이 절에서는 bcachefs에서 genradix가 구체적으로 어떤 구조체에, 어떤 목적으로 사용되는지, 그리고 왜 XArray 대신 genradix를 선택했는지를 분석합니다.

스트라이프(Stripe) 관리

/* fs/bcachefs/ec_types.h, fs/bcachefs/ec.h — 스트라이프 관리 */

/* bcachefs의 Erasure Coding (EC) 스트라이프 정보 */
struct stripe {
    size_t      heap_idx;       /* 힙에서의 위치 */
    u16         nr_blocks;      /* 스트라이프 내 블록 수 */
    u16         nr_redundant;   /* 패리티 블록 수 */
    u16         blocks_nonempty;/* 비어있지 않은 블록 수 */
    unsigned    alive:1;        /* 활성 여부 */
    unsigned    on_heap:1;      /* 힙에 존재 여부 */
};

/* bch_fs 구조체 내에서의 선언 (fs/bcachefs/bcachefs.h) */
struct bch_fs {
    /* ... 다른 필드들 ... */

    struct {
        /* 스트라이프 인덱스로 스트라이프 정보 조회 */
        GENRADIX(struct stripe)  stripes;

        /* 스트라이프 힙: GC 대상 선정에 사용 */
        HEAP(struct stripe_heap_entry) heap;

        size_t                     nr_stripes;
    } ec_stripes;

    /* ... */
};

/* GENRADIX(struct stripe)는 다음과 같이 확장됩니다: */
/*
 * struct {
 *     struct __genradix tree;
 * }
 *
 * sizeof(struct stripe)가 작으므로 (보통 16~24B)
 * 한 리프에 170~256개 스트라이프 정보를 저장할 수 있습니다.
 */

/* 스트라이프 접근 패턴 */
static inline struct stripe *genradix_ptr(
    &c->ec_stripes.stripes, stripe_idx)
{
    /* 스트라이프 번호로 즉시 접근: O(깊이) = O(1~3) */
}

/* 스트라이프 순회: 모든 활성 스트라이프 확인 */
struct stripe *s;
struct genradix_iter iter;

genradix_for_each(&c->ec_stripes.stripes, iter, s) {
    if (s->alive && s->blocks_nonempty < s->nr_blocks)
        /* GC 후보: 부분적으로 비어있는 스트라이프 */
        bch2_stripe_queue_gc(c, iter.pos);
}

스냅샷(Snapshot) 관리

/* fs/bcachefs/snapshot.h — 스냅샷 테이블 */

struct snapshot_t {
    u32     parent;         /* 부모 스냅샷 ID */
    u32     children[2];    /* 자식 스냅샷 (최대 2개: 바이너리 트리) */
    u32     depth;          /* 스냅샷 트리 깊이 */
    u32     skip[3];        /* 스킵 리스트 포인터 */
    u32     equiv;          /* 동등 스냅샷 (live 아닌 경우) */
    bool    live;           /* 활성 스냅샷 여부 */
};

/* bch_fs 내 스냅샷 테이블 선언 */
struct bch_fs {
    struct {
        GENRADIX(struct snapshot_t)  table;
        /* 스냅샷 ID로 인덱싱: table[snapshot_id] */
    } snapshots;
};

/* 왜 XArray가 아닌 genradix인가? */
/*
 * 1. 단순성: genradix는 ~200줄, XArray는 ~4000줄
 *    → 버그 표면적이 적고 검증이 쉽습니다
 * 2. 객체 직접 저장: XArray는 포인터만 저장, genradix는 임의 크기 객체 저장
 *    → kmalloc 없이 genradix 노드 안에 직접 저장됩니다
 * 3. GFP_KERNEL 전용: genradix는 슬립 가능 컨텍스트에서만 사용
 *    → IRQ/atomic 안전이 불필요하므로 락 설계가 단순합니다
 * 4. 안정적 포인터: 한번 할당된 슬롯의 주소가 변하지 않습니다
 *    → XArray는 내부 재조정으로 포인터가 무효화될 수 있습니다
 * 5. 일관된 성능: 트리 리밸런싱이 없어 최악 시간이 예측 가능합니다
 */
코드 설명
  • GENRADIX(struct stripe): 타입 안전 래퍼를 생성합니다. genradix_ptr(&stripes, idx)struct stripe *를 반환하며, 내부에서 obj_size = sizeof(struct stripe)로 바이트 오프셋을 계산합니다.
  • 스트라이프 인덱스: EC(Erasure Coding)에서 각 스트라이프는 고유 번호를 가집니다. 이 번호가 genradix의 인덱스로 사용되어 O(1)~O(3) 조회가 가능합니다.
  • 스냅샷 테이블: bcachefs는 COW(Copy-on-Write) 파일시스템으로, 스냅샷 ID를 키로 스냅샷 메타데이터를 조회합니다. 스냅샷 ID는 순차적으로 증가하므로 배열형 접근이 자연스럽습니다.
  • XArray 대신 genradix를 선택한 결정적 이유: XArray는 void * 포인터만 저장하므로, 작은 구조체를 저장하려면 별도 kmalloc이 필요합니다. genradix는 리프 노드에 직접 객체를 저장하여 할당 횟수를 크게 줄입니다.
bcachefs EC 스트라이프 관리에서의 genradix struct bch_fs ec_stripes.stripes GENRADIX(struct stripe) ec_stripes.heap ec_stripes.nr_stripes 내부 노드 (root) 리프[0] (4KB) stripe[0..170] 각 24B × 170개 리프[1] (4KB) stripe[171..341] struct stripe (24B) heap_idx: 42 GC 힙 위치 nr_blocks: 6 4+2 (데이터+패리티) nr_redundant: 2 패리티 블록 blocks_nonempty: 5 사용 중 블록 alive: 1, on_heap: 1 상태 플래그 확대 genradix vs XArray 비교 (스트라이프 1,000개 저장 시) genradix: 리프 6개 + 내부 1개 = 7 × 4KB = 28KB, kmalloc 0회 XArray: 노드 ~20개 + kmalloc 1,000회(24B×1,000) = ~104KB genradix가 메모리 효율과 할당 횟수 모두에서 유리합니다. 단, RCU 보호 읽기가 필요하면 XArray를 사용해야 합니다.

io_uring에서의 genradix 활용

io_uring 서브시스템에서도 genradix를 사용하여 고정 파일(Fixed File)과 고정 버퍼(Fixed Buffer) 테이블을 관리합니다. 등록된 파일 디스크립터와 버퍼를 인덱스로 빠르게 조회하는 데 활용됩니다.

/* io_uring에서의 genradix 활용 패턴 */

/* io_uring 컨텍스트 (io_uring/io_uring.h) */
struct io_ring_ctx {
    /* ... */

    /* 고정 파일 테이블: 등록된 fd를 인덱스로 조회 */
    struct io_rsrc_data     *file_data;
    /* file_data 내부에서 genradix 사용: */
    /* GENRADIX(struct io_rsrc_node *) 형태 */

    /* 고정 버퍼 테이블 */
    struct io_rsrc_data     *buf_data;

    unsigned                nr_user_files;
    unsigned                nr_user_bufs;
    /* ... */
};

/* 등록된 파일을 genradix로 조회하는 패턴 */
static struct file *io_file_from_index(
    struct io_ring_ctx *ctx, int fd)
{
    struct io_rsrc_node **node_ptr;

    /* genradix_ptr로 인덱스 기반 조회 */
    node_ptr = genradix_ptr(&ctx->file_data->nodes, fd);
    if (!node_ptr || !*node_ptr)
        return NULL;

    return (*node_ptr)->file;
}

/* io_uring에서 genradix를 선택한 이유: */
/*
 * 1. 등록 시(IORING_REGISTER_FILES) GFP_KERNEL 허용
 *    → genradix_ptr_alloc의 제약과 일치
 * 2. 조회 시(submission path)는 읽기만 필요
 *    → genradix_ptr이 락프리이므로 고성능 경로에 적합
 * 3. fd 번호가 0부터 순차적 → 희소 배열이 아니므로 메모리 효율적
 * 4. 파일 수 변경 시 genradix_free 후 새로 구축
 *    → 부분 수정보다 전체 교체가 더 안전
 */
코드 설명
  • GENRADIX(struct io_rsrc_node *): io_uring에서는 포인터를 저장합니다. 포인터 크기가 8바이트이므로 리프 하나에 512개를 저장할 수 있어, 대부분의 경우 깊이 0(리프 1개)으로 충분합니다.
  • 등록(Register) 경로: 사용자가 io_uring_register()를 호출할 때 genradix_ptr_alloc()으로 슬롯을 할당합니다. 이 경로는 프로세스 컨텍스트에서 실행되므로 GFP_KERNEL이 안전합니다.
  • 제출(Submit) 경로: SQ(Submission Queue)에서 요청을 처리할 때 genradix_ptr()로 등록된 파일을 조회합니다. 이 경로는 성능 임계 경로이므로 락프리 읽기가 핵심입니다.
  • 전체 교체 패턴: IORING_REGISTER_FILES_UPDATE로 파일 테이블을 갱신할 때, 기존 genradix를 해제하고 새로 구축하는 것이 개별 수정보다 안전합니다. genradix에는 개별 슬롯 해제 API가 없기 때문입니다.

genradix 사용 판단 기준

genradix가 적합한 경우:

  1. 정수 인덱스로 접근하는 동적 배열이 필요할 때
  2. 임의 크기 구조체를 직접 저장해야 할 때 (포인터가 아님)
  3. 프로세스 컨텍스트에서만 할당/해제할 때
  4. 한번 저장한 원소의 주소가 변하지 않아야 할 때
  5. RCU 보호가 필요 없을 때
  6. 단순한 구현을 선호할 때 (디버깅, 감사 용이성)

genradix가 부적합한 경우 (대안 포함):

조건대안
RCU 읽기 보호 필요XArray (xa_load under rcu_read_lock)
IRQ/atomic 컨텍스트에서 할당XArray (GFP_ATOMIC 지원)
범위 검색, 태깅 필요XArray (xa_find, xa_marked)
키가 정수가 아님rbtree, hashtable
원소 수가 고정일반 배열 또는 kmalloc_array
삭제가 빈번함XArray 또는 IDR (개별 삭제 지원)
동시 읽기/쓰기가 빈번XArray (내장 락킹)

실제 커널 사용 통계 (v6.8 기준, 대략적):

에러 처리와 메모리 해제

Generic Radix Tree의 에러 처리는 할당 실패(OOM)와 메모리 해제에 집중됩니다. genradix_free는 트리의 모든 노드를 깊이 우선(DFS) 방식으로 해제하며, 할당 실패 시에는 부분 트리가 남는 것이 아니라 이전 상태가 유지됩니다.

genradix_free 구현

/* lib/generic-radix-tree.c — __genradix_free 구현 */

static void genradix_free_recurse(struct genradix_node *node,
                                   unsigned level)
{
    if (level) {
        /* 내부 노드: 자식부터 재귀적으로 해제 */
        unsigned i;
        for (i = 0; i < GENRADIX_ARY; i++)
            if (node->children[i])
                genradix_free_recurse(node->children[i], level - 1);
    }
    /* 자식 해제 후 자신을 해제 (post-order) */
    free_page((unsigned long)node);
}

void __genradix_free(struct __genradix *radix)
{
    struct genradix_root *r = radix->root;

    if (r) {
        genradix_free_recurse(genradix_root_to_node(r),
                              genradix_root_to_depth(r));
        radix->root = NULL;
    }
}

/* 호출 예 */
GENRADIX(struct my_obj) my_radix;

/* 사용 후 해제 */
genradix_free(&my_radix);
/* → __genradix_free(&my_radix.tree) 호출 */
/* → root부터 DFS로 모든 노드의 페이지를 free_page()로 반환 */
/* → radix->root = NULL로 초기화 */
코드 설명
  • Post-order 해제: 자식을 먼저 해제한 뒤 부모를 해제합니다. 이 순서가 중요한 이유는, 부모를 먼저 해제하면 자식 포인터에 접근할 수 없기 때문입니다.
  • GENRADIX_ARY 순회: 내부 노드의 모든 512개 슬롯을 확인합니다. NULL이 아닌 자식만 재귀 호출하므로, 희소 배열이면 대부분의 슬롯을 건너뜁니다.
  • free_page: __get_free_page로 할당했으므로 free_page로 해제합니다. Slab이 아닌 버디 시스템(Buddy System)에 직접 반환됩니다.
  • radix->root = NULL: 해제 후 루트를 NULL로 설정하여, 이후 genradix_ptr 호출이 안전하게 NULL을 반환하도록 합니다.
  • 재귀 깊이 안전성: 최대 깊이가 4~5이므로 재귀 스택 오버플로 위험이 없습니다.
genradix_free: DFS Post-order 해제 순서 (depth=2) 루트 (L2) ⑦ 해제 내부[0] (L1) ④ 해제 내부[1] (L1) ⑥ 해제 리프 A ① 해제 리프 B ② 해제 리프 C ③ 해제 리프 D ⑤ 해제 Post-order DFS 해제 순서 ① 리프 A → ② 리프 B → ③ 리프 C → ④ 내부[0] → ⑤ 리프 D → ⑥ 내부[1] → ⑦ 루트 각 free_page() 호출은 O(1)이므로, 전체 해제 시간 = O(노드 수)입니다. 깊이와 무관하게 선형 시간입니다.

해제 비용 분석

/* genradix_free 비용 분석 — 실제 시나리오별 */

/*
 * 시나리오 1: 소규모 배열 (1,000개 64B 객체, depth=1)
 *   리프 노드: ceil(1000/64) = 16개
 *   내부 노드: 1개 (루트)
 *   총 free_page 호출: 17회
 *   예상 시간: ~17µs (free_page 평균 ~1µs)
 *
 * 시나리오 2: 대규모 배열 (1,000,000개 64B 객체, depth=2)
 *   리프 노드: ceil(1000000/64) = 15,625개
 *   L1 내부 노드: ceil(15625/512) = 31개
 *   L2 루트: 1개
 *   총 free_page 호출: 15,657회
 *   예상 시간: ~15.7ms
 *
 * 시나리오 3: 희소 배열 (인덱스 0, 999,999만 사용, depth=2)
 *   리프 노드: 2개 (인덱스 0이 있는 리프, 999,999가 있는 리프)
 *   L1 내부 노드: 2개 (각 리프의 부모)
 *   L2 루트: 1개
 *   총 free_page 호출: 5회
 *   예상 시간: ~5µs
 */

/* 해제 시 주의사항 */
/*
 * 1. genradix_free는 슬립할 수 있음 (free_page → buddy allocator)
 *    → 원자적 컨텍스트(spinlock, IRQ)에서 호출 금지
 *
 * 2. 해제 중 동시 접근 방지 필요
 *    → 호출자가 외부 동기화 보장해야 함
 *    → 모듈에서는 /proc 제거 후 호출하는 패턴 사용
 *
 * 3. 부분 해제 API 없음
 *    → 특정 인덱스만 해제하는 것은 불가능
 *    → 슬롯 값을 0으로 memset하는 것으로 대체
 *    → 메모리는 genradix_free까지 반환되지 않음
 */

/* 부분 무효화 패턴 */
struct my_obj *obj = genradix_ptr(&radix, idx);
if (obj) {
    memset(obj, 0, sizeof(*obj));
    /* 슬롯은 0으로 초기화되지만 리프 페이지는 유지됨 */
    /* genradix_for_each에서 id==0 체크로 건너뛸 수 있음 */
}
코드 설명
  • 해제 비용 선형성: genradix_free의 시간 복잡도는 O(할당된 노드 수)입니다. 희소 배열이면 실제 할당된 노드만 방문하므로 인덱스 범위와 무관하게 빠릅니다.
  • 부분 해제 불가: 개별 슬롯이나 리프를 선택적으로 해제하는 API가 없습니다. 이는 설계 의도적인 것으로, 단순성을 유지하기 위함입니다. 필요하면 슬롯을 0으로 덮어쓰고, 사용자 코드에서 유효성을 판단합니다.
  • 원자적 컨텍스트 금지: free_page는 버디 시스템에 페이지를 반환하는 과정에서 슬립할 수 있습니다. 따라서 스핀락이나 인터럽트 핸들러 내에서 genradix_free를 호출하면 안 됩니다.

할당 실패 시 동작

/* 할당 실패 시나리오 분석 */

/*
 * genradix_ptr_alloc에서 __get_free_page가 실패하는 경우:
 *
 * 시나리오 1: 루트 확장 중 실패
 * - 새 내부 노드 할당 실패 → NULL 반환
 * - 기존 트리는 변경되지 않음 (cmpxchg 전이므로)
 * - 안전: 트리가 이전 상태 그대로 유지됩니다
 *
 * 시나리오 2: 경로 탐색 중 중간 노드 할당 실패
 * - children[idx] = NULL 상태에서 새 노드 할당 실패
 * - NULL 반환
 * - 주의: 루트 확장은 이미 완료되었을 수 있음
 *   → 깊이는 늘었지만 리프는 없는 상태 (빈 서브트리)
 *   → genradix_ptr은 NULL을 반환하므로 안전
 *   → genradix_free는 NULL 자식을 건너뛰므로 안전
 *
 * 시나리오 3: OOM Killer 상황
 * - GFP_KERNEL은 직접 회수(reclaim)를 시도
 * - 그래도 실패하면 NULL 반환
 * - 호출자가 에러 처리 필요:
 */

struct my_obj *obj = genradix_ptr_alloc(&radix, idx, GFP_KERNEL);
if (!obj) {
    /* 할당 실패 처리 */
    pr_err("genradix: 인덱스 %zu 할당 실패\n", idx);

    /* 선택 1: 에러 반환 */
    return -ENOMEM;

    /* 선택 2: 재시도 (메모리가 확보될 때까지) */
    /* schedule_timeout_uninterruptible(HZ); */
    /* goto retry; */
}

/* GFP_NOWAIT 사용 시: 슬립 없이 즉시 실패 가능 */
obj = genradix_ptr_alloc(&radix, idx, GFP_NOWAIT);
/* → 메모리 압박 시 높은 확률로 NULL 반환 */

/* genradix_init을 사용한 명시적 초기화 */
/* (보통은 0으로 초기화된 구조체에 포함되어 불필요) */
genradix_init(&my_radix);
/* → radix->root = NULL 설정 */
코드 설명
  • 원자적 실패 보장: 루트 확장 전의 할당 실패는 트리를 전혀 변경하지 않습니다. cmpxchg 이전 단계에서 실패하기 때문입니다.
  • 부분 확장 상태: 루트는 확장되었지만 리프 할당에 실패한 경우, 트리에 빈 서브트리가 생깁니다. 이는 문제가 되지 않는데, genradix_ptr은 NULL 경로에서 NULL을 반환하고, genradix_free는 NULL 자식을 건너뛰기 때문입니다.
  • GFP_KERNEL vs GFP_NOWAIT: genradix는 주로 GFP_KERNEL을 사용합니다. 이는 메모리 회수, 스왑, OOM Killer까지 동원하여 할당을 시도합니다. GFP_NOWAIT은 비동기 컨텍스트에서만 사용하며, 실패 확률이 높습니다.
  • genradix_init: 구조체가 0으로 초기화된 경우(예: kzalloc으로 할당) 별도 초기화가 불필요합니다. root = NULL이면 빈 트리입니다.

커널 모듈 실전 예제

다음은 Generic Radix Tree를 사용하는 완전한 커널 모듈 예제입니다. 초기화, 원소 추가, 순회, /proc 인터페이스를 통한 데이터 조회, 그리고 정리(Cleanup)까지 전체 흐름을 포함합니다.

/* genradix_demo.c — Generic Radix Tree 커널 모듈 예제 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/generic-radix-tree.h>

#define DEMO_MAX_ENTRIES  1024

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Generic Radix Tree 데모 모듈");

/* 저장할 구조체 정의 */
struct demo_entry {
    u32  id;              /* 항목 ID */
    u32  value;           /* 데이터 값 */
    u64  timestamp;       /* 생성 시각 (jiffies) */
    char name[16];        /* 항목 이름 */
};
/* sizeof(struct demo_entry) = 32 */
/* objs_per_leaf = 4096 / 32 = 128 */

/* genradix 선언: 타입 안전 래퍼 */
static GENRADIX(struct demo_entry) demo_radix;
static size_t demo_count;

/* /proc/genradix_demo 표시 콜백 */
static int demo_show(struct seq_file *m, void *v)
{
    struct demo_entry *entry;
    struct genradix_iter iter;

    seq_printf(m, "%-6s %-8s %-16s %s\n",
               "Index", "ID", "Name", "Value");
    seq_puts(m, "--------------------------------------\n");

    /* genradix_for_each: 모든 유효 원소 순회 */
    genradix_for_each(&demo_radix, iter, entry) {
        if (entry->id == 0)
            continue;  /* 미초기화 슬롯 건너뛰기 */

        seq_printf(m, "%-6zu %-8u %-16s %u\n",
                   iter.pos, entry->id,
                   entry->name, entry->value);
    }

    seq_printf(m, "\n총 %zu개 항목\n", demo_count);
    return 0;
}

static int demo_open(struct inode *inode, struct file *file)
{
    return single_open(file, demo_show, NULL);
}

static const struct proc_ops demo_proc_ops = {
    .proc_open    = demo_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

/* 모듈 초기화 */
static int __init demo_init(void)
{
    struct demo_entry *entry;
    size_t i;

    /* genradix 초기화 (0으로 선언했으므로 사실 불필요) */
    genradix_init(&demo_radix);

    /* 샘플 데이터 삽입 */
    for (i = 0; i < DEMO_MAX_ENTRIES; i++) {
        /* 희소 패턴: 짝수 인덱스만 사용 */
        size_t idx = i * 2;

        entry = genradix_ptr_alloc(&demo_radix, idx, GFP_KERNEL);
        if (!entry) {
            pr_err("genradix_demo: 인덱스 %zu 할당 실패\n", idx);
            goto err_free;
        }

        entry->id        = i + 1;
        entry->value     = i * 100;
        entry->timestamp = jiffies;
        snprintf(entry->name, sizeof(entry->name),
                 "item-%04zu", i);
    }
    demo_count = DEMO_MAX_ENTRIES;

    /* /proc 인터페이스 생성 */
    if (!proc_create("genradix_demo", 0444, NULL, &demo_proc_ops)) {
        pr_err("genradix_demo: /proc 생성 실패\n");
        goto err_free;
    }

    pr_info("genradix_demo: %zu개 항목 로드 완료\n", demo_count);
    return 0;

err_free:
    genradix_free(&demo_radix);
    return -ENOMEM;
}

/* 모듈 정리 */
static void __exit demo_exit(void)
{
    remove_proc_entry("genradix_demo", NULL);
    genradix_free(&demo_radix);
    pr_info("genradix_demo: 모듈 언로드 완료\n");
}

module_init(demo_init);
module_exit(demo_exit);
코드 설명
  • GENRADIX(struct demo_entry): 32바이트 구조체를 저장하는 타입 안전 genradix를 선언합니다. 리프 하나에 128개(4096/32)를 저장할 수 있습니다.
  • genradix_ptr_alloc: 지정한 인덱스의 슬롯을 할당하고 포인터를 반환합니다. GFP_KERNEL은 프로세스 컨텍스트에서 슬립 허용 할당입니다.
  • 희소 패턴(짝수만 사용): 인덱스 0, 2, 4, ..., 2046을 사용합니다. 홀수 인덱스에는 할당하지 않으므로 해당 슬롯은 0으로 남습니다. genradix_for_each는 할당된 리프만 방문하지만, 리프 내 미사용 슬롯도 순회하므로 id == 0 체크가 필요합니다.
  • proc_ops: seq_file 인터페이스를 사용하여 /proc/genradix_demo를 통해 데이터를 조회할 수 있습니다. cat /proc/genradix_demo로 확인합니다.
  • 에러 처리: 할당 실패 시 genradix_free로 이미 할당된 모든 노드를 정리한 뒤 -ENOMEM을 반환합니다.
  • 모듈 정리: /proc 엔트리를 먼저 제거하고(remove_proc_entry), 그 후 genradix를 해제합니다. 순서가 중요한데, /proc가 살아있는 동안 콜백이 트리에 접근할 수 있기 때문입니다.

빌드 및 실행

# Makefile — 커널 모듈 빌드

obj-m += genradix_demo.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean
# 빌드 및 로드
$ make
$ sudo insmod genradix_demo.ko
$ dmesg | tail -1
[  123.456] genradix_demo: 1024개 항목 로드 완료

# 데이터 조회
$ cat /proc/genradix_demo
Index  ID       Name             Value
--------------------------------------
0      1        item-0000        0
2      2        item-0001        100
4      3        item-0002        200
...
2046   1024     item-1023        102300

총 1024개 항목

# 메모리 사용량 확인
# 1024개 × 32B = 32KB 데이터
# 리프: ceil(2048/128) = 16개 × 4KB = 64KB (인덱스 0~2046 범위)
# 내부: 1개 × 4KB = 4KB
# 총: 68KB (데이터 32KB + 오버헤드 36KB)
# 희소 배열이므로 연속 배열(2047×32=64KB)보다 약간 큰 정도

# 모듈 언로드
$ sudo rmmod genradix_demo
$ dmesg | tail -1
[  456.789] genradix_demo: 모듈 언로드 완료
코드 설명
  • Makefile: 표준 외부 커널 모듈 빌드 방식입니다. KDIR은 현재 실행 중인 커널의 빌드 디렉터리를 가리킵니다.
  • insmod / rmmod: 모듈을 동적으로 로드/언로드합니다. dmesg로 커널 로그를 확인할 수 있습니다.
  • 메모리 사용량 분석: 희소 배열(짝수만 사용)이므로 인덱스 범위는 0~2046이지만, 리프는 연속 범위를 커버하므로 16개가 필요합니다. 연속 배열과 비교하면 약간의 내부 노드 오버헤드(4KB)가 추가됩니다.
  • 모듈 언로드 순서: /proc 엔트리 제거 → genradix 해제. 이 순서를 지키지 않으면 use-after-free가 발생할 수 있습니다.

고급 패턴: genradix_ptr을 통한 읽기 전용 접근

/* 읽기 전용 접근 — genradix_ptr vs genradix_ptr_alloc 선택 기준 */

/* genradix_ptr: 읽기 전용, 할당하지 않음 */
struct demo_entry *entry = genradix_ptr(&demo_radix, idx);
if (!entry) {
    /* 해당 인덱스의 리프가 아직 할당되지 않음 */
    /* → 원소가 존재하지 않는다는 의미 */
    pr_info("인덱스 %zu: 존재하지 않음\n", idx);
    return -ENOENT;
}

/* entry가 NULL이 아니면 리프 내 해당 슬롯의 포인터 */
/* 단, 해당 슬롯에 의미있는 데이터가 있는지는 별도 확인 필요 */
if (entry->id == 0) {
    /* 리프는 할당되었지만 이 슬롯은 미사용 */
    /* (__get_free_page | __GFP_ZERO로 0 초기화됨) */
    return -ENOENT;
}

/* 읽기 전용 접근은 동시성에 안전합니다: */
/* - genradix_ptr은 락프리 */
/* - 이미 할당된 리프의 주소는 변하지 않음 */
/* - 단, 다른 스레드가 값을 쓰고 있으면 torn read 가능 */
/*   (크기가 워드보다 크면 별도 락 필요) */

pr_info("인덱스 %zu: id=%u, value=%u, name=%s\n",
        idx, entry->id, entry->value, entry->name);
코드 설명
  • genradix_ptr vs genradix_ptr_alloc: 읽기만 할 때는 반드시 genradix_ptr을 사용합니다. genradix_ptr_alloc은 노드를 할당하므로, 불필요한 메모리 소비와 OOM 위험이 있습니다.
  • NULL 반환의 의미: 해당 인덱스를 포함하는 리프 노드가 아직 존재하지 않는다는 뜻입니다. 리프 내 특정 슬롯의 사용 여부는 별도로 판단해야 합니다.
  • 동시성 주의: 읽기 경로는 락프리이지만, 다른 스레드의 쓰기와 동시에 실행되면 구조체의 일부 필드만 갱신된 상태(Torn Read)를 볼 수 있습니다. 이를 방지하려면 호출자가 별도 동기화를 구현해야 합니다.
커널 모듈 라이프사이클에서의 genradix 사용 흐름 module_init() genradix_init() 데이터 삽입 genradix_ptr_alloc() /proc 조회 genradix_for_each() module_exit() genradix_free() genradix_ptr_alloc(): 쓰기용 접근 genradix_ptr(): 읽기 전용 접근 genradix_for_each(): 전체 순회 genradix_free(): DFS 전체 해제 커널 모듈에서의 genradix 사용 주의점 • genradix_ptr_alloc()은 GFP_KERNEL을 사용하므로 프로세스 컨텍스트(슬립 허용)에서만 호출해야 합니다. • module_exit()에서 반드시 genradix_free()를 호출해야 합니다. 누락하면 페이지 누수(Page Leak)가 발생합니다.

커널 내장 테스트(KUnit) 분석

커널 소스에는 Generic Radix Tree의 정확성을 검증하는 KUnit 테스트가 포함되어 있습니다. lib/test_generic-radix-tree.c의 주요 테스트 케이스를 분석합니다.

/* lib/test_generic-radix-tree.c — KUnit 테스트 분석 */

/* 테스트 1: 기본 할당과 조회 */
static void test_basic(struct kunit *test)
{
    GENRADIX(u64) radix;
    u64 *ptr;
    int i;

    genradix_init(&radix);

    /* 연속 삽입 */
    for (i = 0; i < 1000; i++) {
        ptr = genradix_ptr_alloc(&radix, i, GFP_KERNEL);
        KUNIT_ASSERT_NOT_NULL(test, ptr);
        *ptr = i * 100;
    }

    /* 조회 검증 */
    for (i = 0; i < 1000; i++) {
        ptr = genradix_ptr(&radix, i);
        KUNIT_ASSERT_NOT_NULL(test, ptr);
        KUNIT_EXPECT_EQ(test, *ptr, (u64)(i * 100));
    }

    /* 범위 밖 조회: NULL 반환 확인 */
    ptr = genradix_ptr(&radix, 100000);
    KUNIT_EXPECT_NULL(test, ptr);

    genradix_free(&radix);
}

/* 테스트 2: 깊이 확장 검증 */
static void test_depth_expansion(struct kunit *test)
{
    GENRADIX(u8) radix;
    u8 *ptr;

    genradix_init(&radix);

    /* depth 0: 인덱스 0~4095 */
    ptr = genradix_ptr_alloc(&radix, 0, GFP_KERNEL);
    KUNIT_ASSERT_NOT_NULL(test, ptr);

    /* depth 0→1 확장 트리거: 인덱스 4096 */
    ptr = genradix_ptr_alloc(&radix, 4096, GFP_KERNEL);
    KUNIT_ASSERT_NOT_NULL(test, ptr);
    *ptr = 42;

    /* 기존 인덱스 0의 데이터가 보존되는지 확인 */
    ptr = genradix_ptr(&radix, 0);
    KUNIT_ASSERT_NOT_NULL(test, ptr);

    /* 확장 후 인덱스 4096 데이터 확인 */
    ptr = genradix_ptr(&radix, 4096);
    KUNIT_ASSERT_NOT_NULL(test, ptr);
    KUNIT_EXPECT_EQ(test, *ptr, (u8)42);

    genradix_free(&radix);
}

/* 테스트 3: 순회 검증 */
static void test_iteration(struct kunit *test)
{
    GENRADIX(u32) radix;
    struct genradix_iter iter;
    u32 *ptr;
    size_t count = 0;

    genradix_init(&radix);

    /* 희소 삽입: 인덱스 0, 100, 200 */
    ptr = genradix_ptr_alloc(&radix, 0, GFP_KERNEL);
    *ptr = 10;
    ptr = genradix_ptr_alloc(&radix, 100, GFP_KERNEL);
    *ptr = 20;
    ptr = genradix_ptr_alloc(&radix, 200, GFP_KERNEL);
    *ptr = 30;

    /* genradix_for_each는 리프가 있는 모든 슬롯을 방문 */
    /* 주의: NULL 리프는 건너뛰지만, 리프 내 0 값 슬롯은 방문함 */
    genradix_for_each(&radix, iter, ptr) {
        if (*ptr != 0)
            count++;
    }
    /* count == 3 (값이 있는 슬롯만 카운트) */
    KUNIT_EXPECT_EQ(test, count, (size_t)3);

    genradix_free(&radix);
}

/* KUnit 테스트 실행 방법: */
/* ./tools/testing/kunit/kunit.py run --kunitconfig=lib/Kconfig */
/* 또는 menuconfig에서 CONFIG_GENERIC_RADIX_TREE_TEST=y 설정 후 부팅 */
코드 설명
  • KUNIT_ASSERT vs KUNIT_EXPECT: ASSERT는 실패 시 테스트를 즉시 중단하고, EXPECT는 실패를 기록하되 계속 진행합니다. NULL 포인터 역참조는 커널 패닉을 유발하므로 ASSERT_NOT_NULL을 사용합니다.
  • test_depth_expansion: 깊이 확장 후 기존 데이터가 보존되는지 확인하는 핵심 테스트입니다. cmpxchg 기반 루트 교체가 올바르게 동작하는지 검증합니다.
  • test_iteration의 count 불일치 주의: genradix_for_each는 리프 단위로 방문하므로, 인덱스 0~1023이 모두 하나의 리프(obj_size=4, objs_per_leaf=1024)에 있으면 1024번 반복합니다. *ptr != 0 체크로 실제 삽입된 값만 세는 것이 중요합니다.
  • KUnit 실행: kunit.py 도구를 사용하면 커널을 부팅하지 않고도 UML(User-Mode Linux)에서 테스트를 실행할 수 있습니다.

주소 계산 심층 분석

genradix_ptr()genradix_ptr_alloc()가 인덱스에서 실제 메모리 주소를 도출하는 과정을 커널 소스 레벨에서 분석합니다. 핵심은 바이트 오프셋의 비트 필드를 트리 레벨별 슬롯 인덱스로 분해하는 것입니다.

genradix_ptr() 주소 계산 전체 경로

/* lib/generic-radix-tree.c — __genradix_ptr 주소 계산 */

/* 호출자: genradix_ptr(&radix, idx) 매크로가
 *   __genradix_ptr(&radix->tree, idx * sizeof(type))를 호출합니다
 *   → 인덱스가 바이트 오프셋으로 변환되어 전달됩니다 */

void *__genradix_ptr(struct __genradix *radix,
                      size_t offset)
{
    struct genradix_root *r = READ_ONCE(radix->root);
    struct genradix_node *n;
    unsigned level;

    if (!r)
        return NULL;

    n     = genradix_root_to_node(r);
    /* n = (genradix_node *)((unsigned long)r & ~3UL)
     * 하위 2비트를 마스킹하여 노드 주소를 추출합니다 */

    level = genradix_root_to_depth(r);
    /* level = (unsigned long)r & 3
     * 하위 2비트가 트리 깊이(0~3)를 나타냅니다 */

    /* 현재 깊이로 offset을 수용할 수 있는지 확인 */
    if (offset >= genradix_depth_size(level))
        return NULL;
    /* depth_size(d) = 1UL << (PAGE_SHIFT + d * ilog2(GENRADIX_ARY))
     * 64비트: depth_size(0)=4KB, (1)=2MB, (2)=1GB, (3)=512GB */

    /* 루트에서 리프까지 경로 추적 */
    while (level) {
        /* 현재 레벨에서의 슬롯 인덱스를 비트 시프트+마스크로 추출 */
        unsigned idx = (offset >> (level * PAGE_SHIFT))
                     & (GENRADIX_ARY - 1);
        /* PAGE_SHIFT=12, GENRADIX_ARY=512 (64비트)
         * level=2: (offset >> 24) & 511 → L2 슬롯
         * level=1: (offset >> 12) & 511 → L1 슬롯 */

        n = n->children[idx];
        if (!n)
            return NULL;
        /* NULL 자식 = 해당 범위에 데이터 없음 */

        level--;
    }

    /* 리프 노드 도달: 바이트 오프셋의 하위 12비트가 data[] 인덱스 */
    return &n->data[offset & (PAGE_SIZE - 1)];
    /* offset & 0xFFF → 리프 페이지 내 0~4095 바이트 위치 */
}
코드 설명
  • READ_ONCE: 컴파일러의 값 캐싱을 방지합니다. 다른 스레드가 genradix_ptr_alloc으로 루트를 변경할 수 있으므로, 매번 메모리에서 최신 값을 읽습니다.
  • 포인터 태깅: 페이지 정렬 주소의 하위 비트를 메타데이터로 활용하는 기법입니다. genradix_root는 실제 타입이 아니라 태그된 포인터의 의미론적 표시입니다.
  • 비트 시프트 경로 결정: 나눗셈 대신 시프트/마스크만으로 경로를 결정합니다. level * PAGE_SHIFT가 각 레벨의 비트 시작 위치이고, GENRADIX_ARY - 1이 해당 레벨의 비트 마스크입니다. 이 연산은 CPU 파이프라인에서 단 1~2 사이클에 완료됩니다.
  • NULL 체크 체인: 각 레벨에서 자식 포인터가 NULL이면 즉시 NULL을 반환합니다. 할당되지 않은 인덱스를 읽을 때 안전하게 실패합니다.
  • 최종 반환: &n->data[offset & 0xFFF]는 리프 페이지 내의 바이트 위치를 가리킵니다. 매크로가 이를 올바른 타입 포인터로 캐스팅합니다.

genradix_ptr_alloc() 할당 경로 분석

genradix_ptr_alloc()genradix_ptr()와 동일한 주소 계산을 수행하되, 경로상에 NULL 노드가 있으면 새 페이지를 할당합니다. 두 함수의 차이를 비교합니다.

/* genradix_ptr vs genradix_ptr_alloc 경로 비교 */

/* ┌─────────────────┬────────────────────┬────────────────────────┐
 * │ 조건             │ genradix_ptr       │ genradix_ptr_alloc     │
 * ├─────────────────┼────────────────────┼────────────────────────┤
 * │ root == NULL    │ return NULL        │ 새 리프 할당 (depth=0) │
 * │ depth 부족      │ return NULL        │ 루트 위에 노드 삽입    │
 * │ children[i]==NULL│ return NULL       │ 새 노드 할당           │
 * │ 리프 도달       │ &data[off]         │ &data[off] (동일)      │
 * └─────────────────┴────────────────────┴────────────────────────┘ */

/* genradix_ptr_alloc의 할당 단계 상세 */

/* 단계 A: 빈 트리이거나 깊이 부족 시 트리 확장 */
while (!r || offset >= genradix_depth_size(
                          genradix_root_to_depth(r))) {
    /* 새 페이지 할당: __get_free_page(gfp | __GFP_ZERO) */
    new_node = genradix_alloc_node(gfp);

    if (!r) {
        /* 빈 트리: new_node가 depth=0 리프로 설정 */
        new_root = (void *)((unsigned long)new_node | 0);
    } else {
        /* 깊이 확장: 기존 루트를 new_node->children[0]에 연결 */
        new_node->children[0] = genradix_root_to_node(r);
        new_root = (void *)((unsigned long)new_node |
                           (genradix_root_to_depth(r) + 1));
    }

    /* cmpxchg: 원자적 루트 교체 */
    if (cmpxchg(&radix->root, r, new_root) == r)
        r = new_root;  /* 성공 → 계속 */
    else {
        free_page((unsigned long)new_node);  /* 실패 → 해제 */
        r = READ_ONCE(radix->root);        /* 최신 루트 재읽기 */
    }
}

/* 단계 B: 리프까지 경로 탐색 + 누락 노드 할당 */
node = genradix_root_to_node(r);
level = genradix_root_to_depth(r);

while (level--) {
    unsigned idx = (offset >> (PAGE_SHIFT * (level + 1)))
                   & (GENRADIX_ARY - 1);

    if (!node->children[idx]) {
        /* NULL 자식 → 새 페이지 할당 */
        new_node = genradix_alloc_node(gfp);
        if (!new_node)
            return NULL;  /* OOM: 부분 경로는 그대로 유지 */
        node->children[idx] = new_node;
    }
    node = node->children[idx];
}

/* 단계 C: 리프 도달 → genradix_ptr과 동일한 반환 */
return &node->data[offset & (PAGE_SIZE - 1)];
코드 설명
  • 단계 A (트리 확장): while 루프로 필요한 만큼 깊이를 늘립니다. 예를 들어 빈 트리에 offset=1GB가 들어오면, depth 0→1→2로 두 번 확장합니다. 각 확장마다 cmpxchg로 원자적 교체를 시도하며, 실패 시 새 노드를 해제하고 재시도합니다.
  • 단계 B (경로 할당): 깊이 확장 완료 후, 루트에서 리프까지 내려가면서 NULL인 자식에 새 노드를 할당합니다. 이 부분은 cmpxchg 없이 직접 대입합니다. 호출자가 외부 잠금을 보장해야 안전합니다.
  • OOM 발생 시: 단계 B에서 할당 실패가 발생하면 NULL을 반환합니다. 이미 할당된 경로상의 노드는 해제하지 않습니다. 이는 의도적인 설계로, 다음 접근 시 해당 노드를 재활용할 수 있습니다.
  • 부분 경로 누수 아님: OOM으로 반환된 부분 경로의 노드들은 genradix_free() 호출 시 전체 DFS 해제로 정리됩니다.
페이지 크기 노드 내부 슬롯 배치 (obj_size=64B) 리프 노드: data[4096] = 64개 슬롯 (각 64바이트) 슬롯[0] off 0~63 슬롯[1] off 64~127 슬롯[2] off 128~191 ... 슬롯[40] off 2560~2623 ... 슬롯[62] off 3968~4031 슬롯[63] off 4032~4095 index=1000 → 이 슬롯 offset=64000, leaf_off=64000 & 0xFFF = 2560 슬롯 번호 = 2560 / 64 = 40 반환 주소 = leaf_page_addr + (byte_offset & (PAGE_SIZE - 1)) = leaf_page_addr + 2560 내부 노드: children[512] = 512개 포인터 (각 8바이트, 64비트) ptr[0] ptr[1] ... ptr[15] ... ptr[255] ... ptr[511] NULL 포인터 = 미할당 서브트리 (genradix_ptr → NULL 반환, iter → 건너뛰기) index=1000 → children[15] (0xFA00 >> 12) & 0x1FF = 15 리프: objs_per_page = PAGE_SIZE / obj_size 내부: children_per_page = PAGE_SIZE / sizeof(ptr) = 512
리프 노드는 고정 크기 객체 슬롯으로, 내부 노드는 자식 포인터 배열로 사용됩니다. 동일한 PAGE_SIZE union 구조입니다

다중 깊이 주소 계산 예시

깊이 2 트리에서 인덱스 100,000(obj_size=64B)에 접근하는 전체 주소 계산 과정입니다.

/* 다중 깊이 주소 계산 예시: depth=2, obj_size=64, index=100000 */

/* 1. 바이트 오프셋 */
size_t offset = 100000 * 64;  /* = 6,400,000 = 0x61A800 */

/* 2. 깊이 확인 */
/* depth_size(1) = 2MB  = 2,097,152  → 6.4M > 2M (부족) */
/* depth_size(2) = 1GB  = 1,073,741,824 → 6.4M < 1G (충분) */
/* → depth=2 필요 */

/* 3. 비트 분해 (3단계 경로) */
/*
 * offset = 0x61A800
 * = 0b 0000 0000 0110 0001 1010 1000 0000 0000
 *              ↑↑↑↑↑↑↑↑↑    ↑↑↑↑↑↑↑↑↑    ↑↑↑↑↑↑↑↑↑↑↑↑
 *              bit[23:12]   bit[11:0]은    리프 내 오프셋
 *              불필요(L2)   추후 계산      (이 경우 0x800)
 */

/* L2 (루트 → 중간 노드): */
unsigned l2_idx = (0x61A800 >> 24) & 0x1FF;
/* = 0x0 & 0x1FF = 0 → children[0] */

/* L1 (중간 노드 → 리프): */
unsigned l1_idx = (0x61A800 >> 12) & 0x1FF;
/* = 0x61A & 0x1FF = 0x1A = 26 → children[26] */

/* 리프 내 오프셋: */
unsigned leaf_off = 0x61A800 & 0xFFF;
/* = 0x800 = 2048 → data[2048] */
/* 슬롯 번호 = 2048 / 64 = 32 */

/* 경로: root->children[0]->children[26]->data[2048] */
/* 포인터 역참조: 3회 (depth + 1) */
/* 검증: children[0]의 자식 26 × 리프당 64개 + 슬롯 32 */
/* = 0*32768 + 26*64 + 32 = 1696번째? → 아닙니다! */
/* 정확한 계산: L2*512*64 + L1*64 + slot = 0*32768 + 26*64 + 32 */
/* ... 이것은 리프 기준 인덱스입니다. 원래 인덱스: */
/* 전체: L2*(512*64) + L1*64 + slot = 0 + 1664 + 32 = 1696 */
/* 하지만 원래 인덱스는 100000? 차이: offset 기반이기 때문 */
/* 실제: L2 커버 = 2MB/64 = 32768개, L1 커버 = 4KB/64 = 64개 */
/* index = l2 * 32768 + l1 * 64 + slot */
/* = 0 * 32768 + ... (이 계산이 아니라 byte_offset 기반!) */
/* genradix는 인덱스가 아닌 byte_offset으로 경로를 결정합니다 */
/* 따라서 경로는 바이트 오프셋 6,400,000의 비트 분해로 결정됩니다 */
depth=2 주소 계산: index=100000, obj_size=64B byte_offset = 6,400,000 (0x61A800) bit 24+ → L2=0 bit 12~23 → L1=0x61A&0x1FF=26 bit 0~11 → leaf_off=0x800=2048 Root (depth=2) children[0] 내부 노드 (L1) children[26] 리프 노드 data[2048] 결과 리프 슬롯 32 (2048/64), index 100000 접근 비용 포인터 역참조: 3회 (root→L1→leaf) 시프트/마스크: 3회 (L2, L1, leaf) 비교 연산: 1회 (depth_size 체크) 분기: 2회 (NULL 체크 per level) 전체 경로: root -> children[0] -> children[26] -> data[2048] = index 100000의 struct my_obj 포인터
depth=2에서 인덱스 100,000 접근 시 3번의 포인터 역참조와 비트 시프트/마스크 연산으로 주소를 계산합니다

고정 크기 원소 저장: XArray/Radix Tree 비교

커널의 세 가지 기수 트리 구현이 "고정 크기 원소를 인덱스로 관리"하는 시나리오에서 어떻게 다른지 비교합니다. Generic Radix Tree가 이 용도에 최적화된 이유를 코드 레벨에서 분석합니다.

XArray로 고정 크기 원소 저장 시 문제

/* XArray로 64바이트 구조체를 저장하는 경우 */

struct my_obj {
    u64 fields[8];  /* 64바이트 */
};

/* XArray 방식: 별도 kmalloc + 포인터 저장 */
struct xarray xa;
xa_init(&xa);

struct my_obj *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
xa_store(&xa, 42, obj, GFP_KERNEL);
/* 문제점:
 * 1. kmalloc 오버헤드: 슬랩 메타데이터 + 정렬 패딩
 * 2. 이중 간접 참조: xa_load()가 void* 반환 → 역참조 필요
 * 3. 메모리 산재: 각 obj가 독립 할당 → 캐시 효율 저하
 * 4. 개별 kfree 필요: 해제 시 N번의 kfree 호출
 */

/* 총 메모리 (1000개 저장 시): */
/*   XArray 노드: ~16 * 576B = ~9KB */
/*   kmalloc 객체: 1000 * (64 + 슬랩 오버헤드) ≈ 80~96KB */
/*   합계: ~89~105KB */

/* Generic Radix Tree 방식: 직접 저장 */
GENRADIX(struct my_obj) radix;
genradix_init(&radix);

struct my_obj *obj2 = genradix_ptr_alloc(&radix, 42, GFP_KERNEL);
/* 장점:
 * 1. 별도 kmalloc 없음: 객체가 리프 페이지 내에 직접 저장
 * 2. 단일 간접 참조: 반환 포인터가 곧 객체 포인터
 * 3. 메모리 연속: 같은 리프 내 객체들이 연속 배치 → 캐시 친화
 * 4. 일괄 해제: genradix_free() 한 번으로 전체 정리
 */

/* 총 메모리 (1000개 저장 시): */
/*   리프 16개: 16 * 4KB = 64KB */
/*   내부 노드 1개: 4KB */
/*   합계: 68KB (XArray 대비 ~30% 절약) */
코드 설명
  • XArray의 포인터 저장 모델: XArray는 void *만 저장합니다. 64바이트 구조체를 저장하려면 별도로 kmalloc한 후 그 포인터를 XArray에 넣어야 합니다. 이는 추가 할당 오버헤드와 캐시 미스를 유발합니다.
  • genradix의 직접 저장 모델: 리프 페이지의 data[] 배열에 객체가 직접 저장됩니다. genradix_ptr_alloc은 리프 페이지 할당 후 그 내부의 정확한 바이트 위치를 반환합니다. 별도의 kmalloc이 필요 없습니다.
  • 캐시 효율: genradix의 같은 리프에 있는 64개 객체(64B 기준)는 물리적으로 연속된 4KB 내에 있습니다. 순차 순회 시 L1 캐시와 하드웨어 프리페처가 매우 효과적으로 동작합니다. XArray에서는 각 객체가 독립 kmalloc이므로 메모리 산재가 심합니다.
  • 해제 비용: XArray에서 1000개 객체를 해제하려면 1000번의 kfree와 XArray 노드 해제가 필요합니다. genradix는 genradix_free() 한 번으로 17개 페이지를 일괄 free_page()합니다.

시나리오별 상세 비교

시나리오XArrayGeneric Radix Tree승자
포인터 배열 (void *)직접 저장, 최적8B 객체로 저장 가능하나 과도XArray
64B 구조체 1,000개kmalloc 1000회 + 노드리프 16개 + 내부 1개genradix
희소 배열 (10% 밀도)NULL 슬롯은 공간 안 씀리프 단위 할당 (빈 슬롯 낭비)XArray
밀집 배열 (100% 밀도)kmalloc N회페이지 할당만genradix
RCU 보호 읽기내장 지원미지원 (외부 잠금)XArray
개별 삭제xa_erase + kfree미지원XArray
일괄 해제xa_destroy + N*kfreegenradix_free 1회genradix
순차 순회 캐시 효율중간 (객체 산재)높음 (리프 내 연속)genradix
코드 복잡도~4000줄~200줄genradix

안정적 포인터(Stable Pointer) 보장

/* Generic Radix Tree의 중요한 특성: 포인터 안정성 */

/* genradix_ptr_alloc()이 반환한 포인터는 genradix_free()까지 유효합니다 */
struct my_obj *p1 = genradix_ptr_alloc(&radix, 10, GFP_KERNEL);
struct my_obj *p2 = genradix_ptr_alloc(&radix, 999999, GFP_KERNEL);
/* p1은 여전히 유효합니다!
 * 깊이 확장(0→1→2)이 발생해도 기존 리프 노드는 이동하지 않습니다.
 * 새 루트가 기존 트리를 children[0]에 연결할 뿐입니다. */

p1->fields[0] = 42;  /* 안전: p1이 가리키는 리프 페이지는 그대로 */

/* 반면 일부 자료구조는 이 보장을 제공하지 않습니다: */
/* - realloc 기반 배열: 확장 시 전체 복사 → 기존 포인터 무효 */
/* - 일부 해시 테이블: 리해시(Rehash) 시 포인터 무효 가능 */
/* - XArray: 내부 구조 변경 시 값(포인터) 자체는 보존되지만
 *           XArray에 직접 객체를 저장하는 것이 아니므로 해당 없음 */

/* bcachefs가 genradix를 선택한 핵심 이유 중 하나: */
/* 스트라이프 포인터를 여러 곳에서 캐시해도 안전합니다 */
struct stripe *s = genradix_ptr(&c->ec_stripes.stripes, idx);
/* 이후 다른 스트라이프가 추가되어 트리가 확장되어도 s는 유효 */

트리 성장 시각화

Generic Radix Tree가 원소 추가에 따라 어떻게 성장하는지 단계별로 시각화합니다. obj_size=64B (리프당 64개) 기준입니다.

Generic Radix Tree 성장 과정 (obj_size=64B) Phase 1: 빈 트리 root = NULL 용량: 0, 메모리: 8B (포인터) alloc Phase 2: depth=0 리프 A | d=0 용량: 64개, 메모리: 4KB 인덱스 0~63 사용 가능 idx=64 Phase 3: depth=1 (확장) 새 Root B | d=1 리프 A [0] 새 리프 C [1] [2..511]=NULL 용량: 32,768개, 메모리: 12KB (3페이지) Phase 4: depth=1 (채우기) Root B (depth=1, 512 슬롯) [0] [1] [2] ... [99] [100] ... NULL ... [511] 100개 리프 할당됨 용량: 32,768개, 사용: ~6,400개, 메모리: 101 x 4KB = 404KB, 밀도: ~20% Phase 5: depth=2 (32,769번째 원소 → 재확장) 새 Root D | depth=2 기존 B (내부) [0] 새 내부 노드 [1] [2..511]=NULL 최대 용량: 16,777,216개 (16M), 기존 데이터 경로 보존, cmpxchg로 원자적 루트 교체
트리는 root=NULL에서 시작하여 원소 추가에 따라 depth 0→1→2로 성장합니다. 기존 노드는 이동하지 않습니다

실전 패턴: 밀집 정수 인덱스 배열

Generic Radix Tree를 밀집(Dense) 정수 인덱스 배열로 활용하는 실전 패턴을 분석합니다. 이 패턴은 bcachefs의 버킷 세대(Bucket Generation) 관리와 io_uring의 리소스 테이블에서 실제로 사용됩니다.

밀집 카운터 배열

/* 패턴: CPU별 통계 카운터를 genradix로 관리 */

struct per_cpu_stat {
    u64 rx_packets;
    u64 tx_packets;
    u64 rx_bytes;
    u64 tx_bytes;
};  /* sizeof = 32B → objs_per_leaf = 128 */

static GENRADIX(struct per_cpu_stat) cpu_stats;

/* 초기화: 모든 CPU에 대해 사전 할당 */
static int stats_init(void)
{
    unsigned int cpu;

    genradix_init(&cpu_stats);

    /* nr_cpu_ids 만큼 사전 할당하여 런타임 할당 실패 방지 */
    for (cpu = 0; cpu < nr_cpu_ids; cpu++) {
        struct per_cpu_stat *s;

        s = genradix_ptr_alloc(&cpu_stats, cpu, GFP_KERNEL);
        if (!s)
            goto err;
        /* __GFP_ZERO로 자동 0 초기화 완료 */
    }
    return 0;

err:
    genradix_free(&cpu_stats);
    return -ENOMEM;
}

/* 런타임 업데이트 (소프트IRQ 컨텍스트에서 호출 가능) */
static void stats_update(unsigned int cpu,
                          u64 rx_pkts, u64 tx_pkts)
{
    struct per_cpu_stat *s;

    /* 사전 할당했으므로 genradix_ptr()로 충분 (할당 불필요) */
    s = genradix_ptr(&cpu_stats, cpu);
    if (likely(s)) {
        s->rx_packets += rx_pkts;
        s->tx_packets += tx_pkts;
    }
}

/* 통계 수집: 모든 CPU의 합산 */
static void stats_aggregate(struct per_cpu_stat *total)
{
    struct per_cpu_stat *s;
    struct genradix_iter iter;

    memset(total, 0, sizeof(*total));

    /* genradix_for_each: 할당된 리프만 순회 */
    genradix_for_each(&cpu_stats, iter, s) {
        total->rx_packets += s->rx_packets;
        total->tx_packets += s->tx_packets;
        total->rx_bytes   += s->rx_bytes;
        total->tx_bytes   += s->tx_bytes;
    }
}
코드 설명
  • 사전 할당 패턴: genradix_ptr_alloc을 초기화 시점에 호출하여 모든 슬롯을 미리 할당합니다. 이후 런타임에서는 genradix_ptr만 사용하므로 할당 실패가 발생하지 않습니다.
  • 밀집 배열 효율: CPU 256개(일반 서버)일 경우 256 * 32B = 8KB, 리프 2개로 충분합니다. 내부 노드 없이 depth=0이면 1페이지(4KB)에 128개까지 저장됩니다.
  • genradix_for_each 주의점: 이 매크로는 리프가 있는 모든 슬롯을 방문합니다. 할당된 리프 내의 미사용 슬롯(0으로 초기화)도 방문하므로, 실제 유효 데이터만 처리하려면 별도 체크가 필요합니다.
  • 동시성 참고: 이 예제에서 stats_update는 CPU별 전용 슬롯에 쓰므로 잠금 없이도 안전합니다. 다만 stats_aggregate는 다른 CPU의 업데이트와 동시 실행될 수 있어 정확한 스냅샷이 필요하면 잠금이 필요합니다.

bcachefs 버킷 세대 관리 패턴

/* fs/bcachefs/buckets.h — 버킷 세대 번호 관리 */

/* bcachefs는 디바이스를 버킷(Bucket) 단위로 나누어 관리합니다.
 * 각 버킷에는 세대(Generation) 번호가 있어 재사용 시 증가합니다.
 * 이를 통해 오래된 포인터를 감지합니다. */

struct bch_dev {
    /* 버킷 번호 → 세대 번호 매핑 */
    GENRADIX(u8) bucket_gens;
    /* sizeof(u8)=1 → objs_per_leaf = 4096
     * 한 리프에 4096개 버킷의 세대 번호를 저장합니다 */
};

/* 버킷 세대 조회 */
static inline u8 *bucket_gen(
    struct bch_dev *ca, size_t b)
{
    return genradix_ptr(&ca->bucket_gens, b);
}

/* 사용 패턴: 버킷 할당 시 세대 증가 */
u8 *gen = bucket_gen(ca, bucket_idx);
if (gen)
    (*gen)++;
/* 세대 값이 255(u8 최대)를 넘으면 0으로 래핑됩니다.
 * bcachefs는 이를 고려하여 세대 비교 시 래핑을 처리합니다. */

/* 메모리 효율 분석: 1TB SSD, 버킷 크기 512KB인 경우 */
/*   버킷 수 = 1TB / 512KB = 2,097,152 (2M) */
/*   obj_size = 1 (u8) → objs_per_leaf = 4096 */
/*   리프 수 = 2M / 4096 = 512 */
/*   내부 노드 = 1 (depth=1, 512 슬롯 정확히 채움) */
/*   총 메모리 = 513 × 4KB = 2,052KB ≈ 2MB */
/*   비교: 단순 배열 = 2M × 1B = 2MB (동일!) */
/*   → 밀집 배열에서 genradix 오버헤드는 내부 노드 1개(4KB)뿐 */

io_uring 리소스 테이블 패턴

/* io_uring의 리소스 관리에서 genradix 활용 */

/* io_uring은 등록된 파일/버퍼를 고정 테이블로 관리합니다.
 * 사용자가 io_uring_register()로 파일 디스크립터를 등록하면,
 * 이후 SQE에서 인덱스로 직접 참조할 수 있습니다. */

struct io_ring_ctx {
    /* 등록된 파일 테이블 */
    GENRADIX(struct io_rsrc_node *) rsrc_data;
    /* sizeof(ptr)=8 → objs_per_leaf = 512 */
};

/* 파일 등록 시: */
struct io_rsrc_node **slot;
slot = genradix_ptr_alloc(&ctx->rsrc_data, idx, GFP_KERNEL);
if (!slot)
    return -ENOMEM;
*slot = node;  /* 포인터를 리프의 해당 슬롯에 저장 */

/* SQE 처리 시 (hot path): */
struct io_rsrc_node **slot;
slot = genradix_ptr(&ctx->rsrc_data, sqe->fd);
if (slot && *slot) {
    /* 등록된 파일 사용: 커널 fd 테이블 접근 생략 */
    io_req_set_rsrc_node(req, *slot);
}

/* 왜 genradix인가? */
/* 1. 사용자가 등록할 수 있는 파일 수가 가변적 (최대 수만 개) */
/* 2. 인덱스 기반 O(1) 접근이 SQE 처리 핫 패스에서 필수 */
/* 3. 등록/해제 빈도가 낮고, 조회 빈도가 매우 높음 */
/* 4. 전체 해제 시 genradix_free 한 번으로 정리 */
코드 설명
  • io_uring의 등록 파일 테이블: 일반적인 read/write 시스템 호출은 fd 번호로 프로세스의 fd 테이블을 조회합니다. io_uring은 자주 사용하는 파일을 미리 등록하여 이 조회를 생략합니다. 등록된 파일은 genradix 인덱스로 직접 접근합니다.
  • 포인터 배열 사용: 이 경우 genradix에 포인터(8B)를 저장합니다. XArray와 비슷한 용도지만, 등록 테이블은 개별 삭제보다 전체 초기화가 주된 패턴이므로 genradix가 적합합니다.
  • 핫 패스 성능: SQE 처리 시 genradix_ptr은 1~2번의 포인터 역참조만 수행합니다. 등록 파일 수가 512개 이하면 depth=0으로 역참조 1회입니다.

안전한 래퍼 함수 패턴

/* 프로덕션 코드에서 권장하는 genradix 래퍼 패턴 */

struct my_subsys {
    struct mutex             lock;
    GENRADIX(struct my_obj)  objects;
    size_t                   nr_objects;
    size_t                   max_objects;
};

/* 안전한 추가: 잠금 + 범위 검사 + 할당 에러 처리 */
static struct my_obj *subsys_add_object(
    struct my_subsys *ss, size_t idx)
{
    struct my_obj *obj;

    if (idx >= ss->max_objects)
        return ERR_PTR(-ERANGE);

    mutex_lock(&ss->lock);

    obj = genradix_ptr_alloc(&ss->objects, idx, GFP_KERNEL);
    if (!obj) {
        mutex_unlock(&ss->lock);
        return ERR_PTR(-ENOMEM);
    }

    /* 중복 추가 방지 */
    if (obj->initialized) {
        mutex_unlock(&ss->lock);
        return ERR_PTR(-EEXIST);
    }

    obj->initialized = true;
    obj->id = idx;
    ss->nr_objects++;

    mutex_unlock(&ss->lock);
    return obj;
}

/* 안전한 조회: 잠금 + 유효성 검사 */
static struct my_obj *subsys_get_object(
    struct my_subsys *ss, size_t idx)
{
    struct my_obj *obj;

    mutex_lock(&ss->lock);

    obj = genradix_ptr(&ss->objects, idx);
    if (!obj || !obj->initialized)
        obj = NULL;

    mutex_unlock(&ss->lock);
    return obj;
}

/* 정리: 전체 해제 */
static void subsys_cleanup(struct my_subsys *ss)
{
    mutex_lock(&ss->lock);
    genradix_free(&ss->objects);
    ss->nr_objects = 0;
    mutex_unlock(&ss->lock);
}
코드 설명
  • mutex 보호: genradix는 내부적으로 cmpxchg만 사용하므로, 데이터 접근에 대한 동시성 보호는 호출자 책임입니다. 이 패턴은 mutex로 모든 접근을 직렬화합니다.
  • initialized 플래그: genradix는 개별 삭제를 지원하지 않으므로, initialized 플래그로 유효한 원소를 구분합니다. __GFP_ZERO로 0 초기화되므로 할당 직후 initialized는 false(0)입니다.
  • max_objects 제한: genradix는 이론적으로 무한 확장 가능하지만, 실제 서브시스템에서는 합리적인 상한선을 두어 메모리 과다 사용을 방지합니다.
  • ERR_PTR 패턴: 커널의 표준 에러 반환 패턴으로, NULL과 에러를 구분합니다. 호출자는 IS_ERR()로 에러를 확인하고 PTR_ERR()로 에러 코드를 추출합니다.

참고 자료

커널 소스

커널 문서

외부 자료