B-tree (lib/btree)

리눅스 커널의 범용 인메모리 B-tree 구현(lib/btree.c, lib/btree.h)을 심층 분석합니다. btree_head/btree_geo 구조체(Struct), 32/64/128비트 키 타입, 검색/삽입/삭제 알고리즘, visitor 패턴을 통한 순회, mempool 기반 메모리 관리(Memory Management), 그리고 Maple Tree/rbtree와의 비교까지 포괄합니다. B-tree 이론부터 커널 소스 수준의 구현 세부사항, btrfs의 온디스크 B-tree+와의 차이, 성능 특성, 디버깅(Debugging) 기법까지 다룹니다.

전제 조건: Red-Black Tree (rbtree) 문서를 먼저 읽으세요. B-tree는 rbtree와 함께 커널의 핵심 탐색 자료구조이며, 두 자료구조의 트레이드오프를 이해해야 올바른 선택이 가능합니다.
일상 비유: B-tree는 도서관 색인 카드 시스템과 같습니다. 하나의 색인 카드(노드)에 여러 개의 제목(키)이 정렬되어 있고, 각 제목 사이에 "이 범위의 책은 저쪽 서랍을 보세요"라는 안내(자식 포인터)가 있습니다. 루트 카드에서 시작하여 2~3번만 서랍을 열면 수만 권의 책 중 원하는 것을 찾을 수 있습니다.

핵심 요약

  • 자기 균형 트리 -- B-tree는 삽입/삭제 시 자동으로 균형을 유지하며, 모든 리프가 같은 깊이에 위치합니다.
  • 노드당 다중 키 -- 하나의 노드에 여러 키-값 쌍을 저장하여 트리 높이를 낮추고, 캐시(Cache) 지역성을 개선합니다.
  • 3가지 키 타입 -- btree_geo32(32비트), btree_geo64(64비트), btree_geo128(128비트) 키를 지원합니다. 각각 unsigned long 배열로 표현되며, 키 크기가 노드당 저장 가능한 쌍 수를 결정합니다.
  • mempool 기반 -- 노드 할당/해제에 mempool을 사용하여 메모리 부족 상황에서도 안정적으로 동작합니다.
  • visitor 패턴 -- btree_visitorbtree_grim_visitor로 트리를 순회하며, grim_visitor는 순회와 동시에 트리를 해체합니다.

단계별 이해

  1. B-tree 이론 파악
    최소 차수, 노드 키 범위, 높이 공식 등 기본 속성을 이해합니다.
  2. 커널 구조체 분석
    btree_head, btree_geo 구조체의 필드와 의미를 파악합니다.
  3. 키 타입 이해
    geo32/64/128 각각의 레이아웃과 슬롯 계산 방식을 이해합니다.
  4. 핵심 연산 추적
    검색, 삽입(분할 포함), 삭제(병합 포함) 알고리즘을 코드 수준에서 따라갑니다.
  5. 실전 활용과 비교
    visitor 패턴, mempool 통합, Maple Tree/rbtree와의 차이를 이해하고 적절한 선택 기준을 세웁니다.
관련 커널 소스: lib/btree.c (구현), include/linux/btree.h (헤더), include/linux/btree-128.h / btree-type.h (타입별 래퍼 매크로(Macro)). Joern Engel이 LogFS 파일시스템(Filesystem)을 위해 2009년(v2.6.31)에 기여하였으며, 이후 범용 인메모리 B-tree로 lib/에 독립 배치되었습니다.

B-tree 개요

B-tree는 1970년 Rudolf Bayer와 Edward McCreight가 발명한 자기 균형 탐색 트리입니다. 하나의 노드에 여러 개의 키를 저장하여 트리 높이를 최소화하고, 디스크 I/O 최적화를 위해 설계되었습니다. 리눅스 커널의 lib/btree.c는 이 개념을 인메모리 환경에 적용한 구현으로, 노드 크기를 128바이트(L1 캐시라인 2개)로 고정하여 캐시 성능을 최적화합니다.

이진 트리와의 차이

특성이진 탐색 트리(BST)B-tree
노드당 키 수1개t-1 ~ 2t-1개 (t: 최소 차수)
노드당 자식 수최대 2개t ~ 2t개
트리 높이 (n개 키)O(log2 n)O(logt n)
캐시 효율낮음 (포인터 추적 빈번)높음 (노드 내 연속 접근)
균형 유지별도 재균형 필요 (AVL/RB)삽입/삭제 시 자동 유지

커널 내 위치와 용도

lib/btree.c는 범용 키-값 저장소로 설계되었습니다. 주요 사용처는 다음과 같습니다:

소스 파일 구조

파일줄 수역할
lib/btree.c~600핵심 구현: 삽입/삭제/검색/순회/병합
include/linux/btree.h~50기본 구조체와 API 선언
include/linux/btree-type.h~130타입별(32/64) 래퍼 매크로 생성
include/linux/btree-128.h~70128비트 키 전용 래퍼 (u64 2개)
코드 규모: lib/btree.c는 약 600줄로, 커널 내 주요 자료구조 중 가장 작은 편입니다. 비교: rbtree ~800줄, Maple Tree ~7,000줄, btrfs ctree ~6,000줄. 단순함이 이 라이브러리의 최대 장점입니다.
B-tree 구조 개관 (차수 t=2, 최대 3키/노드) 30 60 root (height=2) 10 20 40 50 70 80 5,8 12,15 22,25 32,35 42,45 52,55 62,65 72,75 82,90 모든 리프가 같은 깊이(height=0) -- B-tree 핵심 속성 B-tree 속성 (t=2) 1. 모든 리프 동일 깊이 2. 내부 노드 키: [1, 3]개 3. 내부 노드 자식: [2, 4]개 4. 루트는 최소 1개 키 5. 키는 노드 내에서 정렬됨 6. n개 키 노드는 n+1개 자식
B-tree의 계층적 구조: 루트에서 리프까지 모든 경로의 길이가 동일합니다

B-tree 이론

B-tree는 데이터베이스와 파일시스템의 근간이 되는 자료구조로, 1970년 Rudolf Bayer와 Edward McCreight가 Boeing Research Labs에서 발명했습니다. 이름의 "B"에 대해서는 "Balanced", "Bayer", "Boeing" 등 여러 해석이 있지만, 원저자는 공식적으로 밝히지 않았습니다.

B-tree의 핵심은 최소 차수(minimum degree) t로 정의되는 5가지 속성입니다. 커널의 lib/btree.c는 노드 크기 128바이트에서 키 길이에 따라 t가 자동 결정됩니다.

역사적 맥락: B-tree는 디스크 기반 시스템을 위해 설계되었지만, 현대 CPU의 캐시 계층 구조에서도 동일한 원리가 적용됩니다. 디스크의 "블록 읽기"가 CPU의 "캐시라인 로드"로 대체될 뿐, 하나의 접근으로 최대한 많은 정보를 얻는다는 핵심 아이디어는 동일합니다.

B-tree 5대 속성

#속성설명
1균일 리프 깊이모든 리프 노드의 깊이가 동일합니다. 이 속성이 O(logt n) 보장의 핵심입니다.
2키 범위루트가 아닌 내부 노드는 최소 t-1개, 최대 2t-1개의 키를 가집니다.
3자식 범위내부 노드의 자식 수는 키 수 + 1, 즉 [t, 2t] 범위입니다.
4루트 제약루트는 비어있지 않은 한 최소 1개의 키를 가집니다.
5정렬각 노드 내의 키는 오름차순으로 정렬됩니다.

높이 분석

n개의 키를 가진 B-tree의 높이 h는 다음 부등식을 만족합니다:

h ≤ logt((n + 1) / 2)

예시: t=32 (커널 geo32의 실제 값), n=1,000,000
h ≤ log32(500000.5) ≈ 3.79
--> 최대 높이 4, 즉 4번의 노드 접근으로 백만 개 키 검색 가능
캐시 의미: 커널의 B-tree 노드는 128바이트로, L1 캐시라인 2개(64바이트 x 2)에 맞습니다. 높이 3~4의 트리에서 루트와 상위 레벨 노드는 대부분 L1/L2 캐시에 상주하므로, 실제 메모리 접근은 리프 노드 1회로 줄어드는 경우가 많습니다.

B-tree 변형들

학술/산업 분야에서 B-tree의 다양한 변형이 존재합니다. 커널에서 사용되는 것들과의 관계를 정리합니다:

변형특징커널 사용
B-tree내부 노드에도 데이터 저장lib/btree.c
B+tree데이터는 리프에만, 리프 연결 리스트(Linked List)btrfs (fs/btrfs/ctree.c)
B*tree노드 2/3 이상 채움 강제사용 안 함
Maple Tree범위 B-tree + RCU + 다중 노드 유형lib/maple_tree.c
COW B-treeCopy-on-Write 수정btrfs

시간 복잡도 요약

연산최악평균비고
검색O(t · logt n)O(log n)노드 내 선형 검색 포함
삽입O(t · logt n)O(log n)분할(split) 전파 포함
삭제O(t · logt n)O(log n)병합(merge)/재분배 포함
순회O(n)O(n)visitor로 전체 순회

노드 레이아웃

커널 B-tree의 노드는 128바이트(NODESIZE) 고정 크기의 unsigned long 배열입니다. 키, 값(포인터), 자식 포인터가 하나의 배열 안에 인터리빙되어 배치됩니다. 이 128바이트라는 크기는 대부분의 아키텍처에서 L1 캐시라인 2개에 해당하며, 노드 하나를 읽는 데 최대 2번의 캐시라인 로드만 필요합니다.

128바이트 선택 근거: 일반적인 L1 캐시라인은 64바이트입니다. 64바이트 노드로는 geo32 기준 3~4쌍만 저장할 수 있어 트리 높이가 증가합니다. 256바이트는 4개 캐시라인을 차지하여 캐시 오염이 커집니다. 128바이트(2 캐시라인)는 노드당 충분한 쌍 수(8개)와 캐시 효율의 균형점입니다.
/* lib/btree.c */
#define NODESIZE     128    /* 바이트 단위 노드 크기 */
#define MAX_KEYLEN   4      /* unsigned long 단위 최대 키 길이 (128비트) */

/* 노드 = unsigned long 배열 (32비트: 32 슬롯, 64비트: 16 슬롯) */
static unsigned long *btree_node_alloc(struct btree_head *head, gfp_t gfp)
{
    unsigned long *node;
    node = mempool_alloc(head->mempool, gfp);
    if (likely(node))
        memset(node, 0, NODESIZE);
    return node;
}
B-tree 노드 내부 레이아웃 (128바이트, geo32 기준) unsigned long node[NODESIZE/sizeof(long)] -- 64비트: 16슬롯, 32비트: 32슬롯 리프: key[0] val[0] key[1] val[1] ... key[n] val[n] (미사용 = 0) 내부 노드 -- 자식 포인터가 val 위치에 저장 key[0] child[0] key[1] child[1] ... key[n] child[n] child[n+1] 슬롯 배치 함수 (lib/btree.c) bkey(geo, node, n) = &node[geo->keylen * n] -- n번째 키 위치 bval(geo, node, n) = (void *)node[geo->no_longs + n] -- n번째 값(자식) 위치 no_longs = geo->keylen * geo->no_pairs -- 키 영역이 차지하는 long 수 no_pairs = NODESIZE / sizeof(long) / (geo->keylen + 1) -- 노드당 키-값 쌍 수
노드는 unsigned long 배열로 표현되며, 키 영역과 값/자식 영역이 분리되어 배치됩니다
주의: 커널 B-tree의 노드 레이아웃은 전통적 B-tree 교과서와 다릅니다. 키-값 쌍이 인터리빙되지 않고, 키 배열과 값 배열이 분리(segregated)되어 있습니다. 이는 키 비교 시 값 포인터를 캐시에 불필요하게 로딩하지 않도록 하기 위한 최적화입니다.

btree_head 구조체

btree_head는 B-tree 인스턴스의 루트를 가리키는 핸들입니다. 모든 B-tree API는 이 구조체를 첫 번째 인자로 받습니다. 이 구조체는 매우 작아서(포인터 2개 + int 1개, 약 20바이트) 스택이나 다른 구조체에 임베딩하기에 적합합니다. rbtree의 rb_root(8바이트)보다는 크지만, Maple Tree의 maple_tree(16바이트)와 비슷한 수준입니다.

/* include/linux/btree.h */
struct btree_head {
    unsigned long *node;       /* 루트 노드 포인터 (NULL이면 빈 트리) */
    mempool_t     *mempool;    /* 노드 할당용 메모리 풀 */
    int            height;     /* 트리 높이 (0 = 리프만 존재) */
};
필드타입설명
nodeunsigned long *루트 노드 배열 포인터. NULL이면 트리가 비어 있음을 의미
mempoolmempool_t *btree_init()에서 생성되며, 128바이트 객체를 할당하는 풀. 최소 예약 개수로 메모리 부족 시에도 노드 할당 보장
heightint트리 높이. 0이면 루트가 리프(또는 빈 트리), 양수이면 루트에서 리프까지의 간선 수
/* 초기화 패턴 */
struct btree_head my_tree;

/* 트리 초기화 -- mempool 생성 포함 */
ret = btree_init(&my_tree);
if (ret)
    return ret;  /* -ENOMEM */

/* ... 사용 ... */

/* 트리 해체 -- 모든 노드 해제 + mempool 파괴 */
btree_destroy(&my_tree);
height 인코딩: height=0이고 node=NULL이면 빈 트리, height=0이고 node!=NULL이면 루트 자체가 유일한 리프입니다. height=1이면 루트 아래에 리프가 있는 2단 구조입니다.

btree_geo 구조체

btree_geo는 B-tree의 기하학적 파라미터를 정의합니다. 키 크기에 따라 노드당 몇 개의 키-값 쌍을 저장할 수 있는지가 결정됩니다.

/* include/linux/btree.h */
struct btree_geo {
    int keylen;     /* 키 길이 (unsigned long 단위) */
    int no_pairs;   /* 노드당 키-값 쌍의 최대 수 */
    int no_longs;   /* 키 영역이 차지하는 총 long 수 = keylen * no_pairs */
};
btree_geo 파라미터 계산 (64비트 시스템 기준) no_pairs = NODESIZE / sizeof(long) / (keylen + 1) = 128 / 8 / (keylen + 1) = 16 / (keylen + 1) btree_geo32 keylen = 1 no_pairs = 16 / 2 = 8 no_longs = 1 x 8 = 8 btree_geo64 keylen = 1 (64비트) / 2 (32비트) no_pairs = 8 (64비트) / 10 (32비트) no_longs = 8 (64비트) / 20 (32비트) btree_geo128 keylen = 2 (64비트) / 4 (32비트) no_pairs = 16/3=5 (64비트) no_longs = 2 x 5 = 10 (64비트) 64비트 시스템 비교 키 비트 keylen no_pairs 최소 차수 t fanout 범위 32비트 1 8 4 [5, 9] 128비트 2 5 3 [3, 6]
키 길이가 늘어나면 노드당 쌍 수(no_pairs)가 줄어들고, 트리가 더 깊어질 수 있습니다
/* lib/btree.c -- geo 상수 정의 */
struct btree_geo btree_geo32 = {
    .keylen  = 1,
    .no_pairs = NODESIZE / sizeof(long) / 2,
    .no_longs = NODESIZE / sizeof(long) / 2,
};
EXPORT_SYMBOL_GPL(btree_geo32);

struct btree_geo btree_geo64 = {
    .keylen  = LONG_PER_U64,
    .no_pairs = NODESIZE / sizeof(long) / (LONG_PER_U64 + 1),
    .no_longs = LONG_PER_U64 * (NODESIZE / sizeof(long) / (LONG_PER_U64 + 1)),
};
EXPORT_SYMBOL_GPL(btree_geo64);

키 타입 (geo32/geo64/geo128)

커널 B-tree는 3가지 키 크기를 지원합니다. 각 키 타입에 대해 편의 래퍼 함수가 btree-type.h 매크로로 생성됩니다. 키 타입 선택은 저장 효율과 트리 높이에 직접적인 영향을 미칩니다. 래퍼 함수는 키의 unsigned long 배열 변환을 캡슐화(Encapsulation)하여, 호출자가 원시 배열을 직접 다룰 필요가 없게 합니다.

키 타입 선택 원칙: 가능한 한 가장 작은 키 타입을 선택하세요. geo32는 노드당 8쌍을 저장하지만, geo128은 5쌍만 저장합니다. 동일한 데이터 양에서 geo128은 60% 더 많은 노드가 필요하고 트리가 더 깊어집니다.

btree_geo32 -- 32비트 키

32비트 키는 unsigned long 1개로 표현됩니다(64비트 시스템에서도 상위 비트는 0). 단순 정수 키가 필요한 경우에 사용합니다. 64비트 시스템에서 geo32를 사용하면 상위 32비트가 0으로 채워지므로 키 공간의 절반을 낭비하지만, 노드당 쌍 수는 geo64와 동일합니다(둘 다 keylen=1).

/* include/linux/btree-type.h 매크로 확장 예시 */
static inline void *btree_lookup32(struct btree_head32 *head, u32 key)
{
    unsigned long _key = key;
    return btree_lookup(&head->h, &btree_geo32, &_key);
}

static inline int btree_insert32(struct btree_head32 *head,
                                   u32 key, void *val, gfp_t gfp)
{
    unsigned long _key = key;
    return btree_insert(&head->h, &btree_geo32, &_key, val, gfp);
}

btree_geo64 -- 64비트 키

64비트 키는 u64를 사용합니다. 32비트 시스템에서는 unsigned long 2개(LONG_PER_U64=2)로 분할되고, 64비트 시스템에서는 1개(LONG_PER_U64=1)로 표현됩니다. 64비트 시스템에서는 geo32와 geo64가 동일한 노드 레이아웃을 가집니다 (둘 다 keylen=1).

LONG_PER_U64 매크로: #define LONG_PER_U64 (sizeof(u64) / sizeof(unsigned long))으로 정의됩니다. 64비트 시스템에서는 1, 32비트 시스템에서는 2입니다. 이 매크로를 통해 동일한 코드가 두 아키텍처에서 올바르게 동작합니다.
/* 64비트 키 사용 예시 */
struct btree_head tree;
u64 key = 0xDEADBEEF12345678ULL;
void *val = my_data;

btree_insert64(&tree, key, val, GFP_KERNEL);
val = btree_lookup64(&tree, key);  /* --> my_data */

btree_geo128 -- 128비트 키

128비트 키는 unsigned long 2개(64비트 시스템) 또는 4개(32비트 시스템)로 구성됩니다. 복합 키(예: inode 번호 + offset)에 유용합니다. 128비트 키는 별도의 헤더 파일(btree-128.h)에 정의되어 있으며, 두 개의 u64를 인자로 받는 편의 함수를 제공합니다.

/* include/linux/btree-128.h */
static inline void *btree_lookup128(struct btree_head128 *head,
                                     u64 k1, u64 k2)
{
    unsigned long key[2] = { k1, k2 };  /* 64비트 시스템 */
    return btree_lookup(&head->h, &btree_geo128, key);
}
128비트 키 활용: LogFS에서는 (inode_number, block_offset) 복합 키를 geo128로 표현하여, 특정 inode의 특정 블록을 단일 B-tree 검색으로 찾습니다. 첫 번째 u64가 major key(inode), 두 번째가 minor key(offset)가 됩니다.
키 타입키 크기래퍼 헤더노드당 쌍 (64비트)주요 용도
btree_geo3232비트btree-type.h8단순 정수 ID, HID report
btree_geo6464비트btree-type.h8 (64비트) / 10 (32비트)주소, 오프셋(Offset)
btree_geo128128비트btree-128.h5복합 키 (inode+offset)

검색 알고리즘

B-tree 검색은 루트에서 시작하여 각 노드에서 키를 비교하며 적절한 자식으로 내려갑니다. 커널 구현은 노드 내에서 선형 검색을 사용합니다. 노드당 최대 8개 키(geo32 기준)이므로 이진 검색 대비 추가 비교 횟수는 미미하고, 분기 예측(Branch Prediction) 실패 비용을 피할 수 있습니다.

선형 vs 이진 검색: 노드당 키가 8개일 때, 이진 검색은 3번 비교, 선형 검색은 최대 8번 비교입니다. 그러나 이진 검색의 분기 예측 실패(branch misprediction, ~15 cycles/회)를 고려하면, 8개 이하에서는 선형 검색이 더 빠른 경우가 많습니다. 커널 B-tree의 노드 크기(128바이트)가 이 최적점을 결정합니다.
/* lib/btree.c -- btree_lookup */
void *btree_lookup(struct btree_head *head, struct btree_geo *geo,
                   unsigned long *key)
{
    int i, height = head->height;
    unsigned long *node = head->node;

    if (height == 0)
        return NULL;

    for ( ; height > 1; height--) {
        for (i = 0; i < geo->no_pairs; i++)
            if (keycmp(geo, node, i, key) <= 0)
                break;
        if (i == geo->no_pairs)
            return NULL;
        node = bval(geo, node, i);   /* 자식 노드로 이동 */
        if (!node)
            return NULL;
    }

    /* 리프 레벨: 정확한 키 매칭 */
    if (!node)
        return NULL;
    for (i = 0; i < geo->no_pairs; i++)
        if (keycmp(geo, node, i, key) == 0)
            return bval(geo, node, i);
    return NULL;
}
검색 경로: key=45 찾기 20 40 60 Step 1: 45 > 40, 45 < 60 --> child[2] (40~60 사이) 43 50 55 Step 2: 45 > 43, 45 < 50 --> child[1] (43~50 사이) 44 45 48 Step 3: key[1] == 45 --> return bval(1) 찾음! val = 0xBEEF 총 3개 노드 방문, height=2인 트리에서 O(log n) 검색 완료
key=45 검색: 루트에서 2번의 분기와 리프에서 1번의 선형 검색으로 완료

keycmp / longcmp 함수

키 비교는 unsigned long 배열을 첫 번째 원소부터 순차적으로 비교합니다. 이는 big-endian 스타일 비교로, 128비트 키에서 첫 번째 u64가 major key 역할을 합니다. 비교 함수의 반환값은 표준 strcmp 규칙을 따릅니다: 음수(l1 < l2), 0(같음), 양수(l1 > l2).

/* lib/btree.c */
static int keycmp(struct btree_geo *geo,
                  unsigned long *node, int pos,
                  unsigned long *key)
{
    return longcmp(bkey(geo, node, pos), key, geo->keylen);
}

static int longcmp(const unsigned long *l1,
                   const unsigned long *l2, size_t n)
{
    int i;
    for (i = 0; i < n; i++) {
        if (l1[i] < l2[i])
            return -1;
        if (l1[i] > l2[i])
            return 1;
    }
    return 0;
}

삽입 알고리즘

B-tree 삽입은 항상 리프 노드에서 이루어집니다. 리프가 가득 찬 경우(no_pairs개 키) 노드 분할(split)이 발생하며, 분할은 부모로 전파될 수 있습니다. 루트까지 전파되면 트리 높이가 1 증가합니다. 삽입 알고리즘은 top-down 방식으로, 루트에서 시작하여 적절한 리프까지 내려가면서 필요한 분할을 수행합니다.

삽입 보장: mempool_alloc()이 성공하는 한, 삽입은 항상 성공합니다. mempool은 최소 예약 객체를 보장하므로, 메모리 압박 상황에서도 일정 수준의 삽입이 가능합니다. 단, 매우 큰 트리에서 연속적인 분할이 필요하면 여러 노드를 할당해야 하므로, mempool 예약 수를 초과할 수 있습니다.
/* lib/btree.c -- btree_insert (핵심 흐름) */
int btree_insert(struct btree_head *head, struct btree_geo *geo,
                 unsigned long *key, void *val, gfp_t gfp)
{
    /* 빈 트리이면 루트 노드 생성 */
    if (!head->node) {
        head->node = btree_node_alloc(head, gfp);
        if (!head->node)
            return -ENOMEM;
        head->height = 1;
    }

    /* 재귀적 삽입 -- 분할이 필요하면 반환값으로 전파 */
    return btree_insert_level(head, geo, key, val,
                             1, gfp);  /* level=1: 리프 */
}

노드 분할 메커니즘

분할 시 가득 찬 노드의 상위 절반 키를 새 노드로 이동하고, 중간 키를 부모로 올립니다(promote). 이 과정은 O(no_pairs) 시간이 소요되며, 주로 memcpy에 해당하는 setkey/setval 반복으로 구성됩니다:

/* lib/btree.c -- btree_splitnode (간략화) */
static unsigned long *btree_splitnode(struct btree_head *head,
    struct btree_geo *geo, unsigned long *node, gfp_t gfp)
{
    unsigned long *new;
    int fill = geo->no_pairs;

    new = btree_node_alloc(head, gfp);
    if (!new)
        return ERR_PTR(-ENOMEM);

    /* 상위 절반을 새 노드로 복사 */
    for (i = 0; i < fill / 2; i++) {
        setkey(geo, new, i, bkey(geo, node, i));
        setval(geo, new, i, bval(geo, node, i));
        clearpair(geo, node, i);
    }

    return new;
}
삽입 + 분할 과정 (no_pairs=4일 때 key=25 삽입) Phase 1: 삽입 전 (노드 가득 참) 10 20 30 40 FULL split! Phase 2: 분할 후 30 부모로 승격 10 20 40 insert 25 Phase 3: key=25 삽입 완료 30 10 20 25 40
노드가 가득 찬 상태에서 삽입하면 분할이 발생하고, 중간 키가 부모로 승격됩니다
메모리 할당 실패: 분할에는 새 노드 할당이 필요합니다. mempool이 보장하는 최소 예약 개수를 초과하면 btree_insert()-ENOMEM을 반환합니다. 호출자는 반드시 반환값을 확인해야 합니다.

btree_insert_level 세부 흐름

실제 삽입은 재귀적 btree_insert_level()에서 처리됩니다. 각 레벨에서 적절한 자식을 찾아 내려가고, 리프에서 삽입 후 분할이 필요하면 상위로 전파합니다:

/* lib/btree.c -- btree_insert_level (간략화) */
static int btree_insert_level(struct btree_head *head,
    struct btree_geo *geo, unsigned long *key,
    void *val, int level, gfp_t gfp)
{
    unsigned long *node = head->node;
    int i, pos, fill, err;

    /* 노드가 가득 찼는지 확인 */
    fill = getfill(geo, node, 0);
    if (fill == geo->no_pairs) {
        /* 분할 필요 → 새 루트 생성 */
        err = btree_grow(head, geo, gfp);
        if (err)
            return err;
    }

    /* 적절한 위치에 키-값 쌍 삽입 */
    /* 기존 키들을 shift하여 정렬 위치 확보 */
    for (i = fill; i > pos; i--) {
        setkey(geo, node, i, bkey(geo, node, i - 1));
        setval(geo, node, i, bval(geo, node, i - 1));
    }
    setkey(geo, node, pos, key);
    setval(geo, node, pos, val);
    return 0;
}

btree_grow -- 트리 높이 증가

루트 노드가 가득 차면 btree_grow()가 호출되어 새 루트를 생성합니다:

/* lib/btree.c -- btree_grow */
static int btree_grow(struct btree_head *head,
                      struct btree_geo *geo, gfp_t gfp)
{
    unsigned long *node;

    node = btree_node_alloc(head, gfp);
    if (!node)
        return -ENOMEM;

    /* 기존 루트의 첫 번째 키를 새 루트에 복사 */
    setkey(geo, node, 0, bkey(geo, head->node, 0));
    /* 기존 루트를 새 루트의 자식으로 설정 */
    setval(geo, node, 0, head->node);

    head->node = node;
    head->height++;
    return 0;
}

삭제 알고리즘

B-tree 삭제는 리프에서의 키 제거, 필요 시 형제 노드와의 재분배(redistribute) 또는 병합(merge)으로 구성됩니다. 커널 구현에서는 삭제 후 노드의 키가 0개가 되면 노드를 해제합니다. 삭제는 삽입과 달리 메모리 할당이 필요하지 않으므로, 항상 성공합니다(키가 존재하면).

삭제의 반환값: btree_remove()는 삭제된 키에 연결되어 있던 값 포인터를 반환합니다. 이를 통해 호출자는 해당 값 객체를 적절히 해제(kfree 등)할 수 있습니다. 키가 존재하지 않으면 NULL을 반환합니다.
/* lib/btree.c -- btree_remove (핵심 흐름 간략화) */
void *btree_remove(struct btree_head *head,
                   struct btree_geo *geo,
                   unsigned long *key)
{
    /* 1. 리프까지 내려가며 키 위치 탐색 */
    /* 2. 키-값 쌍 제거 (배열 shift) */
    /* 3. 노드가 비면(fill==0) 부모에서 자식 포인터 제거 */
    /* 4. 루트가 비고 자식이 1개면 높이 감소 (shrink) */
    return btree_remove_level(head, geo, key, 1);
}

병합과 수축

노드가 비어지면 형제와 병합하거나, 부모에서 해당 자식 포인터를 제거합니다. 루트에 키가 없고 자식이 하나만 남으면 그 자식이 새 루트가 되어 트리 높이가 1 감소합니다.

삭제 + 수축 과정 (key=30 삭제) 삭제 전 30 root 10 20 40 remove(30) 삭제 직후: 루트에서 30 제거 (빈 루트) 10 20 shrink 수축 완료 (height--) 10 20 40 새 루트 (height=0) 커널 B-tree는 언더플로우 시 재분배 대신 노드 해제 + 수축으로 처리합니다 (교과서 B-tree와 다름)
키 삭제 후 루트가 비면 자식이 새 루트로 승격되어 트리 높이가 감소합니다

btree_shrink -- 트리 높이 감소

삭제 후 루트에 키가 없고 자식이 하나만 남으면, 그 자식이 새 루트가 됩니다:

/* lib/btree.c -- btree_shrink (간략화) */
static void btree_shrink(struct btree_head *head,
                         struct btree_geo *geo)
{
    unsigned long *node = head->node;
    unsigned long *child;
    int fill = getfill(geo, node, 0);

    if (fill != 1)
        return;  /* 2개 이상이면 수축 불필요 */

    child = bval(geo, node, 0);
    head->node = child;
    head->height--;

    mempool_free(node, head->mempool);  /* 기존 루트 해제 */
}

btree_remove_level 세부 흐름

삭제의 핵심인 btree_remove_level()은 다음 단계로 동작합니다:

/* 삭제 핵심 흐름 (의사 코드) */
static void *btree_remove_level(head, geo, key, level)
{
    /* 1. 키 위치 탐색 */
    for (i = 0; i < geo->no_pairs; i++)
        if (keycmp(geo, node, i, key) == 0)
            break;

    /* 2. 값 보존 (반환용) */
    val = bval(geo, node, i);

    /* 3. 배열 shift로 gap 제거 */
    for (j = i; j < fill - 1; j++) {
        setkey(geo, node, j, bkey(geo, node, j + 1));
        setval(geo, node, j, bval(geo, node, j + 1));
    }
    clearpair(geo, node, fill - 1);

    /* 4. 필요 시 수축 */
    if (getfill(geo, head->node, 0) == 1)
        btree_shrink(head, geo);

    return val;
}
교과서와의 차이: 표준 B-tree 삭제 알고리즘은 언더플로우(키 수 < t-1) 시 형제에서 키를 빌리거나(rotate) 형제와 병합(merge)합니다. 커널의 lib/btree.c는 이를 단순화하여, 노드가 완전히 비면 해제하고 부모에서 해당 엔트리를 제거하는 방식을 사용합니다. 이로 인해 트리가 최적 균형 상태보다 약간 희소해질 수 있지만, 구현 복잡도가 크게 줄어듭니다.

update 연산

btree_update()는 이미 존재하는 키의 값(포인터)을 교체합니다. 새로운 노드 할당이 필요하지 않으므로 항상 성공하며(키가 존재하면), GFP 플래그가 필요 없습니다. 이전 값은 반환되지 않으므로, 이전 값의 해제가 필요하면 호출 전에 btree_lookup()으로 먼저 조회해야 합니다.

/* lib/btree.c */
int btree_update(struct btree_head *head, struct btree_geo *geo,
                 unsigned long *key, void *val)
{
    /* 트리를 순회하며 키를 찾고, bval 슬롯만 교체 */
    unsigned long *node = head->node;
    int i, height = head->height;

    for ( ; height > 1; height--) {
        for (i = 0; i < geo->no_pairs; i++)
            if (keycmp(geo, node, i, key) <= 0)
                break;
        node = bval(geo, node, i);
    }

    for (i = 0; i < geo->no_pairs; i++)
        if (keycmp(geo, node, i, key) == 0) {
            setval(geo, node, i, val);
            return 0;
        }
    return -ENOENT;  /* 키가 존재하지 않음 */
}
insert vs update 선택: btree_insert()는 키가 이미 존재하면 기존 값을 덮어쓰지 않고 중복 삽입을 시도합니다(동일 키 두 개 가능). 기존 키의 값을 변경하려면 반드시 btree_update()를 사용하거나, btree_remove()btree_insert()를 호출해야 합니다.

Upsert 패턴 (Insert or Update)

키가 존재하면 업데이트, 없으면 삽입하는 "upsert" 패턴입니다:

/* upsert: 있으면 update, 없으면 insert */
static int btree_upsert32(struct btree_head32 *head,
                           u32 key, void *val, gfp_t gfp)
{
    int ret;

    /* 먼저 update 시도 (메모리 할당 불필요) */
    ret = btree_update32(head, key, val);
    if (ret == -ENOENT)
        /* 키 없음 → insert */
        ret = btree_insert32(head, key, val, gfp);
    return ret;
}

/* remove + insert 방식 (이전 값 보존 필요 시) */
static int btree_replace32(struct btree_head32 *head,
                            u32 key, void *new_val,
                            void **old_val, gfp_t gfp)
{
    *old_val = btree_remove32(head, key);
    return btree_insert32(head, key, new_val, gfp);
}

완전한 사용 예시

커널 모듈(Kernel Module)에서 B-tree를 사용하는 전체 흐름입니다:

#include <linux/btree.h>
#include <linux/slab.h>

struct my_data {
    u32 id;
    char name[32];
};

static struct btree_head32 my_tree;

static int __init my_init(void)
{
    struct my_data *d;
    int ret;

    /* 1. 트리 초기화 */
    ret = btree_init32(&my_tree);
    if (ret)
        return ret;

    /* 2. 데이터 삽입 */
    d = kmalloc(sizeof(*d), GFP_KERNEL);
    if (!d) { ret = -ENOMEM; goto err; }
    d->id = 42;
    strscpy(d->name, "test", sizeof(d->name));

    ret = btree_insert32(&my_tree, 42, d, GFP_KERNEL);
    if (ret) { kfree(d); goto err; }

    /* 3. 검색 */
    d = btree_lookup32(&my_tree, 42);
    if (d)
        pr_info("found: %s\n", d->name);

    return 0;

err:
    btree_destroy32(&my_tree);
    return ret;
}

static void __exit my_exit(void)
{
    /* grim_visitor로 모든 데이터 해제 + 노드 해제 */
    btree_grim_visitor32(&my_tree, free_entry);
    btree_destroy32(&my_tree);
}

visitor 패턴

B-tree 순회에는 두 가지 visitor가 있습니다. btree_visitor는 읽기 전용(Read-Only) 순회, btree_grim_visitor는 순회와 동시에 모든 노드를 해제하는 파괴적 순회입니다. visitor 패턴은 GoF 디자인 패턴의 Visitor 패턴과 유사하며, 트리 구조의 순회 로직과 처리 로직을 분리합니다.

네이밍 유래: "grim visitor"의 "grim"은 "Grim Reaper(사신)"에서 따온 것입니다. 방문하는 모든 노드를 "거둬가는(해제하는)" 파괴적 순회를 비유한 이름입니다.
/* include/linux/btree.h */
typedef void (*visitor_t)(void *elem, unsigned long opaque,
                          unsigned long *key, size_t index,
                          void *__func);

/* 읽기 전용 순회 -- 트리 구조 유지 */
size_t btree_visitor(struct btree_head *head,
                     struct btree_geo *geo,
                     unsigned long opaque,
                     visitor_t func);

/* 파괴적 순회 -- 방문 후 모든 노드 해제 */
size_t btree_grim_visitor(struct btree_head *head,
                          struct btree_geo *geo,
                          unsigned long opaque,
                          visitor_t func);

visitor 사용 예시

/* 모든 항목 출력 */
static void print_entry(void *elem, unsigned long opaque,
                       unsigned long *key, size_t index,
                       void *__func)
{
    pr_info("[%zu] key=%lu val=%p\n", index, key[0], elem);
}

/* 순회 호출 */
size_t count = btree_visitor(&my_tree, &btree_geo32, 0, print_entry);
pr_info("total: %zu entries\n", count);

grim_visitor를 이용한 트리 해체

/* 모든 값 객체를 kfree한 후 트리 파괴 */
static void free_entry(void *elem, unsigned long opaque,
                      unsigned long *key, size_t index,
                      void *__func)
{
    kfree(elem);  /* 값 객체 해제 */
}

/* grim_visitor는 콜백 후 노드도 해제 */
btree_grim_visitor(&my_tree, &btree_geo32, 0, free_entry);
/* 이 시점에서 트리는 완전히 비어 있음 (head->node = NULL) */
주의: btree_grim_visitor() 호출 후에는 트리가 파괴되므로, 이후 btree_destroy()만 호출하면 됩니다(mempool 해제). btree_visitor()는 트리를 수정하지 않으므로, 콜백(Callback) 내에서 btree_remove()를 호출하면 안 됩니다.

메모리 풀 통합

커널 B-tree는 노드 할당에 mempool을 사용합니다. mempool은 최소 개수의 객체를 미리 예약하여, GFP_ATOMIC 컨텍스트에서도 할당 실패를 방지합니다. 이는 인터럽트(Interrupt) 핸들러(Handler)나 spinlock 보호 구간에서도 B-tree 삽입을 안전하게 수행할 수 있게 합니다.

btree_alloc / btree_free 콜백

mempool의 할당/해제 백엔드로 사용되는 함수입니다:

/* lib/btree.c */
static void *btree_alloc(gfp_t gfp_mask, void *pool_data)
{
    return kmalloc(NODESIZE, gfp_mask);
}

static void btree_free(void *element, void *pool_data)
{
    kfree(element);
}
SLAB 할당자와의 관계: kmalloc(128, ...)은 SLUB 할당자에서 kmalloc-128 slab 캐시를 사용합니다. 이 캐시는 정확히 128바이트 객체를 위해 최적화되어 있으므로, 내부 단편화(Fragmentation)가 없습니다. /proc/slabinfo에서 kmalloc-128 항목으로 B-tree 노드의 메모리 사용량을 추적할 수 있습니다.
/* lib/btree.c -- 초기화 */
int btree_init(struct btree_head *head)
{
    head->node = NULL;
    head->height = 0;
    head->mempool = mempool_create(0,
        btree_alloc, btree_free, NULL);
    if (!head->mempool)
        return -ENOMEM;
    return 0;
}

/* lib/btree.c -- 해체 */
void btree_destroy(struct btree_head *head)
{
    mempool_free(head->node, head->mempool);
    mempool_destroy(head->mempool);
    head->mempool = NULL;
}
mempool과 B-tree 노드 관리 btree_head .node = root .mempool = pool .height = 2 mempool_t min_nr = 0 (초기) alloc = btree_alloc free = btree_free kmalloc size = 128 bytes (NODESIZE) SLAB/SLUB 할당 노드 할당/해제 흐름 btree_insert() btree_node_alloc() mempool_alloc(pool, gfp) btree_remove() btree_node_free() mempool_free(node, pool) btree_destroy() mempool_destroy(pool) -- 잔여 객체 전부 kfree
mempool이 btree 노드의 할당/해제를 관리하며, 백엔드로 kmalloc(SLAB/SLUB)을 사용합니다
GFP 플래그 가이드: 프로세스(Process) 컨텍스트에서는 GFP_KERNEL, 인터럽트/softirq 컨텍스트에서는 GFP_ATOMIC을 사용합니다. mempool은 GFP_ATOMIC에서도 예약된 객체를 반환하므로 삽입 실패 확률이 낮습니다.

API 레퍼런스

lib/btree.c가 내보내는 전체 API 목록입니다. 모든 함수는 EXPORT_SYMBOL_GPL로 모듈에서 사용 가능합니다.

함수반환값설명
btree_init(head)int (0 또는 -ENOMEM)트리 초기화, mempool 생성
btree_destroy(head)void트리 해체, mempool 파괴
btree_lookup(head, geo, key)void * (NULL: 없음)키로 값 검색
btree_insert(head, geo, key, val, gfp)int (0 또는 -ENOMEM)키-값 쌍 삽입
btree_update(head, geo, key, val)int (0 또는 -ENOENT)기존 키의 값 교체
btree_remove(head, geo, key)void * (이전 값 또는 NULL)키-값 쌍 삭제, 이전 값 반환
btree_merge(target, victim, geo, gfp)int두 트리 병합 (victim -> target)
btree_last(head, geo, key)void *최대 키와 그 값 반환
btree_get_prev(head, geo, key)void *key 이하의 최대 키와 값 반환
btree_visitor(head, geo, opaque, func)size_t읽기 전용 순회, 항목 수 반환
btree_grim_visitor(head, geo, opaque, func)size_t파괴적 순회 (노드 해제 포함)

타입별 래퍼

include/linux/btree-type.hBTREE_TYPE_SUFFIX 매크로는 위 API 각각에 대해 32/64 접미사 래퍼를 생성합니다:

/* btree-type.h 매크로 사용법 */
BTREE_TYPE_SUFFIX              /* 32, 64 */
btree_lookup32(head, key)      /* -> btree_lookup(&head->h, &btree_geo32, &key) */
btree_insert32(head, key, val, gfp)
btree_update32(head, key, val)
btree_remove32(head, key)
btree_last32(head, key)
btree_get_prev32(head, key)

btree_merge -- 두 트리 병합

btree_merge()는 victim 트리의 모든 키-값 쌍을 target 트리로 이동합니다:

/* lib/btree.c */
int btree_merge(struct btree_head *target,
                struct btree_head *victim,
                struct btree_geo *geo, gfp_t gfp)
{
    unsigned long key[MAX_KEYLEN];
    void *val;
    int err;

    /* victim에서 하나씩 꺼내서 target에 삽입 */
    while ((val = btree_last(victim, geo, key))) {
        err = btree_insert(target, geo, key, val, gfp);
        if (err)
            return err;
        btree_remove(victim, geo, key);
    }
    return 0;
}
merge 비용: btree_merge()는 O(n · log n) 시간이 소요됩니다(n = victim 크기). 각 키를 개별적으로 remove+insert하기 때문입니다. 대량의 데이터를 병합해야 한다면 성능에 주의하세요.

에러 코드 요약

에러 코드발생 함수원인대처
-ENOMEMbtree_initmempool 생성 실패메모리 확보 후 재시도
-ENOMEMbtree_insert노드 할당 실패 (분할 시)GFP 플래그 확인, mempool 크기 조정
-ENOENTbtree_update키가 존재하지 않음insert로 전환 또는 키 확인
NULLbtree_lookup키가 존재하지 않음정상 (not-found 의미)
NULLbtree_remove키가 존재하지 않음정상 (이미 삭제됨)

btree_last / btree_get_prev

이 두 함수는 범위 스캔과 역순 순회의 핵심입니다. 커널 B-tree에는 정순(forward) 이터레이터가 없으므로, 역순 순회가 키 순서 기반 범위 검색의 유일한 방법입니다. btree_last()는 최대 키를 O(height) 시간에 반환하고, btree_get_prev()는 주어진 키 이하의 최대 키를 O(height) 시간에 반환합니다.

정순 순회가 없는 이유: 커널 B-tree의 노드에는 형제 노드로의 포인터(sibling pointer)가 없습니다. B-tree+ 변형에서는 리프 노드가 연결 리스트로 연결되어 정순 순회가 효율적이지만, lib/btree.c는 순수 B-tree이므로 정순 범위 스캔이 비효율적입니다. 전체 순회가 필요하면 btree_visitor()를 사용하세요.

btree_last -- 최대 키 조회

/* lib/btree.c */
void *btree_last(struct btree_head *head,
                 struct btree_geo *geo,
                 unsigned long *key)
{
    int height = head->height;
    unsigned long *node = head->node;

    if (height == 0)
        return NULL;

    /* 각 레벨에서 가장 마지막(최대) 키 쪽으로 이동 */
    for ( ; height > 1; height--)
        node = bval(geo, node, 0);  /* 첫 번째 = 가장 큰 키 */

    longcpy(key, bkey(geo, node, 0), geo->keylen);
    return bval(geo, node, 0);
}
역순 정렬: 커널 B-tree는 키를 내림차순으로 저장합니다(slot 0이 가장 큰 키). 이는 일반적인 교과서 B-tree(오름차순)와 반대입니다. 이 설계 덕분에 btree_last()는 단순히 slot 0을 따라가기만 하면 됩니다.

btree_get_prev -- 이전 키 조회 (범위 스캔)

btree_get_prev()는 주어진 키보다 작거나 같은 가장 큰 키를 찾습니다. 이를 반복 호출하면 역순 범위 스캔이 가능합니다:

/* 역순 범위 스캔 패턴 */
unsigned long key;
void *val;

/* 최대 키부터 시작 */
val = btree_last(&tree, &btree_geo32, &key);
while (val) {
    process(key, val);
    /* 현재 키보다 작은 다음 키 찾기 */
    val = btree_get_prev(&tree, &btree_geo32, &key);
}

범위 쿼리 패턴

특정 범위 [lo, hi] 내의 모든 키를 찾으려면 btree_get_prev()를 반복 호출하면서 범위를 체크합니다:

/* 범위 [lo, hi] 내 모든 항목 처리 */
static void btree_range_query(struct btree_head *head,
                               unsigned long lo,
                               unsigned long hi)
{
    unsigned long key = hi;
    void *val;

    /* hi 이하의 최대 키부터 시작 */
    val = btree_get_prev(head, &btree_geo32, &key);
    while (val && key >= lo) {
        process(key, val);
        if (key == 0)
            break;  /* 언더플로 방지 */
        key--;  /* 현재 키보다 작은 키 탐색 */
        val = btree_get_prev(head, &btree_geo32, &key);
    }
}

/* 주의: 이 패턴은 O(range * log n)으로,
 * 범위가 넓으면 btree_visitor()가 더 효율적 */
범위 쿼리 성능: btree_get_prev() 기반 범위 쿼리는 각 호출마다 루트에서 리프까지 순회하므로 O(k · log n)입니다 (k: 범위 내 키 수). 범위가 넓으면 btree_visitor()로 전체 순회 후 필터링하는 것이 O(n)으로 더 효율적일 수 있습니다.

내부 헬퍼 함수

lib/btree.c의 핵심 내부 함수들입니다. 이 함수들은 static으로 외부에 노출되지 않지만, 알고리즘 이해에 필수적입니다:

함수역할
bkey(geo, node, n)n번째 키의 시작 주소 반환 (&node[geo->keylen * n])
bval(geo, node, n)n번째 값 반환 ((void *)node[geo->no_longs + n])
setkey(geo, node, n, key)n번째 슬롯에 키 복사 (longcpy 사용)
setval(geo, node, n, val)n번째 슬롯에 값 설정
clearpair(geo, node, n)n번째 키-값 쌍을 0으로 클리어
getfill(geo, node, start)start부터 첫 번째 빈 슬롯까지의 채워진 쌍 수 반환
keycmp(geo, node, pos, key)노드의 pos번째 키와 key 비교 (-1/0/1)
keyzero(geo, key)키가 모두 0인지 확인 (빈 슬롯 판별)
/* lib/btree.c -- getfill: 채워진 슬롯 수 계산 */
static int getfill(struct btree_geo *geo,
                   unsigned long *node, int start)
{
    int i;
    for (i = start; i < geo->no_pairs; i++)
        if (!bval(geo, node, i))
            break;
    return i;
}

/* lib/btree.c -- keyzero: 빈 키 판별 */
static int keyzero(struct btree_geo *geo,
                   unsigned long *key)
{
    int i;
    for (i = 0; i < geo->keylen; i++)
        if (key[i])
            return 0;
    return 1;
}
키 값 0의 제약: keyzero()는 키가 모두 0이면 "빈 슬롯"으로 판단합니다. 따라서 키 값 0은 사용할 수 없습니다. 0을 키로 삽입하면 해당 슬롯이 빈 것으로 잘못 인식되어 트리 구조가 손상될 수 있습니다. 이는 lib/btree.c의 알려진 제약사항입니다.

Maple Tree 비교

Maple Tree(Linux 6.1+)는 B-tree에서 영감을 받은 범위 기반 자료구조로, rbtree를 대체하여 VMA(Virtual Memory Area) 관리에 사용됩니다. Liam Howlett(Oracle)이 개발했으며, Matthew Wilcox의 xarray와 B-tree의 아이디어를 결합한 것입니다. lib/btree.c의 범용 B-tree와는 설계 목적과 구현이 크게 다릅니다.

왜 lib/btree.c로 VMA를 관리하지 않는가? VMA 관리에는 (1) 범위 검색(주소 -> VMA 매핑), (2) gap 검색(빈 주소 공간(Address Space) 찾기), (3) RCU 기반 동시 읽기가 필요합니다. lib/btree.c는 포인트 키만 지원하고 RCU가 없으므로 이 요구사항을 충족하지 못합니다. Maple Tree는 이 세 가지를 모두 네이티브로 지원합니다.
lib/btree.c B-tree vs Maple Tree lib/btree.c (범용 B-tree) 키 타입: 고정 (32/64/128비트) 노드 크기: 128바이트 고정 범위 검색: btree_get_prev 반복 RCU 지원: 없음 잠금: 호출자 책임 순회: visitor 콜백 용도: 범용 키-값 맵 메모리: mempool (kmalloc) 장점: 단순, 이해 용이 단점: 범위 쿼리 비효율 코드 크기: ~600줄 도입: v2.6.31 (2009) lib/maple_tree.c (Maple Tree) 키 타입: unsigned long (범위) 노드 크기: 256바이트 (가변) 범위 검색: 네이티브 지원 (gap) RCU 지원: 내장 (rcu_read_lock) 잠금: 내부 lock + RCU 순회: ma_state 이터레이터 용도: VMA 관리 전용 최적화 메모리: SLAB 캐시 (maple_node) 장점: 범위 쿼리, RCU, gap 탐색 단점: 복잡 (~7,000줄) 코드 크기: ~7,000줄 도입: v6.1 (2022)
lib/btree.c는 단순한 키-값 맵, Maple Tree는 범위 기반 VMA 관리에 최적화된 특수 자료구조입니다
비교 항목lib/btree.cMaple Tree
기본 단위키-값 쌍 (포인트 키)범위-값 쌍 ([start, end] -> val)
Gap 검색미지원mas_empty_area()로 빈 구간 검색
동시성호출자가 락 관리내부 RCU + lock
노드 유형단일 (128바이트 배열)maple_range_16/32/64 + maple_arange 등 다중
교체 대상-VMA rbtree + linked list
선택 가이드: VMA 관리나 범위 검색이 필요하면 Maple Tree, 단순한 키-값 매핑(정수 키, 포인트 검색)이면 lib/btree.c 또는 rbtree를 사용하세요. 자세한 내용은 Maple Tree 자료구조 문서를 참고하세요.

rbtree 비교

Red-Black Tree(rbtree)는 커널에서 가장 널리 사용되는 탐색 트리입니다. CFS 스케줄러(Scheduler), VMA 관리(Linux 6.0 이전), ext4 extent tree 등 수백 곳에서 사용됩니다. B-tree와의 차이를 이해하면 적절한 자료구조를 선택할 수 있습니다.

rbtree의 보편성: git grep 'rb_root'으로 검색하면 커널 전체에서 수천 건의 rbtree 사용이 나옵니다. 반면 btree_head는 수십 건에 불과합니다. 이는 rbtree가 커널 개발자들에게 "기본값"으로 자리잡았기 때문이며, lib/btree.c가 기능적으로 부족해서가 아닙니다.
비교 항목rbtreelib/btree.c
노드 크기32~40바이트 (rb_node)128바이트 고정
캐시 효율낮음 (포인터 추적 많음)높음 (노드 내 연속 접근)
삽입 오버헤드(Overhead)재색칠 + 최대 2회 회전분할 전파 (노드 복사)
메모리 사용키당 ~40바이트키당 ~16~32바이트 (노드 공유)
순서 순회rb_next/rb_prev (O(log n))visitor (O(n))
커널 사용 빈도매우 높음 (CFS, VMA, ext4 등)낮음 (LogFS, HID)
RCU 지원별도 구현 필요미지원
augmented 트리지원 (interval tree 등)미지원
캐시 성능: B-tree는 노드당 다중 키를 저장하므로, 하나의 캐시라인 접근으로 여러 키를 비교할 수 있습니다. 반면 rbtree는 각 키 비교마다 포인터를 따라가야 하므로 캐시 미스가 더 자주 발생합니다. 그러나 rbtree의 노드 크기가 작으므로 더 많은 노드가 캐시에 들어갈 수 있어, 실제 성능은 워크로드에 따라 다릅니다.

lib/btree.c 선택 시점

rbtree 선택 시점

실전 비교: 어떤 자료구조를 선택할 것인가

요구사항추천이유
VMA 관리 (Linux 6.1+)Maple Tree범위 기반, gap 검색 네이티브
스케줄러 타임라인rbtree정순 순회, augmented 지원
구간 검색 (overlap)Interval Treeaugmented rbtree 기반
정수 키 맵핑 (소규모)lib/btree.c단순, mempool 보장
정수 키 맵핑 (대규모)xarray/radix tree해시(Hash) 기반으로 더 빠름
문자열 키rbtree + strcmp가변 키 지원

btrfs B-tree 참고

btrfs 파일시스템도 "B-tree"를 사용하지만, lib/btree.c와는 완전히 별개의 구현입니다. btrfs의 B-tree+는 온디스크(on-disk) 자료구조로, COW(Copy-on-Write) 시맨틱과 결합됩니다. 사실 "btrfs"라는 이름 자체가 "B-tree File System"의 약자입니다.

혼동 주의: lib/btree.c와 btrfs의 B-tree는 이름만 같을 뿐 완전히 다른 코드입니다. btrfs는 자체 B-tree+ 구현(fs/btrfs/ctree.c)을 가지고 있으며, lib/btree.c를 사용하지 않습니다. 코드 리뷰나 디버깅 시 혼동하지 않도록 주의하세요.
lib/btree.c vs btrfs B-tree+ lib/btree.c (인메모리) 저장: 메모리 전용 (휘발성) 노드 크기: 128바이트 COW: 없음 (직접 수정) 키: unsigned long 배열 값: void * (포인터) 트랜잭션: 없음 체크섬: 없음 스냅샷: 없음 용도: 범용 키-값 맵 btrfs B-tree+ (온디스크) 저장: 디스크 + 페이지 캐시 노드 크기: 4KB~64KB (페이지 정렬) COW: 전면 적용 (스냅샷 지원) 키: (objectid, type, offset) 3튜플 값: 가변 길이 inline 데이터 트랜잭션: 전체 트리 단위 atomic 체크섬: 노드별 CRC32C/SHA256 스냅샷: 참조 카운트 기반 공유 용도: 파일시스템 메타데이터
같은 "B-tree"라는 이름이지만, 설계 목적과 구현이 완전히 다른 별개의 자료구조입니다
특성lib/btree.cbtrfs B-tree+
키 형식unsigned long[1~4]struct btrfs_key (objectid + type + offset, 17바이트)
리프 데이터포인터 1개가변 길이 inline 아이템
수정 방식직접 수정 (in-place)COW (새 노드 할당 후 교체)
소스 위치lib/btree.cfs/btrfs/ctree.c
코드 규모~600줄~6,000줄 (ctree.c 단독)
B-tree+와 B-tree의 차이: btrfs는 B-tree+ 변형을 사용합니다. B-tree+에서는 모든 데이터가 리프에만 저장되고, 내부 노드는 키와 자식 포인터만 갖습니다. 이는 리프 노드의 연결 리스트를 통한 효율적인 범위 스캔을 가능하게 합니다. lib/btree.c의 B-tree는 내부 노드에도 값(포인터)이 저장됩니다.

btrfs 키 형식

btrfs의 키는 3-튜플 구조로, lib/btree.c의 단순 정수 키보다 훨씬 복잡합니다:

/* fs/btrfs/ctree.h */
struct btrfs_key {
    __le64 objectid;   /* 8바이트: 객체 ID (inode 번호 등) */
    __u8   type;       /* 1바이트: 아이템 유형 (INODE_ITEM 등) */
    __le64 offset;     /* 8바이트: 유형별 오프셋 (블록 번호 등) */
} __attribute__((__packed__));  /* 총 17바이트 */

btrfs COW 시맨틱

btrfs는 노드를 직접 수정하지 않고, 수정된 복사본을 생성(Copy-on-Write)합니다. 이는 원자적(Atomic) 트랜잭션(Transaction)과 스냅샷 지원의 핵심이지만, lib/btree.c에는 없는 기능입니다. COW의 비용은 수정이 루트까지 전파되어야 하므로 경로(path) 전체에 대한 복사가 필요하다는 점입니다:

lib/btree.c: node[slot] = new_value  (직접 수정, O(1))
btrfs:       new_node = copy(node)   (복사 후 수정, O(node_size))
             parent->child = new_node
             --> 수정이 루트까지 전파 (path COW)
혼동 주의: "커널의 B-tree"라고 하면 lib/btree.c와 btrfs의 B-tree+ 모두를 가리킬 수 있습니다. 문맥에 따라 구분이 필요합니다. 이 문서에서 "커널 B-tree"는 lib/btree.c를 가리킵니다.

성능 분석

커널 B-tree의 실제 성능은 키 타입, 트리 크기, 접근 패턴에 따라 달라집니다. 이 섹션에서는 이론적 복잡도부터 캐시 성능, 메모리 오버헤드, 확장성까지 종합적으로 분석합니다.

성능 측정 도구: 커널 내 B-tree 성능 측정에는 ktime_get_ns()로 개별 연산 시간을 측정하거나, perf stat으로 캐시 미스를 프로파일링(Profiling)할 수 있습니다. ftrace의 함수 트레이싱으로 btree_lookup/btree_insert 호출 빈도와 소요 시간을 추적할 수도 있습니다.

이론적 복잡도

연산geo32 (t=4)geo128 (t=3)rbtree 대비
검색~4 · log4 n~5 · log3 n비슷 (상수 차이)
삽입~4 · log4 n + split 비용~5 · log3 n + split 비용분할 > 회전 비용
삭제~4 · log4 n~5 · log3 n비슷
메모리/키~16바이트~25바이트rbtree: ~40바이트

캐시 성능 분석

B-tree 노드 접근 패턴:
  - 노드 크기 128바이트 = L1 캐시라인 2개 (64바이트 x 2)
  - 한 번의 노드 접근으로 최대 8개 키 비교 (geo32)
  - 캐시 미스: O(height) = O(log_t n) 회

rbtree 노드 접근 패턴:
  - 노드 크기 ~40바이트 = 캐시라인 1개
  - 한 번의 노드 접근으로 1개 키 비교
  - 캐시 미스: O(log_2 n) 회 (각각 다른 캐시라인)

예시 n=10,000:
  B-tree (geo32): height ≤ 4 --> 최대 4회 캐시 미스
  rbtree: 깊이 ≤ 27 (2 * log_2(10001)) --> 최대 27회 캐시 미스
벤치마크 주의: 실제 성능은 마이크로벤치마크와 크게 다를 수 있습니다. rbtree의 노드가 연속 할당되면 prefetch가 효과적이고, B-tree의 128바이트 노드는 메모리 대역폭(Bandwidth)을 더 많이 사용합니다. 실제 워크로드에서 측정하는 것이 중요합니다.

메모리 오버헤드 분석

자료구조의 메모리 효율은 키-값 쌍당 소비하는 바이트 수로 비교할 수 있습니다:

자료구조노드당 쌍노드 크기쌍당 바이트메모리 효율
btree geo328128B~16B가장 효율적
btree geo1285128B~26B보통
rbtree1~40B~40B노드 오버헤드 큼
Maple Tree (dense)~16256B~16B밀집 데이터에 효율적
메모리 예산 계산: 10,000개 키를 저장할 때, btree geo32는 약 10,000/8 = 1,250개 노드 x 128B = 160KB, rbtree는 10,000 x 40B = 400KB입니다. B-tree가 약 60% 적은 메모리를 사용합니다. 단, 트리가 희소해지면(삭제가 많으면) B-tree의 이점이 줄어듭니다.

삽입 분할 비용 분석

분할(split)은 B-tree 삽입의 주요 비용입니다. 분할 발생 확률과 비용을 분석합니다:

항목geo32 (no_pairs=8)geo128 (no_pairs=5)
분할당 복사 바이트4 쌍 x 16B = 64B2~3 쌍 x 24B = 48~72B
분할당 mempool 할당1회 (128B)1회 (128B)
분할 전파 확률약 1/8 (부모도 가득 찬 경우)약 1/5
루트 분할 확률약 1/8^h약 1/5^h

무작위 삽입 시 분할 확률은 1/no_pairs에 근사합니다. geo32에서 100번 삽입하면 평균 12~13회 분할이 발생하며, 루트 분할(트리 높이 증가)은 매우 드뭅니다.

삭제 비용 분석

커널 B-tree의 삭제는 교과서 B-tree보다 단순하지만, 트리가 희소해질 수 있습니다:

교과서 B-tree 삭제: merge/rotate 비용 O(t) + 전파 O(log_t n)
커널 B-tree 삭제:   배열 shift O(no_pairs) + shrink O(1)

절충점:
- 교과서: 노드 utilization >= 50% 보장 → 메모리 효율적
- 커널:   노드 utilization 보장 없음 → 구현 단순, 약간 희소

확장성 (Scalability)

트리 크기별 예상 높이와 노드 접근 횟수입니다 (geo32, 64비트 시스템 기준):

키 수 (n)B-tree 높이최대 노드 접근rbtree 최대 깊이
1002214
1,0003320
10,0004427
100,0005534
1,000,0006640

디버깅

B-tree 관련 버그는 대부분 잘못된 API 사용, 메모리 누수, 동시성 문제에서 발생합니다. 이 섹션에서는 일반적인 오류 패턴과 체계적인 진단 절차를 다룹니다.

디버깅 원칙: B-tree 관련 문제의 80%는 (1) 초기화/해체 누락, (2) 동시성 미보호, (3) 키 값 0 사용 중 하나입니다. 복잡한 알고리즘 버그보다 API 사용 오류가 훨씬 많으므로, 기본부터 점검하세요.

일반적 오류 패턴

오류증상해결
btree_init() 누락NULL 포인터 역참조(Dereference)사용 전 반드시 초기화
btree_destroy() 누락메모리 누수 (mempool + 노드)모듈 제거 시 반드시 해체
insert 반환값 무시-ENOMEM 무시 시 데이터 손실반환값 확인 필수
동시 접근 무보호트리 구조 손상외부 락 (spinlock/mutex) 사용
grim_visitor 후 사용해제된 메모리 접근grim_visitor 후 tree 사용 금지
visitor 내 remove 호출순회 중 구조 변경 → 크래시visitor 내 수정 금지

트리 무결성(Integrity) 검증

/* 디버그용: 트리 내용 덤프 */
static void btree_dump_entry(void *elem, unsigned long opaque,
                             unsigned long *key, size_t index,
                             void *__func)
{
    pr_debug("btree[%zu]: key=%lx val=%px\n",
             index, key[0], elem);

    /* 값 포인터 유효성 검사 */
    WARN_ON(!virt_addr_valid(elem));
}

/* 전체 트리 덤프 */
size_t n = btree_visitor(&my_tree, &btree_geo32, 0,
                        btree_dump_entry);
pr_debug("btree: %zu entries, height=%d\n",
         n, my_tree.height);
B-tree 디버깅 진단 흐름 B-tree 이상 감지 NULL 역참조 / OOPS 1. btree_init() 호출 확인 2. head->node != NULL 확인 kmemleak 경고 1. btree_destroy() 호출 확인 2. grim_visitor로 값 해제 확인 데이터 불일치 / 검색 실패 1. 동시 접근 락 확인 2. visitor 내 수정 여부 확인 진단 도구 btree_visitor() -- 트리 내용 덤프, 항목 수 확인 kmemleak -- 해제되지 않은 노드 탐지 (CONFIG_DEBUG_KMEMLEAK) KASAN -- use-after-free 탐지 (CONFIG_KASAN), WARN_ON -- 값 포인터 유효성
증상별 진단 경로: NULL 역참조, 메모리 누수, 데이터 불일치 각각의 접근법

ftrace를 이용한 B-tree 추적

# btree 관련 함수 추적 활성화
echo 'btree_*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# ... 작업 수행 ...

# 트레이스 확인
cat /sys/kernel/debug/tracing/trace
# 출력 예시:
# my_module-1234  [002]  123.456789: btree_lookup <- my_lookup_func
# my_module-1234  [002]  123.456790: btree_insert <- my_insert_func

# 함수 그래프 트레이서로 호출 깊이 확인
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# btree_insert -> btree_insert_level -> btree_grow -> btree_node_alloc 경로 확인

kmemleak으로 B-tree 메모리 누수 추적

# kmemleak 활성화 (CONFIG_DEBUG_KMEMLEAK=y)
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

# 누수된 B-tree 노드 예시 출력:
# unreferenced object 0xffff888012345000 (size 128):
#   comm "my_module", pid 1234
#   backtrace:
#     btree_node_alloc+0x1c/0x40
#     btree_insert_level+0xab/0x200
#     btree_insert+0x56/0x80
#     my_insert_func+0x30/0x60

디버깅 관련 커널 설정

CONFIG 옵션용도
CONFIG_DEBUG_KMEMLEAK해제되지 않은 mempool 노드 탐지
CONFIG_KASANuse-after-free, 버퍼 오버플로(Buffer Overflow) 탐지
CONFIG_LOCKDEPB-tree 접근 시 락 누락 탐지
CONFIG_DEBUG_SLAB128바이트 노드의 slab 오염 탐지

동시성 보호 패턴

커널 B-tree는 내부적으로 동시성 보호를 제공하지 않습니다. 호출자가 외부 락으로 보호해야 합니다:

/* 패턴 1: spinlock 보호 (짧은 연산) */
static DEFINE_SPINLOCK(tree_lock);

void my_insert(u32 key, void *val)
{
    spin_lock(&tree_lock);
    btree_insert32(&my_tree, key, val, GFP_ATOMIC);
    spin_unlock(&tree_lock);
}

/* 패턴 2: mutex 보호 (GFP_KERNEL 사용 가능) */
static DEFINE_MUTEX(tree_mutex);

void my_insert_sleepable(u32 key, void *val)
{
    mutex_lock(&tree_mutex);
    btree_insert32(&my_tree, key, val, GFP_KERNEL);
    mutex_unlock(&tree_mutex);
}
RCU 미지원: lib/btree.c는 RCU와 호환되지 않습니다. 삽입/삭제 시 노드 내용이 직접 수정되므로, 동시 읽기가 부분적으로 수정된 노드를 볼 수 있습니다. RCU 기반 읽기-쓰기 분리가 필요하면 rbtree의 RCU 변형을 사용하세요.

lockdep 어노테이션 패턴

B-tree를 보호하는 락에 대해 lockdep 어노테이션을 추가하면 잠금(Lock) 순서 위반을 자동 탐지할 수 있습니다:

/* lockdep을 위한 올바른 패턴 */
static DEFINE_MUTEX(tree_A_mutex);
static DEFINE_MUTEX(tree_B_mutex);

/* 항상 A -> B 순서로 잠금 (역순 금지) */
void transfer(u32 key)
{
    void *val;

    mutex_lock(&tree_A_mutex);     /* 항상 A 먼저 */
    mutex_lock(&tree_B_mutex);     /* 그 다음 B */

    val = btree_remove32(&tree_A, key);
    if (val)
        btree_insert32(&tree_B, key, val, GFP_KERNEL);

    mutex_unlock(&tree_B_mutex);
    mutex_unlock(&tree_A_mutex);
}

/* lockdep이 A->B 의존성을 기록하고,
 * B->A 순서 시 경고를 출력함 */

흔한 실수 코드 vs 올바른 코드

/* [잘못된 코드] 키 값 0 사용 */
btree_insert32(&tree, 0, my_data, GFP_KERNEL);
/* --> 키 0은 빈 슬롯으로 인식되어 검색 불가! */

/* [올바른 코드] 키 값 0 회피 */
btree_insert32(&tree, key + 1, my_data, GFP_KERNEL);
/* 또는 0이 아닌 매핑 함수 사용 */
/* [잘못된 코드] insert 반환값 무시 */
btree_insert32(&tree, key, val, GFP_KERNEL);
/* --> -ENOMEM 시 데이터 손실 가능 */

/* [올바른 코드] 반환값 확인 */
ret = btree_insert32(&tree, key, val, GFP_KERNEL);
if (ret) {
    pr_err("btree insert failed: %d\n", ret);
    kfree(val);
    return ret;
}
/* [잘못된 코드] destroy 없이 해제 */
btree_grim_visitor32(&tree, free_cb);
/* --> mempool이 해제되지 않아 메모리 누수! */

/* [올바른 코드] grim_visitor + destroy */
btree_grim_visitor32(&tree, free_cb);
btree_destroy32(&tree);  /* mempool 해제 */

안전한 정리 패턴

/* 모듈 제거 시 안전한 정리 순서 */
static void my_exit(void)
{
    /* 1. 새로운 삽입 차단 (플래그 또는 락) */
    spin_lock(&my_lock);
    shutting_down = true;
    spin_unlock(&my_lock);

    /* 2. 모든 값 객체 해제 + 노드 해제 */
    btree_grim_visitor(&my_tree, &btree_geo32, 0, free_entry);

    /* 3. mempool 파괴 (잔여 예약 객체 해제) */
    btree_destroy(&my_tree);
}

커널 설정 옵션

lib/btree.cCONFIG_BTREE 옵션으로 제어됩니다:

# lib/Kconfig
config BTREE
    bool
    help
      B-tree library, useful for indexing/searching in kernel space.
      This is selected by drivers/modules that need it.

CONFIG_BTREE는 직접 선택하지 않고, btree를 사용하는 드라이버/모듈이 select BTREE로 간접 활성화합니다. 대부분의 배포판 커널에서는 기본적으로 포함됩니다.

모듈에서 B-tree 사용 체크리스트

#항목확인
1Kconfig에 select BTREE 추가depends on BTREE 또는 select BTREE
2#include <linux/btree.h>기본 API
3적절한 geo 헤더 포함geo128이면 btree-128.h 추가
4btree_init() 호출모듈 초기화 시
5외부 락 보호spinlock 또는 mutex
6insert 반환값 확인-ENOMEM 처리
7키 값 0 미사용빈 슬롯으로 인식됨
8btree_grim_visitor() + btree_destroy()모듈 해제 시

B-트리 변형: B+트리, B*트리

커널의 lib/btree.c는 표준 B-tree이지만, 파일시스템에서는 B+트리와 B*트리 변형이 더 널리 사용됩니다. 각 변형의 구조적 차이와 커널에서의 활용을 비교합니다.

특성B-tree (lib/btree)B+트리 (Btrfs, XFS)B*트리
데이터 위치모든 노드에 키-값 쌍리프(Leaf) 노드에만 데이터B-tree와 동일
내부 노드키 + 값 + 자식 포인터키 + 자식 포인터만키 + 값 + 자식 포인터
리프 연결없음리프 간 연결 리스트(Linked List)없음
범위 검색트리 순회 필요리프 체인으로 O(k)트리 순회 필요
내부 노드 팬아웃중간높음 (데이터 없으므로)2/3 이상 충전
분할 정책1/2 충전 시 분할1/2 충전 시 분할2/3 충전 시 분할
공간 효율최소 50%최소 50%최소 66%
커널 사용처lib/btree.c (인메모리)Btrfs, XFS (온디스크)

lib/btree.c 구현 특성

커널의 인메모리 B-tree는 디스크 B-tree와 여러 면에서 다릅니다:

Btrfs B+트리 구조

/* Btrfs B+트리 온디스크 구조 (fs/btrfs/ctree.h) */
struct btrfs_header {
    u8  csum[32];        /* 노드 체크섬 */
    u8  fsid[16];        /* 파일시스템 UUID */
    __le64 bytenr;         /* 디스크 오프셋 */
    __le64 flags;
    __le64 generation;     /* COW 트랜잭션 세대 */
    __le64 owner;          /* 소유 트리 ID */
    __le32 nritems;        /* 아이템 수 */
    u8  level;             /* 0 = 리프 */
};

/* 내부 노드: 키 + 자식 블록 포인터 */
struct btrfs_key_ptr {
    struct btrfs_disk_key key;
    __le64 blockptr;       /* 자식 노드 디스크 주소 */
    __le64 generation;     /* 자식 세대 (무결성 검증) */
};

/* 리프 노드: 키 + 아이템 데이터 (인라인) */
struct btrfs_item {
    struct btrfs_disk_key key;
    __le32 offset;         /* 리프 내 데이터 오프셋 */
    __le32 size;           /* 데이터 크기 */
};

XFS B+트리 활용

XFS는 여러 용도에 각각 독립적인 B+트리를 사용합니다:

B+트리용도소스 위치
Inode B+treeinode 번호inode 할당/해제 추적fs/xfs/libxfs/xfs_ialloc_btree.c
Alloc B+tree블록 번호/크기프리 스페이스 관리fs/xfs/libxfs/xfs_alloc_btree.c
BMap B+tree파일 오프셋extent 매핑(Mapping)fs/xfs/libxfs/xfs_bmap_btree.c
Refcount B+tree블록 번호reflink 참조 카운트fs/xfs/libxfs/xfs_refcount_btree.c
Rmap B+tree블록 번호역매핑(Reverse Map)fs/xfs/libxfs/xfs_rmap_btree.c

ext4 Extent 트리

ext4의 extent 트리는 완전한 B+트리는 아니지만, B-tree 변형의 트리 구조를 사용합니다:

/* ext4 extent 트리 구조 (fs/ext4/ext4_extents.h) */
struct ext4_extent_header {
    __le16 eh_magic;       /* 0xF30A */
    __le16 eh_entries;     /* 유효 엔트리 수 */
    __le16 eh_max;         /* 최대 엔트리 수 */
    __le16 eh_depth;       /* 0 = 리프 */
    __le32 eh_generation;
};

/* 리프: 논리 블록 → 물리 블록 매핑 */
struct ext4_extent {
    __le32 ee_block;       /* 논리 블록 시작 */
    __le16 ee_len;         /* extent 길이 */
    __le16 ee_start_hi;    /* 물리 블록 상위 16비트 */
    __le32 ee_start_lo;    /* 물리 블록 하위 32비트 */
};

/* 내부 노드: 인덱스 엔트리 */
struct ext4_extent_idx {
    __le32 ei_block;       /* 논리 블록 범위 시작 */
    __le32 ei_leaf_lo;     /* 자식 블록 하위 32비트 */
    __le16 ei_leaf_hi;     /* 자식 블록 상위 16비트 */
    __le16 ei_unused;
};

커널 btree API 사용 가이드

이 섹션은 lib/btree.c의 타입별 래퍼 API를 중심으로, 실전 모듈 개발에 필요한 패턴을 정리합니다. 모든 타입별 래퍼는 btree-type.h의 매크로로 생성되며, 내부적으로 동일한 범용 함수를 호출합니다.

초기화와 해체

/* 초기화: mempool 생성 + 빈 트리 설정 */
struct btree_head32 my_tree;
int ret;

ret = btree_init32(&my_tree);
if (ret) {
    pr_err("btree init failed: %d\n", ret);
    return ret;
}

/* 해체: 모든 노드 + mempool 해제 */
/* 주의: 값 객체는 btree_destroy가 해제하지 않음! */
btree_grim_visitor32(&my_tree, my_free_callback);
btree_destroy32(&my_tree);

CRUD 연산

/* Insert: 키가 이미 존재하면 -EEXIST가 아니라 값이 교체됨 */
ret = btree_insert32(&my_tree, key, value, GFP_KERNEL);
if (ret == -ENOMEM) {
    pr_err("OOM during btree insert\n");
}

/* Lookup: 키가 없으면 NULL 반환 */
void *val = btree_lookup32(&my_tree, key);
if (!val)
    pr_debug("key %u not found\n", key);

/* Remove: 키가 없으면 NULL 반환, 있으면 제거하고 값 반환 */
void *removed = btree_remove32(&my_tree, key);
if (removed)
    kfree(removed);   /* 값 객체 해제는 호출자 책임 */

/* Update (lib/btree에는 별도 update가 없으므로 remove+insert) */
old_val = btree_remove32(&my_tree, key);
kfree(old_val);
btree_insert32(&my_tree, key, new_val, GFP_KERNEL);

/* Last: 가장 큰 키와 값 조회 */
void *last_val = btree_last32(&my_tree, &last_key);

/* Get Prev: 주어진 키보다 작은 가장 큰 키 조회 */
void *prev_val = btree_get_prev32(&my_tree, &prev_key);

GFP 플래그 선택 가이드

컨텍스트GFP 플래그슬립(Sleep) 가능보호 락
프로세스 컨텍스트GFP_KERNELOmutex
인터럽트 컨텍스트GFP_ATOMICXspinlock + irqsave
소프트IRQ / BHGFP_ATOMICXspin_lock_bh
메모리 회수 경로GFP_NOFS / GFP_NOIOOmutex
사전 할당 보장GFP_KERNEL + mempoolOmutex
mempool 보장: btree_init()은 내부적으로 mempool_create(0, ...)를 호출합니다. 최소 예약 개수가 0이므로, 메모리 압박 시 GFP_ATOMIC 삽입이 실패할 수 있습니다. 고신뢰 경로에서는 GFP_KERNEL을 사용하거나 사전에 mempool 크기를 늘리세요.

파일시스템별 B-tree/B+트리 활용 비교

파일시스템별 B-tree 변형 비교 Btrfs (COW B+트리) 노드 크기: 16KB (기본) COW: 수정 시 새 노드 할당 복합 키: (objectid, type, offset) 트리: fs-tree, extent-tree, chunk-tree 등 체크섬: 각 노드에 CRC32C/SHA256 XFS (다중 B+트리) 노드 크기: 블록 크기 (4KB 기본) 저널링(Journaling): WAL 기반 AG별 독립 B+트리 (병렬 할당) 5종 B+트리: inode/alloc/bmap/rmap/refcnt 버전 5: 셀프 검증 메타데이터 ext4 (Extent 트리) 노드 크기: 블록 크기 (4KB) 최대 깊이: 5 레벨 루트: inode 내 인라인 (60B) 리프: extent (논리→물리 매핑) 단순 구조, 작은 파일 최적화 lib/btree.c (인메모리 B-tree) 노드 크기: 128B (L1 캐시 정렬) 키 타입: geo32/64/128 메모리: mempool 기반 동적 할당 용도: 범용 인메모리 키-값 검색 (드라이버, 모듈) 리프 연결: 없음 (범위 검색 비효율) 동시성: 내장 없음 (호출자 보호) RCU: 미지원 장점: 트리 높이 낮음, 캐시 친화적
커널 파일시스템별 B-tree 변형 비교: 각각의 설계 목표와 구현 특성이 다름

성능 분석

lib/btree.c의 성능 특성을 이론적 분석과 실측 관점에서 상세히 다룹니다.

연산별 복잡도 상세

연산시간 복잡도노드 접근 수메모리 할당설명
lookupO(logk n)h (높이)0각 레벨에서 이진 검색(Binary Search) 후 하강
insert (분할 없음)O(logk n)h0빈 슬롯에 삽입
insert (분할 발생)O(logk n)h1~h 노드최악 시 루트까지 분할 전파
remove (병합 없음)O(logk n)h0슬롯 정리만
remove (병합 발생)O(logk n)h0 (해제만)최악 시 루트까지 병합 전파
visitorO(n)전체 노드0모든 키-값 쌍 순회
grim_visitorO(n)전체 노드0 (해제만)순회 + 노드 해체
lastO(logk n)h0각 노드의 마지막 슬롯 하강
get_prevO(logk n)h0주어진 키 이하의 최대 키

여기서 k는 노드당 키 수, h는 트리 높이입니다. geo32의 경우 k=14이므로 log14(1,000,000) ≈ 5.2, 즉 백만 개 항목도 6회 이내의 노드 접근으로 검색됩니다.

캐시(Cache) 효율 분석

# B-tree 연산의 캐시 미스 측정
perf stat -e L1-dcache-load-misses,L1-dcache-loads,\
LLC-load-misses,LLC-loads \
-a -- modprobe my_btree_test_module

# B-tree vs rbtree 비교 분석:
# B-tree: 128B 노드 = 2개 캐시 라인, 14개 키 검색 가능
#   → 1 노드 접근 = 2 캐시 미스로 14개 키 비교
# rbtree: 24B 노드, 2개 자식만
#   → 1 노드 접근 = 1 캐시 미스로 1개 키 비교
#
# 결론: 동일 키 수에서 B-tree가 캐시 미스 횟수 ~3배 적음
# 단, rbtree는 침투적이라 노드 할당 오버헤드 없음

대용량 벤치마크 (geo32 기준)

항목 수insert (ns/op)lookup (ns/op)remove (ns/op)메모리 사용
1,000~150~80~120~10 KB
10,000~200~110~170~100 KB
100,000~350~180~280~1 MB
1,000,000~600~300~450~10 MB
벤치마크 조건: 위 수치는 순차 키 삽입, 랜덤 키 검색, 랜덤 키 삭제 기준의 대략적 참고값입니다. 실제 성능은 CPU 아키텍처, 캐시 크기, 메모리 대역폭, NUMA 토폴로지에 따라 크게 달라집니다.

실전 커널 모듈 예제

B-tree API를 사용하는 완전한 커널 모듈 예제입니다. 초기화, CRUD 연산, 순회, 해체를 모두 포함합니다.

/*
 * btree_demo.c -- lib/btree API 데모 모듈
 *
 * 빌드: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
 * 로드: insmod btree_demo.ko
 * 제거: rmmod btree_demo
 * 로그: dmesg | grep btree_demo
 */

#include <linux/module.h>
#include <linux/btree.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("B-tree API demo");

struct my_data {
    u32 id;
    char name[32];
};

static struct btree_head32 my_tree;
static DEFINE_MUTEX(tree_mutex);

/* visitor 콜백: 항목 출력 */
static int print_entry(void *elem, unsigned long opaque,
                       unsigned long *key, size_t index,
                       void *__func)
{
    struct my_data *d = elem;
    pr_info("btree_demo: [%zu] key=%lu id=%u name=%s\n",
            index, key[0], d->id, d->name);
    return 0;
}

/* grim_visitor 콜백: 값 객체 해제 */
static int free_entry(void *elem, unsigned long opaque,
                      unsigned long *key, size_t index,
                      void *__func)
{
    kfree(elem);
    return 0;
}

static int __init btree_demo_init(void)
{
    struct my_data *d;
    void *found;
    int ret, i;
    size_t count;

    /* 1. 트리 초기화 */
    ret = btree_init32(&my_tree);
    if (ret)
        return ret;

    /* 2. 100개 항목 삽입 (키 1~100, 0은 사용 금지!) */
    mutex_lock(&tree_mutex);
    for (i = 1; i <= 100; i++) {
        d = kzalloc(sizeof(*d), GFP_KERNEL);
        if (!d) { ret = -ENOMEM; goto err_unlock; }
        d->id = i;
        snprintf(d->name, sizeof(d->name), "item-%03d", i);

        ret = btree_insert32(&my_tree, i, d, GFP_KERNEL);
        if (ret) {
            kfree(d);
            goto err_unlock;
        }
    }
    mutex_unlock(&tree_mutex);

    /* 3. 검색 */
    found = btree_lookup32(&my_tree, 42);
    if (found)
        pr_info("btree_demo: found key=42 → %s\n",
                ((struct my_data *)found)->name);

    /* 4. 삭제 */
    mutex_lock(&tree_mutex);
    found = btree_remove32(&my_tree, 50);
    mutex_unlock(&tree_mutex);
    if (found) {
        pr_info("btree_demo: removed key=50 → %s\n",
                ((struct my_data *)found)->name);
        kfree(found);
    }

    /* 5. 전체 순회 */
    count = btree_visitor32(&my_tree, 0, print_entry);
    pr_info("btree_demo: %zu entries total\n", count);

    return 0;

err_unlock:
    mutex_unlock(&tree_mutex);
    btree_grim_visitor32(&my_tree, free_entry);
    btree_destroy32(&my_tree);
    return ret;
}

static void __exit btree_demo_exit(void)
{
    mutex_lock(&tree_mutex);
    btree_grim_visitor32(&my_tree, free_entry);
    btree_destroy32(&my_tree);
    mutex_unlock(&tree_mutex);
    pr_info("btree_demo: cleaned up\n");
}

module_init(btree_demo_init);
module_exit(btree_demo_exit);
# Makefile
obj-m += btree_demo.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
B-tree 모듈 생명주기 (초기화 → 연산 → 해체) btree_init() btree_insert() lookup / remove 사용자 요청 처리 grim_visitor() 값 해제 + 노드 해체 btree_destroy() 핵심 주의사항 1. 키 값 0 사용 금지 — 빈 슬롯 센티넬로 예약됨 2. insert 반환값 반드시 확인 — -ENOMEM 시 데이터 손실 3. 외부 락 필수 — lib/btree에 내장 동기화 없음 4. grim_visitor + destroy 순서 — grim_visitor만으로는 mempool 미해제 5. visitor 내 트리 수정 금지 — 구조 변경 시 순회 중단/크래시
B-tree 모듈의 생명주기와 핵심 주의사항: 초기화부터 해체까지 반드시 지켜야 할 순서

메모리 할당 전략

lib/btree.c는 노드 할당에 mempool을 사용합니다. 모든 노드가 동일한 128바이트 크기이므로, slab 캐시(kmalloc-128)에서 빠르게 할당/해제됩니다.

노드 크기 결정 로직

/* lib/btree.c의 노드 크기 결정 */
#define NODESIZE   max_t(int, L1_CACHE_BYTES, 128)
/* L1 캐시 라인이 128B 이하면 128B 사용 (대부분의 아키텍처)
 * L1 캐시 라인이 128B 초과면 캐시 라인 크기 사용 (POWER9 등)
 */

/* 노드 내부 레이아웃 (geo32, NODESIZE=128) */
/*
 * keys[]:   unsigned long × 14개 = 112B (64비트) 또는 56B (32비트)
 * vals[]:   void * × 14개 (키와 인터리브)
 * child[]:  void * × 15개 (내부 노드만)
 *
 * 실제 슬롯 수 계산:
 *   no_pairs = NODESIZE / (keysize + sizeof(void *))
 *   no_longs = keysize / sizeof(unsigned long)
 */

mempool 동작 상세

상황mempool 동작결과
메모리 충분kmalloc(128)에서 직접 할당정상 할당
메모리 부족 + GFP_KERNEL예약 풀에서 할당 → 잠재적 슬립성공 보장 (예약분 내)
메모리 부족 + GFP_ATOMIC예약 풀에서 할당 → 비슬립예약 소진 시 실패 가능
예약 풀 고갈-ENOMEM 반환insert 실패
노드 해제예약 풀 미충족 시 풀에 반환, 아니면 kfree자동 풀 보충

키 타입별 슬롯 수 상세

키 타입키 크기키+값 크기 (64비트)슬롯 수자식 수최소 차수
btree_geo328B (1 × long)16B784
btree_geo648B (1 × long)16B784
btree_geo12816B (2 × long)24B563
32비트 vs 64비트: 32비트 아키텍처에서는 unsigned long이 4바이트이므로 geo32의 슬롯 수가 달라집니다. geo32는 키 4B + 포인터 4B = 8B → 128/8 = 16 슬롯이 됩니다. 64비트 아키텍처에서는 키 8B + 포인터 8B = 16B → 128/16 = 7 슬롯입니다. 32비트에서 B-tree의 팬아웃이 더 높아 동일 키 수에서 트리 높이가 낮아집니다.

참고 자료