Red-Black Tree (rbtree)

커널에서 가장 널리 사용되는 자기 균형 이진 탐색 트리. rb_node/rb_root 구조체, 삽입·삭제·순회 API, rb_root_cached, 증강 rbtree, RCU-safe rbtree, Latched rbtree, CFS·VMA·hrtimer 활용 사례를 다룹니다.

전제 조건: 동기화 기법메모리 배리어 문서를 먼저 읽으세요. 커널 자료구조는 연산 복잡도뿐 아니라 동시 접근 안전성까지 함께 설계되므로, 성능과 동기화 관점을 동시에 봐야 합니다.
일상 비유: rbtree는 균형 잡힌 토너먼트 대진표와 비슷합니다. 참가자(노드)가 추가/탈퇴해도 대진표가 자동으로 재배치되어 항상 비슷한 라운드 수(O(log n))만에 원하는 대결을 찾을 수 있습니다. 빨간/검은 색상 규칙이 대진표의 균형을 보장합니다.

핵심 요약

  • O(log n) 보장 — 검색·삽입·삭제 모두 최악의 경우에도 O(log n)입니다.
  • 삽입 최대 2회전 — AVL 대비 리밸런싱 비용이 낮아 쓰기 빈번한 커널에 적합합니다.
  • 침투적(intrusive) 설계rb_node를 사용자 구조체에 임베딩하여 별도 메모리 할당 없이 사용합니다.
  • rb_root_cached — 최솟값(rb_first)을 O(1)에 접근할 수 있는 캐시 변형입니다.
  • CFS·hrtimer·epoll 핵심 — 커널 스케줄러, 타이머, I/O 멀티플렉싱의 기반 자료구조입니다.

단계별 이해

  1. rb_node 임베딩 이해
    rb_node를 사용자 구조체에 포함시키고 rb_entry()/container_of()로 역참조하는 침투적 패턴을 파악합니다.
  2. 삽입 2단계 흐름 추적
    BST 위치 탐색(사용자 코드) → rb_link_node()rb_insert_color() 순서와 색상 조정/회전 과정을 따라갑니다.
  3. 변형 선택하기
    최솟값 빈번 접근 시 rb_root_cached, 서브트리 정보 필요 시 증강 rbtree, RCU 읽기 필요 시 latched rbtree를 선택합니다.
  4. 동시성 규칙 설계
    rbtree 자체에는 잠금이 없으므로, 외부 spinlock/rwlock/RCU 보호 전략을 반드시 결정합니다.
예제 읽기 가이드: 이 문서는 개념 설명용 의사코드를 중심으로 구성하되, 일부 섹션은 커널 모듈 기준의 컴파일 가능한 예제 형태를 함께 제공합니다. 코드 주석의 개념 예시는 구조 이해 목적, 실습 예제는 실제 빌드/실행 흐름 점검 목적입니다.
관련 표준: (자료구조 알고리즘, Cormen et al. Introduction to Algorithms 참조) 커널 내부 균형 트리 구현입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

개요

Red-Black Tree(이하 rbtree)는 Linux 커널에서 널리 사용되는 자기 균형 이진 탐색 트리입니다. CFS 스케줄러의 태스크 정렬, hrtimer 타이머 큐, epoll 파일 디스크립터 관리 등에서 핵심적으로 활용됩니다. VMA(가상 메모리 영역)는 커널 6.1 이상에서 Maple Tree가 기본이며, rbtree 기반 설명은 주로 6.0 이하/역사적 맥락에 해당합니다.

Red-Black Tree 5가지 속성

rbtree는 다음 5가지 불변 조건(invariant)을 항상 만족하는 이진 탐색 트리입니다:

  1. 모든 노드는 빨간색(Red) 또는 검은색(Black)이다.
  2. 루트 노드는 항상 검은색이다.
  3. 모든 리프(NIL) 노드는 검은색이다. (NIL은 실제 노드가 아닌 논리적 자식)
  4. 빨간 노드의 두 자식은 반드시 검은색이다. (연속된 빨간 노드 불가 — "No Red-Red" 규칙)
  5. 임의의 노드에서 모든 리프(NIL)까지의 경로에 포함된 검은 노드 수가 동일하다. (Black-Height 불변)

이 5가지 속성으로 인해 트리의 최장 경로가 최단 경로의 2배를 초과할 수 없으며, 검색·삽입·삭제 모두 O(log n)의 시간 복잡도를 보장합니다.

AVL 트리 대비 장점

커널이 AVL 대신 rbtree를 선택한 핵심 이유는 삽입/삭제 시 리밸런싱 비용입니다:

ℹ️

커널의 rbtree 구현은 lib/rbtree.c에 위치하며, 헤더는 include/linux/rbtree.hinclude/linux/rbtree_augmented.h입니다. 증강 rbtree, RCU-safe 변형, latched rbtree 등 다양한 확장을 제공합니다.

내부 구조

rb_node 구조체

/* 개념 예시: include/linux/rbtree_types.h 핵심 구조체 */
/* include/linux/rbtree_types.h */
struct rb_node {
    unsigned long  __rb_parent_color;  /* 부모 포인터 + 색상 비트 */
    struct rb_node *rb_right;           /* 오른쪽 자식 */
    struct rb_node *rb_left;            /* 왼쪽 자식 */
} __attribute__((aligned(sizeof(long))));

/* rb_root: 트리의 루트 */
struct rb_root {
    struct rb_node *rb_node;  /* 루트 노드 포인터 */
};

/* 초기화 매크로 */
#define RB_ROOT  (struct rb_root) { NULL, }

/* 빈 트리 확인 */
#define RB_EMPTY_ROOT(root)  (READ_ONCE((root)->rb_node) == NULL)
#define RB_EMPTY_NODE(node)  \
    ((node)->__rb_parent_color == (unsigned long)(node))

__rb_parent_color 비트 트릭

__rb_parent_color 필드는 커널 rbtree의 핵심 최적화입니다. rb_node 구조체가 sizeof(long)으로 정렬(align)되므로, 부모 포인터의 하위 2비트는 항상 0입니다. 커널은 이 하위 비트 중 LSB(비트 0)노드의 색상 정보로 활용합니다:

/* include/linux/rbtree_augmented.h — 내부 매크로 */
#define RB_RED    0
#define RB_BLACK  1

#define __rb_parent(pc)    ((struct rb_node *)(pc & ~3))
#define __rb_color(pc)     ((pc) & 1)
#define rb_parent(rb)      __rb_parent((rb)->__rb_parent_color)
#define rb_color(rb)       __rb_color((rb)->__rb_parent_color)
#define rb_is_red(rb)      (!__rb_color((rb)->__rb_parent_color))
#define rb_is_black(rb)    (__rb_color((rb)->__rb_parent_color))

이 기법으로 별도의 색상 필드 없이 노드 1개당 메모리를 8바이트(64비트) 절약합니다. 수만~수십만 노드가 존재하는 커널 자료구조에서 이 절약은 매우 유의미합니다.

rb_node 메모리 레이아웃

struct rb_node (24 bytes on 64-bit) __rb_parent_color (unsigned long — 8 bytes) 비트 63 ~ 비트 2: 부모 포인터 (상위 62비트) 비트 0: 색상 rb_right (struct rb_node * — 8 bytes) rb_left (struct rb_node * — 8 bytes) +0 +8 +16

핵심 API

rb_entry() / container_of()

커널의 rbtree는 침투적(intrusive) 설계입니다. rb_node를 사용자 구조체에 임베딩하고, rb_entry()(container_of()의 래퍼)로 부모 구조체를 역참조합니다. 별도의 노드 할당 없이 데이터 구조체 안에 트리 노드가 직접 포함되므로, 메모리 할당 오버헤드가 없고 캐시 지역성이 우수합니다.

/* rb_entry는 container_of의 별칭 */
#define rb_entry(ptr, type, member) \
    container_of(ptr, type, member)

/* 사용 예 */
struct my_data {
    int               key;
    int               value;
    struct rb_node    node;  /* rbtree 노드 임베딩 */
};

/* rb_node 포인터에서 my_data 포인터로 변환 */
struct rb_node *n = ...;
struct my_data *data = rb_entry(n, struct my_data, node);

rbtree 삽입은 2단계로 이루어집니다:

/* 1단계: rb_link_node() — 노드를 트리에 연결 (아직 리밸런싱 안됨) */
void rb_link_node(struct rb_node *node,
                   struct rb_node *parent,
                   struct rb_node **rb_link);

/* 2단계: rb_insert_color() — 색상 조정 + 회전으로 5가지 속성 복원 */
void rb_insert_color(struct rb_node *node,
                      struct rb_root *root);
💡

rbtree는 비교 함수를 프레임워크에 전달하지 않습니다. 삽입 위치 탐색(BST 탐색)은 사용자 코드가 직접 수행하며, 위치를 찾은 뒤 rb_link_node() + rb_insert_color()를 호출합니다. 이 설계로 인해 비교 로직의 인라인화가 가능하여 함수 포인터 간접 호출 오버헤드를 제거합니다.

삭제: rb_erase()

/* 노드를 트리에서 제거하고 리밸런싱 수행 */
void rb_erase(struct rb_node *node, struct rb_root *root);

/* 노드 교체 (위치를 그대로 유지하며 노드만 교체) */
void rb_replace_node(struct rb_node *victim,
                      struct rb_node *new_node,
                      struct rb_root *root);

순회 함수

/* 최솟값 / 최댓값 (in-order 순서) */
struct rb_node *rb_first(const struct rb_root *root);  /* 최좌 노드 */
struct rb_node *rb_last(const struct rb_root *root);   /* 최우 노드 */

/* 전후 순회 */
struct rb_node *rb_next(const struct rb_node *node);  /* in-order 후계자 */
struct rb_node *rb_prev(const struct rb_node *node);  /* in-order 전임자 */

/* post-order 순회 (삭제 시 안전한 순회) */
struct rb_node *rb_first_postorder(const struct rb_root *root);
struct rb_node *rb_next_postorder(const struct rb_node *node);

/* 안전한 순회 매크로 (순회 중 삭제 가능) */
#define rbtree_postorder_for_each_entry_safe(pos, n, root, field) \
    for (pos = rb_entry_safe(rb_first_postorder(root), \
                             typeof(*pos), field); \
         pos && ({ n = rb_entry_safe( \
              rb_next_postorder(&pos->field), \
              typeof(*pos), field); 1; }); \
         pos = n)

전체 코드 예제

커널 모듈 형태의 완전한 rbtree 사용 예제입니다. 삽입, 검색, 순회, 삭제, 안전한 삭제 순회를 모두 포함합니다.

/* 실습 예제: 커널 모듈에서 rbtree 삽입/검색/삭제 */
#include <linux/module.h>
#include <linux/rbtree.h>
#include <linux/slab.h>

struct my_node {
    int               key;
    int               value;
    struct rb_node    rb;
};

static struct rb_root my_tree = RB_ROOT;

/* ---- 삽입 ---- */
static int my_insert(struct rb_root *root, struct my_node *new_node)
{
    struct rb_node **link = &root->rb_node;
    struct rb_node *parent = NULL;
    int key = new_node->key;

    /* BST 탐색: 삽입 위치 찾기 */
    while (*link) {
        struct my_node *entry = rb_entry(*link, struct my_node, rb);

        parent = *link;
        if (key < entry->key)
            link = &(*link)->rb_left;
        else if (key > entry->key)
            link = &(*link)->rb_right;
        else
            return -EEXIST;  /* 중복 키 */
    }

    /* 1단계: 노드 연결 */
    rb_link_node(&new_node->rb, parent, link);
    /* 2단계: 리밸런싱 */
    rb_insert_color(&new_node->rb, root);
    return 0;
}

/* ---- 검색 ---- */
static struct my_node *my_search(struct rb_root *root, int key)
{
    struct rb_node *n = root->rb_node;

    while (n) {
        struct my_node *entry = rb_entry(n, struct my_node, rb);

        if (key < entry->key)
            n = n->rb_left;
        else if (key > entry->key)
            n = n->rb_right;
        else
            return entry;  /* 찾음 */
    }
    return NULL;
}

/* ---- 삭제 ---- */
static void my_erase(struct rb_root *root, struct my_node *node)
{
    rb_erase(&node->rb, root);
    kfree(node);
}

/* ---- in-order 순회 ---- */
static void my_traverse(struct rb_root *root)
{
    struct rb_node *n;

    for (n = rb_first(root); n; n = rb_next(n)) {
        struct my_node *entry = rb_entry(n, struct my_node, rb);
        pr_info("key=%d value=%d\\n", entry->key, entry->value);
    }
}

/* ---- 전체 트리 삭제 (post-order safe) ---- */
static void my_destroy_tree(struct rb_root *root)
{
    struct my_node *pos, *n;

    rbtree_postorder_for_each_entry_safe(pos, n, root, rb) {
        kfree(pos);
    }
    root->rb_node = NULL;
}

/* ---- 모듈 init ---- */
static int __init rbtree_demo_init(void)
{
    int keys[] = { 50, 30, 70, 20, 40, 60, 80 };
    int i;

    for (i = 0; i < ARRAY_SIZE(keys); i++) {
        struct my_node *node = kmalloc(sizeof(*node), GFP_KERNEL);
        if (!node)
            return -ENOMEM;
        node->key = keys[i];
        node->value = keys[i] * 10;
        my_insert(&my_tree, node);
    }

    pr_info("=== In-order traversal ===\\n");
    my_traverse(&my_tree);

    /* 검색 테스트 */
    struct my_node *found = my_search(&my_tree, 40);
    if (found)
        pr_info("Found: key=%d value=%d\\n", found->key, found->value);

    /* 삭제 테스트 */
    found = my_search(&my_tree, 30);
    if (found)
        my_erase(&my_tree, found);

    pr_info("=== After delete 30 ===\\n");
    my_traverse(&my_tree);

    return 0;
}

/* ---- 모듈 exit ---- */
static void __exit rbtree_demo_exit(void)
{
    my_destroy_tree(&my_tree);
    pr_info("rbtree demo unloaded\\n");
}

module_init(rbtree_demo_init);
module_exit(rbtree_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Red-Black Tree demo module");
순회 중 삭제 주의:

rb_first()/rb_next()를 사용한 in-order 순회 중에는 노드를 삭제하면 안 됩니다. 순회 중 삭제가 필요한 경우 반드시 rbtree_postorder_for_each_entry_safe()를 사용하세요. 이 매크로는 post-order 순회이므로 자식 노드가 먼저 처리되어 삭제 후에도 순회가 안전합니다.

rb_root_cached

rb_root_cached는 rbtree의 최좌(최솟값) 노드를 캐싱하여 O(1) 접근을 제공하는 확장 구조체입니다.

/* include/linux/rbtree_types.h */
struct rb_root_cached {
    struct rb_root  rb_root;       /* 기본 rbtree root */
    struct rb_node *rb_leftmost;   /* 최좌(최솟값) 노드 캐시 */
};

#define RB_ROOT_CACHED  (struct rb_root_cached) { {NULL}, NULL }

/* O(1) 최솟값 접근 */
struct rb_node *rb_first_cached(const struct rb_root_cached *root);

/* cached 전용 삽입 — leftmost 자동 갱신 */
void rb_insert_color_cached(struct rb_node *node,
                            struct rb_root_cached *root,
                            bool leftmost);

/* cached 전용 삭제 — leftmost 자동 갱신 */
void rb_erase_cached(struct rb_node *node,
                      struct rb_root_cached *root);

사용 패턴

static struct rb_root_cached my_cached_tree = RB_ROOT_CACHED;

static void insert_cached(struct my_node *new_node)
{
    struct rb_node **link = &my_cached_tree.rb_root.rb_node;
    struct rb_node *parent = NULL;
    bool leftmost = true;  /* 새 노드가 최좌인지 추적 */

    while (*link) {
        struct my_node *entry = rb_entry(*link, struct my_node, rb);
        parent = *link;
        if (new_node->key < entry->key) {
            link = &(*link)->rb_left;
        } else {
            link = &(*link)->rb_right;
            leftmost = false;  /* 오른쪽으로 이동했으므로 최좌 아님 */
        }
    }
    rb_link_node(&new_node->rb, parent, link);
    rb_insert_color_cached(&new_node->rb, &my_cached_tree, leftmost);
}

CFS 스케줄러의 활용

CFS(Completely Fair Scheduler)는 rb_root_cached를 사용하여 태스크를 vruntime(가상 실행 시간) 기준으로 정렬합니다. rb_first_cached()로 vruntime이 가장 작은(가장 적게 실행된) 태스크를 O(1)에 선택합니다.

/* kernel/sched/sched.h */
struct cfs_rq {
    struct load_weight    load;
    unsigned int          nr_running;
    u64                   min_vruntime;
    struct rb_root_cached tasks_timeline;  /* vruntime 기준 rbtree */
    /* ... */
};

/* kernel/sched/fair.c — 다음 실행할 태스크 선택 */
static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}
ℹ️

rb_root_cachedrb_root에 포인터 1개만 추가한 것이므로 메모리 오버헤드가 극히 작습니다. 최솟값을 자주 조회하는 경우(스케줄러, 타이머 등) 우선적으로 검토할 가치가 큽니다.

증강 rbtree (Augmented RB Tree)

증강(Augmented) rbtree는 각 노드에 서브트리 정보를 캐싱하여 트리 회전 시 이를 갱신하는 메커니즘입니다. 대표적인 활용이 Interval Tree입니다.

rb_augment_callbacks

/* include/linux/rbtree_augmented.h */
struct rb_augment_callbacks {
    /* 노드에서 루트까지 서브트리 정보 전파 */
    void (*propagate)(struct rb_node *node, struct rb_node *stop);
    /* 노드 간 서브트리 정보 복사 */
    void (*copy)(struct rb_node *old, struct rb_node *new_node);
    /* 회전 후 서브트리 정보 갱신 */
    void (*rotate)(struct rb_node *old, struct rb_node *new_node);
};

/* 증강 삽입: 리밸런싱 시 콜백 호출 */
void rb_insert_augmented(struct rb_node *node,
                         struct rb_root *root,
                         const struct rb_augment_callbacks *augment);

/* 증강 삭제: 리밸런싱 시 콜백 호출 */
void rb_erase_augmented(struct rb_node *node,
                        struct rb_root *root,
                        const struct rb_augment_callbacks *augment);

Interval Tree

Interval Tree는 증강 rbtree의 가장 대표적인 활용입니다. 각 노드가 구간 [start, last]을 나타내며, 서브트리의 최대 끝점(subtree_last)을 증강 정보로 유지합니다. 이를 통해 겹치는 구간을 O(k log n)에 검색합니다. (k = 결과 수) 자세한 내용은 Interval Tree 문서를 참고하세요.

/* include/linux/interval_tree_generic.h */
/* INTERVAL_TREE_DEFINE 매크로로 타입별 interval tree 생성 */

/* include/linux/interval_tree.h — 기본 interval_tree_node */
struct interval_tree_node {
    struct rb_node  rb;
    unsigned long   start;          /* 구간 시작 */
    unsigned long   last;           /* 구간 끝 */
    unsigned long   __subtree_last; /* 서브트리 최대 끝점 (증강) */
};

/* API */
void interval_tree_insert(struct interval_tree_node *node,
                          struct rb_root_cached *root);
void interval_tree_remove(struct interval_tree_node *node,
                          struct rb_root_cached *root);
/* [start, last] 구간과 겹치는 첫 번째 노드 검색 */
struct interval_tree_node *
interval_tree_iter_first(struct rb_root_cached *root,
                         unsigned long start, unsigned long last);
/* 다음 겹치는 노드 */
struct interval_tree_node *
interval_tree_iter_next(struct interval_tree_node *node,
                        unsigned long start, unsigned long last);

VMA/Interval Tree (버전 조건 병기)

커널 6.1+에서는 VMA의 주 인덱스 자료구조가 Maple Tree입니다. 아래 rbtree/interval tree 패턴은 커널 6.0 이하 및 역사적 구현을 이해하기 위한 예시이며, 현재도 파일 매핑 reverse-map 계열 경로에서는 interval tree 개념이 사용됩니다.

/* 개념 예시: 6.0 이하 vm_area_struct의 rbtree/interval tree 패턴 */
/* include/linux/mm_types.h */
struct vm_area_struct {
    unsigned long  vm_start;     /* 시작 가상 주소 */
    unsigned long  vm_end;       /* 끝 가상 주소 */
    struct {
        struct rb_node  rb;
        unsigned long   rb_subtree_last;
    } shared;                      /* interval tree (address_space) */
    /* ... */
};

/* mm/interval_tree.c — VMA interval tree 검색 */
/* 파일의 특정 페이지 오프셋 범위를 매핑한 모든 VMA를 찾음 */
/* → reverse mapping (rmap)에서 사용 */

RCU-safe rbtree

읽기가 빈번하고 쓰기가 드문 패턴에서, RCU 보호 하에 rbtree를 lock-free로 읽기할 수 있습니다.

/* include/linux/rbtree.h */
static inline void rb_link_node_rcu(struct rb_node *node,
                                      struct rb_node *parent,
                                      struct rb_node **rb_link)
{
    node->__rb_parent_color = (unsigned long)parent;
    node->rb_left = node->rb_right = NULL;

    /* rcu_assign_pointer로 새 노드 발행 */
    rcu_assign_pointer(*rb_link, node);
}

/* 사용 패턴: 쓰기 측 (잠금 보유) */
spin_lock(&tree_lock);
rb_link_node_rcu(&new->rb, parent, link);
rb_insert_color(&new->rb, &my_root);
spin_unlock(&tree_lock);

/* 읽기 측 (RCU 보호) */
rcu_read_lock();
struct rb_node *n = rcu_dereference(my_root.rb_node);
while (n) {
    struct my_node *entry = rb_entry(n, struct my_node, rb);
    if (key < entry->key)
        n = rcu_dereference(n->rb_left);
    else if (key > entry->key)
        n = rcu_dereference(n->rb_right);
    else
        break;
}
rcu_read_unlock();
RCU rbtree 한계:

rbtree 리밸런싱(회전)은 여러 포인터를 원자적으로 변경해야 하므로 쓰기 측은 여전히 잠금이 필요합니다. rb_link_node_rcu()는 새 노드의 삽입 발행만 RCU-safe하며, 전체 삽입 과정(rb_insert_color() 포함)은 잠금 하에 수행해야 합니다. 삭제도 마찬가지로 잠금이 필수입니다.

Latched rbtree

Latched rbtree는 이중 rbtree를 유지하여 완전한 lock-free 읽기를 달성합니다. 쓰기 시 두 트리를 순차적으로 갱신하고, 읽기 측은 seqcount_latch를 이용해 일관된 트리를 선택합니다.

/* include/linux/rbtree_latch.h */
struct latch_tree_node {
    struct rb_node node[2];  /* 이중 rbtree 노드 */
};

struct latch_tree_root {
    seqcount_latch_t  seq;
    struct rb_root     tree[2];  /* 이중 rbtree */
};

/* 콜백 구조체: 비교/검색 함수 정의 */
struct latch_tree_ops {
    bool (*less)(struct latch_tree_node *a,
                 struct latch_tree_node *b);
    int  (*comp)(void *key,
                 struct latch_tree_node *b);
};

/* API */
void latch_tree_insert(struct latch_tree_node *node,
                       struct latch_tree_root *root,
                       const struct latch_tree_ops *ops);

void latch_tree_erase(struct latch_tree_node *node,
                      struct latch_tree_root *root,
                      const struct latch_tree_ops *ops);

/* lock-free 검색 */
struct latch_tree_node *
latch_tree_find(void *key,
                struct latch_tree_root *root,
                const struct latch_tree_ops *ops);

모듈 주소 검색 사례

커널 모듈의 코어/init 주소 범위를 관리하는 module_tree는 latched rbtree를 사용합니다. 모듈 로드/언로드(쓰기)는 드물고, 스택 트레이스 등에서의 주소-모듈 매핑(읽기)은 매우 빈번하기 때문입니다.

/* kernel/module/tree_lookup.c */
static struct latch_tree_root mod_tree __cacheline_aligned;

/* 모듈 주소 검색 — NMI/인터럽트 컨텍스트에서도 안전 */
struct module *__module_address(unsigned long addr)
{
    struct latch_tree_node *ltn;
    ltn = latch_tree_find((void *)addr, &mod_tree, &mod_tree_ops);
    if (!ltn)
        return NULL;
    return container_of(ltn, struct mod_tree_node, node)->mod;
}
💡

Latched rbtree는 메모리를 2배 사용하지만, 읽기 측에서 잠금이 전혀 필요 없으므로 NMI 컨텍스트에서도 안전합니다. 쓰기가 극히 드물고 읽기 성능이 중요한 경우에 적합합니다.

실제 커널 활용 사례

서브시스템rbtree 변형키(Key)용도소스 위치
CFS 스케줄러rb_root_cachedvruntime태스크를 가상 실행 시간 순으로 정렬. rb_first_cached()로 O(1) 다음 태스크 선택kernel/sched/fair.c
VMA6.1+: Maple Tree / 6.0 이하: Interval Tree(증강)vm_start / vm_endVMA 관리는 6.1+에서 Maple Tree가 기본, interval tree는 역사적/특정 경로 중심mm/mmap.c, mm/interval_tree.c
hrtimerrb_root_cached만료 시간 (ktime)고해상도 타이머 관리. 최소 만료 시간 O(1) 조회kernel/time/hrtimer.c
epollrb_root_cached파일 디스크립터모니터링 대상 fd 관리. 등록/해제/검색fs/eventpoll.c
ext4 extentrb_root논리 블록 번호extent status tree: 디스크 블록 매핑 캐시fs/ext4/extents_status.c
I/O 스케줄러rb_root섹터 번호mq-deadline: 요청을 섹터 순서로 정렬 (병합 최적화)block/mq-deadline.c
perf eventsrb_root_cached주소/시간소프트웨어 이벤트 관리, 와치포인트 관리kernel/events/core.c
모듈 주소Latched rbtree주소 범위모듈 코드 주소 → 모듈 구조체 매핑 (lock-free)kernel/module/tree_lookup.c
NUMA balancingrb_root_cached접근 빈도페이지 마이그레이션 대상 선정mm/mempolicy.c

구조 다이어그램

rbtree 삽입 및 리밸런싱

아래 다이어그램은 rbtree에 노드를 삽입한 후, Red-Red 위반을 해결하기 위한 리밸런싱 과정을 보여줍니다.

삽입 전 (Before) 50 Black 30 Red 70 Red insert(20) 삽입 후 (위반 발생) 50 30 70 20 Red-Red! Recolor (삼촌이 Red) 리밸런싱 완료 (Recolor) 50 Black(root) 30 70 20

리밸런싱은 크게 세 가지 경우로 나뉩니다:

경우조건조치회전 횟수
Case 1삼촌(uncle)이 Red부모와 삼촌을 Black으로, 조부모를 Red로 변경 후 조부모에서 재귀0
Case 2삼촌이 Black + 삼각형 배치부모를 기준으로 회전 → Case 3으로 변환1
Case 3삼촌이 Black + 직선 배치조부모를 기준으로 회전 + 색상 교환1
ℹ️

삽입 시 최대 2번의 회전(Case 2 + Case 3)으로 리밸런싱이 완료됩니다. Case 1은 O(log n)번 반복될 수 있지만 회전 없이 색상 변경만 수행하므로 비용이 낮습니다.

자료구조 비교

특성Red-Black TreeAVL TreeB-TreeSkip List
검색O(log n)O(log n) — 약간 더 빠름O(log n)O(log n) 기대값
삽입O(log n), 최대 2회전O(log n), 상수 회전(단/이중)O(log n), 노드 분할 가능O(log n) 기대값
삭제O(log n), 최대 3회전O(log n), 최악 O(log n) 회전O(log n), 노드 병합 가능O(log n) 기대값
높이 제한≤ 2 log2(n+1)≤ 1.44 log2(n+2)O(logB n)기대 O(log n)
노드 오버헤드포인터 3개 (24B/64bit)포인터 2~3개 + 높이다수 키/포인터다수 포인터 (레벨 수만큼)
캐시 친화성보통보통우수 (높은 팬아웃)낮음 (비연속 포인터)
구현 복잡도중간중간높음낮음
장점쓰기 빈번 시 유리, 회전 적음읽기 빈번 시 유리, 엄격한 균형디스크/캐시 친화적, 범위 검색 우수구현 간단, lock-free 가능
단점AVL보다 높이가 약간 높음쓰기 시 회전이 많음메모리 오버헤드 큼최악 시 O(n), 메모리 사용 불규칙
커널 사용광범위 (CFS, epoll, hrtimer, ext4 extent 등)사용 안 함btrfs, XFS (디스크상)사용 안 함
💡

커널에서 B-Tree가 사용되는 곳은 주로 디스크상 자료구조(btrfs, XFS)입니다. 인메모리 자료구조로는 rbtree가 압도적으로 많이 사용됩니다. 이는 rbtree의 낮은 오버헤드쓰기 성능 덕분입니다.

rbtree 디버깅 루틴

rbtree 버그는 삽입/삭제 후 균형 불변식 위반으로 드러납니다. 문제를 재현할 때는 회전과 링크 변경 경로를 함께 추적하세요.

  1. 삽입 경로 점검: 링크 설정 후 rb_insert_color() 호출 순서 확인
  2. 삭제 경로 점검: rb_erase() 뒤 노드 수명주기 확인
  3. 중복 키 정책 확인: 동일 키 처리 규칙(허용/거부) 명확화
  4. 증강 필드 검증: interval/augmented tree라면 propagate 경로 확인
# 사용 지점 및 오용 패턴 확인
git grep -n "rb_insert_color\\|rb_erase\\|rb_link_node" -- "*.c"
git grep -n "container_of(.*rb_node" -- "*.c"

회전 알고리즘 심화 (Rotation Deep Dive)

rbtree의 균형 유지는 회전(rotation)색상 변경(recoloring) 두 가지 연산으로 이루어집니다. 회전은 트리의 구조를 변경하면서도 BST 속성(왼쪽 < 부모 < 오른쪽)을 유지하는 핵심 연산입니다.

좌회전 (Left Rotation)

좌회전은 노드 X를 기준으로, X의 오른쪽 자식 Y를 X의 부모 위치로 올리는 연산입니다. Y의 왼쪽 서브트리 β는 X의 오른쪽 자식이 됩니다.

좌회전 전 P X α Y β γ rotate_left(X) 좌회전 후 P Y X α β γ ① X→rb_right = Y→rb_left(β) ② β의 부모 = X ③ Y→rb_left = X ④ X의 부모 = Y ⑤ P의 자식 포인터를 Y로 변경 β(점선)가 Y의 왼쪽에서 X의 오른쪽으로 이동 — BST 불변식(α < X < β < Y < γ)은 유지됨

우회전 (Right Rotation)

우회전은 좌회전의 대칭 연산입니다. 노드 Y를 기준으로, Y의 왼쪽 자식 X를 Y의 부모 위치로 올립니다.

우회전 전 P Y X α β γ rotate_right(Y) 우회전 후 P X α Y β γ ① Y→rb_left = X→rb_right(β) ② β의 부모 = Y ③ X→rb_right = Y ④ Y의 부모 = X ⑤ P의 자식 포인터를 X로 변경 좌회전과 완전 대칭 — β가 X의 오른쪽에서 Y의 왼쪽으로 이동

__rb_rotate_set_parents() 커널 구현

커널의 회전 함수는 부모 포인터와 색상을 동시에 설정하는 최적화된 헬퍼를 사용합니다. __rb_rotate_set_parents()는 회전 시 old 노드의 부모/색상을 new 노드에 이전하고, old 노드의 부모를 new 노드로 설정합니다.

/* lib/rbtree.c — 회전 시 부모/색상 일괄 설정 */
static inline void
__rb_rotate_set_parents(struct rb_node *old, struct rb_node *new,
                        struct rb_root *root, int color)
{
    struct rb_node *parent = rb_parent(old);

    /* new 노드에 old의 부모와 old의 원래 색상을 설정 */
    new->__rb_parent_color = old->__rb_parent_color;
    /* old 노드의 부모를 new로, 색상을 color로 설정 */
    rb_set_parent_color(old, new, color);
    /* 조부모(parent)의 자식 포인터를 old → new로 갱신 */
    __rb_change_child(old, new, parent, root);
}
최적화 포인트: __rb_parent_color를 통째로 복사하면 부모 포인터와 색상을 한 번의 대입으로 전달할 수 있습니다. 이는 rb_set_parent() + rb_set_color()를 별도 호출하는 것보다 효율적입니다.

삽입 리밸런싱 3가지 케이스 상세

새 노드 N을 삽입한 후 부모 P가 Red이면 "No Red-Red" 위반이 발생합니다. 삼촌(Uncle) U와 배치에 따라 아래 3가지 케이스로 처리합니다.

Case 1: 삼촌(U)이 Red → Recolor G P U N P,U→Black, G→Red, G에서 재귀 Case 2: 삼각형 → 직선 변환 G P U N P 기준 좌회전 → Case 3으로 변환 Case 3: 직선 → 균형 G P U N G 기준 우회전 + P↔G 색상 교환 삽입 리밸런싱 흐름 새 노드 N은 항상 Red로 삽입 → 부모 P가 Black이면 완료 → P가 Red이면 아래 분기: 삼촌 U가 Red → Case 1 (recolor, 조부모에서 재귀) │ 삼촌 U가 Black + 삼각형 → Case 2 → Case 3 삼촌 U가 Black + 직선 → Case 3 (1회전 + 색상 교환으로 완료) 커널 소스 매핑 (lib/rbtree.c __rb_insert) while (true) { parent = rb_parent(node); if (!parent) { /* node는 root → Black으로 설정 후 종료 */ } if (rb_is_black(parent)) break; /* 부모가 Black이면 위반 없음 */ gparent = rb_red_parent(parent); /* 조부모 (Red 부모의 부모) */ uncle = gparent->rb_right; /* (또는 rb_left, 대칭 처리) */ if (uncle && rb_is_red(uncle)) { /* Case 1: Recolor */ } if (node == parent->rb_right) { /* Case 2: 좌회전 → Case 3 */ } /* Case 3: 우회전 + __rb_rotate_set_parents() */ break; /* Case 3 완료 후 루프 종료 */ }
/* lib/rbtree.c — __rb_insert() 핵심 루프 (간략화) */
static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
            void (*augment_rotate)(struct rb_node *old,
                                    struct rb_node *new))
{
    struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;

    while (true) {
        /* 부모가 없으면 node가 root → Black으로 설정 */
        if (unlikely(!parent)) {
            rb_set_parent_color(node, NULL, RB_BLACK);
            break;
        }
        /* 부모가 Black이면 위반 없음 → 종료 */
        if (rb_is_black(parent))
            break;

        gparent = rb_red_parent(parent);
        tmp = gparent->rb_right;  /* 삼촌 후보 */

        if (parent != tmp) {  /* parent == gparent→rb_left */
            if (tmp && rb_is_red(tmp)) {
                /* Case 1: 삼촌이 Red → Recolor */
                rb_set_parent_color(tmp, gparent, RB_BLACK);
                rb_set_parent_color(parent, gparent, RB_BLACK);
                node = gparent;
                parent = rb_parent(node);
                rb_set_parent_color(node, parent, RB_RED);
                continue;
            }
            tmp = parent->rb_right;
            if (node == tmp) {
                /* Case 2: 삼각형 → 부모 좌회전 */
                tmp = node->rb_left;
                WRITE_ONCE(parent->rb_right, tmp);
                WRITE_ONCE(node->rb_left, parent);
                if (tmp)
                    rb_set_parent_color(tmp, parent, RB_BLACK);
                rb_set_parent_color(parent, node, RB_RED);
                augment_rotate(parent, node);
                parent = node;
                tmp = node->rb_right;
            }
            /* Case 3: 직선 → 조부모 우회전 */
            WRITE_ONCE(gparent->rb_left, tmp);
            WRITE_ONCE(parent->rb_right, gparent);
            if (tmp)
                rb_set_parent_color(tmp, gparent, RB_BLACK);
            __rb_rotate_set_parents(gparent, parent, root, RB_RED);
            augment_rotate(gparent, parent);
            break;
        }
        /* else: 대칭 케이스 (parent == gparent→rb_right) — 동일 로직 좌우 반전 */
    }
}
WRITE_ONCE 사용 이유: 컴파일러 최적화로 인한 store tearing을 방지합니다. 포인터 변경이 원자적으로 관찰되어야 RCU 읽기 측이 일관된 트리 구조를 볼 수 있습니다.

삭제 알고리즘 심화 (Delete Deep Dive)

rbtree 삭제는 삽입보다 복잡합니다. 삭제된 노드가 Black이면 Black-Height 불변식이 깨져 double-black 문제가 발생하며, 이를 해결하기 위해 4가지 케이스의 fixup이 필요합니다.

삭제 전체 흐름

  1. BST 삭제: 자식이 0~1개면 직접 제거, 자식 2개면 in-order 후계자(successor)로 교체 후 후계자를 제거합니다.
  2. 색상 판단: 실제 제거된 노드가 Red이면 불변식 위반 없이 종료. Black이면 fixup 진행.
  3. Fixup: double-black 노드에서 시작하여 4가지 케이스를 처리합니다.
삭제 Fixup 4가지 케이스 (N = double-black 노드, S = 형제) Case 1: 형제 S가 Red → 부모 P 기준 회전 (N 방향으로) + P↔S 색상 교환 → S가 Black인 새 형제로 교체됨 → Case 2/3/4로 이동 회전 1회, fixup 계속 커널: __rb_erase_color() 첫 번째 분기 Case 2: S가 Black, S의 자식 둘 다 Black → S를 Red로 변경 (Black-Height를 맞추기 위해) → double-black이 부모 P로 전파 회전 0회, 부모에서 재귀 (P가 Red면 Black으로 바꾸고 종료) 커널: sibling→__rb_parent_color에 RB_RED 설정 Case 3: S가 Black, S의 가까운 자식만 Red → S 기준 회전 (N 반대 방향) + S↔가까운 자식 색상 교환 → 새 형제의 먼 자식이 Red인 상태 → Case 4로 이동 회전 1회, Case 4로 진행 커널: Case 3 → Case 4 연속 처리 (fall-through) Case 4: S가 Black, S의 먼 자식이 Red → 부모 P 기준 회전 (N 방향) + P→Black, S→P색상, 먼 자식→Black → double-black 해소 완료! 회전 1회, fixup 종료 (terminal case) 커널: __rb_rotate_set_parents() + break Fixup 흐름도 삭제된 노드 Black? No 완료 Yes 형제 S Red? Yes Case 1 No S 자식 모두 Black? Yes Case 2 No C3→C4 새 형제로 재진입 부모로 재귀 (P Red면 종료)

__rb_erase_augmented() 핵심 흐름

__rb_erase_augmented()는 삭제할 노드의 자식 수에 따라 처리를 분기합니다. 증강 rbtree의 경우 삭제/교체 시 콜백을 호출하여 서브트리 정보를 갱신합니다.

/* include/linux/rbtree_augmented.h — 삭제 핵심 (간략화) */
static __always_inline struct rb_node *
__rb_erase_augmented(struct rb_node *node, struct rb_root *root,
                     const struct rb_augment_callbacks *augment)
{
    struct rb_node *child = node->rb_right;
    struct rb_node *tmp = node->rb_left;
    struct rb_node *parent, *rebalance;

    if (!tmp) {
        /* Case A: 왼쪽 자식 없음 → 오른쪽 자식(또는 NULL)으로 교체 */
        parent = rb_parent(node);
        __rb_change_child(node, child, parent, root);
        if (child) {
            child->__rb_parent_color = node->__rb_parent_color;
            rebalance = NULL;
        } else {
            rebalance = rb_is_black(node) ? parent : NULL;
        }
    } else if (!child) {
        /* Case B: 오른쪽 자식 없음 → 왼쪽 자식으로 교체 */
        parent = rb_parent(node);
        tmp->__rb_parent_color = node->__rb_parent_color;
        __rb_change_child(node, tmp, parent, root);
        rebalance = NULL;
    } else {
        /* Case C: 자식 2개 → in-order 후계자(successor)를 찾아 교체 */
        struct rb_node *successor = child, *child2;

        tmp = child->rb_left;
        if (!tmp) {
            /* 후계자가 바로 오른쪽 자식인 경우 */
            parent = successor;
            child2 = successor->rb_right;
            augment->copy(node, successor);
        } else {
            /* 후계자가 오른쪽 서브트리의 최좌 노드 */
            do {
                parent = successor;
                successor = tmp;
                tmp = tmp->rb_left;
            } while (tmp);
            child2 = successor->rb_right;
            WRITE_ONCE(parent->rb_left, child2);
            WRITE_ONCE(successor->rb_right, child);
            rb_set_parent(child, successor);
            augment->copy(node, successor);
            augment->propagate(parent, successor);
        }
        /* successor를 node 위치에 배치 */
        /* ... 부모/자식 포인터 재연결 ... */
        rebalance = rb_is_black(successor) ? parent : NULL;
    }
    return rebalance;  /* NULL이면 fixup 불필요, 아니면 fixup 시작점 */
}
삭제가 삽입보다 복잡한 이유: 삽입은 항상 Red 노드를 추가하므로 Black-Height 위반은 없고 "No Red-Red" 위반만 처리하면 됩니다. 반면 삭제는 Black 노드가 제거될 수 있어 Black-Height 불변식이 깨질 수 있습니다. 이 "double-black" 문제 해결이 4가지 케이스를 요구합니다. 그럼에도 최대 3번의 회전으로 완료됩니다.

증강 rbtree 심화 (rb_augment Deep Dive)

증강 rbtree는 각 노드에 서브트리 요약 정보(예: 서브트리 내 최댓값, 총 합 등)를 캐싱하고, 트리 회전이나 노드 교체 시 이 정보를 자동으로 갱신합니다. 커널은 세 가지 콜백(propagate, copy, rotate)을 통해 이를 처리합니다.

콜백 체인: propagate → copy → rotate

증강 콜백 호출 시점 propagate(node, stop) node에서 stop까지 위로 올라가며 각 노드의 증강 데이터를 자식 정보로부터 재계산 호출: 삽입 후, 삭제 시 후계자 교체 후 목적: 변경 경로의 모든 조상 갱신 copy(old, new) old 노드의 증강 데이터를 new 노드로 복사 (서브트리 구조가 동일할 때) 호출: 삭제 시 후계자로 교체할 때 목적: 위치 교체 시 데이터 이전 rotate(old, new) 회전으로 old가 아래로, new가 위로 이동한 후 old의 증강 데이터 재계산 호출: 좌/우 회전 직후 목적: 회전으로 바뀐 서브트리 갱신 예시: Interval Tree에서의 증강 — __subtree_last 관리 [10,20] max=50 [5,15] max=15 [25,50] max=50 __subtree_last = max(self.last, left.__subtree_last, right.__subtree_last) propagate()가 리프→루트로 갱신

RB_DECLARE_CALLBACKS 매크로

커널은 RB_DECLARE_CALLBACKSRB_DECLARE_CALLBACKS_MAX 매크로로 증강 콜백 보일러플레이트를 자동 생성합니다.

/* include/linux/rbtree_augmented.h */

/*
 * RB_DECLARE_CALLBACKS — propagate/copy/rotate 콜백 자동 생성
 *
 * rbstruct:  rb_node를 포함하는 구조체 타입
 * rbfield:   rb_node 필드 이름
 * rbtype:    증강 데이터의 타입
 * rbaugmented: 증강 데이터 필드 이름
 * rbcompute: 증강 데이터 재계산 함수
 */
#define RB_DECLARE_CALLBACKS(rbstatic, rbname,               \
                             rbstruct, rbfield,              \
                             rbtype, rbaugmented, rbcompute) \
static inline void                                          \
rbname##_propagate(struct rb_node *rb, struct rb_node *stop) \
{                                                            \
    while (rb != stop) {                                     \
        rbstruct *node = rb_entry(rb, rbstruct, rbfield);     \
        rbtype augmented = rbcompute(node);                  \
        if (node->rbaugmented == augmented)                   \
            break;  /* 변경 없으면 조기 종료 */             \
        node->rbaugmented = augmented;                       \
        rb = rb_parent(&node->rbfield);                     \
    }                                                        \
}                                                            \
/* ... copy, rotate 콜백도 유사하게 생성 ... */

/*
 * RB_DECLARE_CALLBACKS_MAX — max 계산 전용 변형
 * 서브트리의 최댓값을 자동으로 계산 (Interval Tree 패턴)
 */
#define RB_DECLARE_CALLBACKS_MAX(rbstatic, rbname,           \
                                 rbstruct, rbfield,          \
                                 rbtype, rbaugmented,        \
                                 rbcompute)                  \
/* 내부적으로 left/right 자식의 max와 self 값 중 최대를 선택 */

augment_rotate 동작 예시

/* Interval Tree의 rotate 콜백 — 간략화 */
static inline void
interval_tree_augment_rotate(struct rb_node *old, struct rb_node *new)
{
    struct interval_tree_node *old_it, *new_it;

    old_it = rb_entry(old, struct interval_tree_node, rb);
    new_it = rb_entry(new, struct interval_tree_node, rb);

    /* new는 old의 서브트리를 그대로 물려받으므로
     * new→__subtree_last = old→__subtree_last (이전 값 유지) */
    new_it->__subtree_last = old_it->__subtree_last;

    /* old는 서브트리가 변경되었으므로 재계산 */
    old_it->__subtree_last = interval_tree_compute_max(old_it);
}
rotate 콜백 최적화: 회전 시 위로 올라간 노드(new)는 아래로 내려간 노드(old)의 서브트리 정보를 그대로 승계합니다. old 노드만 재계산하면 되므로 O(1)에 완료됩니다. propagate()는 경로를 따라 올라가며 O(log n)이지만, 값이 변경되지 않으면 조기 종료합니다.

메모리 레이아웃과 색상 인코딩

rb_node는 64비트 시스템에서 정확히 24바이트를 차지합니다. 색상 정보를 별도 필드 없이 부모 포인터의 LSB에 인코딩하여 메모리를 절약하는 것이 핵심입니다.

rb_node 메모리 레이아웃 (64-bit, 24 bytes) 오프셋 필드 비트 구성 +0 __rb_parent_color 비트 63~2: 부모 포인터 (62비트) 1 C bit 1 bit 0 색상 +8 rb_right 오른쪽 자식 포인터 (64비트) +16 rb_left 왼쪽 자식 포인터 (64비트) 캐시 라인 분석 1. rb_node 24바이트 → 64바이트 캐시 라인에 2.6개 적재 가능 2. __attribute__((aligned(sizeof(long))))으로 8바이트 정렬 보장 3. 정렬 보장 → 하위 2비트(bit 0, bit 1)는 항상 0 → bit 0을 색상으로 활용 4. 부모 포인터 추출: (pc & ~3) → 하위 2비트 마스킹 5. 색상 추출: (pc & 1) → 0=Red, 1=Black 6. 침투적(intrusive) 설계 → rb_node가 사용자 구조체 내부에 있으므로 데이터와 같은 캐시 라인에 위치할 수 있음
왜 bit 0이 색상인가: rb_nodesizeof(long) 정렬(64비트 시스템에서 8바이트)이므로, 유효한 포인터의 하위 3비트는 항상 0입니다. 커널은 이 중 bit 0만 색상으로 사용하고 bit 1은 미래 확장을 위해 예약합니다. & ~3 마스크로 하위 2비트를 모두 제거하여 부모 포인터를 추출합니다.

CFS 스케줄러 rbtree 분석

CFS(Completely Fair Scheduler)는 rb_root_cached를 사용하여 실행 가능한 태스크를 vruntime(가상 실행 시간) 순서로 관리합니다. vruntime이 가장 작은(가장 적게 실행된) 태스크를 O(1)에 선택합니다.

CFS tasks_timeline (rb_root_cached) cfs_rq.tasks_timeline rb_root + rb_leftmost O(1) pick_next_task! rb_root rb_leftmost Task C vr=500 Task B vr=200 Task D vr=800 Task A vr=100 Task E vr=300 Task F vr=1000 ← vruntime 작음 (적게 실행) │ 많이 실행 → rb_first_cached() → Task A (vr=100) → O(1) pick_next_task_fair()가 이 태스크를 선택

__enqueue_entity() 분석

/* kernel/sched/fair.c — CFS rbtree 삽입 */
static void __enqueue_entity(struct cfs_rq *cfs_rq,
                             struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
    struct rb_node *parent = NULL;
    bool leftmost = true;

    while (*link) {
        struct sched_entity *entry;
        parent = *link;
        entry = rb_entry(parent, struct sched_entity, run_node);

        /*
         * vruntime 비교: entity_before()
         * se->vruntime < entry->vruntime이면 왼쪽으로
         */
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = false;  /* 오른쪽으로 이동 → 최좌 아님 */
        }
    }

    rb_link_node(&se->run_node, parent, link);
    rb_insert_color_cached(&se->run_node,
                            &cfs_rq->tasks_timeline, leftmost);
}

__dequeue_entity() 분석

/* kernel/sched/fair.c — CFS rbtree 삭제 */
static void __dequeue_entity(struct cfs_rq *cfs_rq,
                              struct sched_entity *se)
{
    rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline);
}

/* pick_next_entity — O(1) 태스크 선택 */
static struct sched_entity *
__pick_first_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}
EEVDF와의 관계: 커널 6.6부터 CFS가 EEVDF(Earliest Eligible Virtual Deadline First)로 진화했습니다. EEVDF도 rbtree를 사용하지만, 단순 vruntime 최솟값 대신 eligible(자격) + virtual deadline 두 가지 기준으로 태스크를 선택합니다. rb_root_cached 구조는 그대로 유지되며, pick_eevdf()가 적격 태스크 중 deadline이 가장 빠른 것을 선택합니다.

hrtimer rbtree 분석

고해상도 타이머(hrtimer)는 rb_root_cached를 사용하여 타이머를 만료 시간(ktime) 순서로 관리합니다. rb_first_cached()로 다음에 만료될 타이머를 O(1)에 조회합니다.

hrtimer: per-CPU timerqueue (rb_root_cached) CPU 0 hrtimer_cpu_base CPU 1 hrtimer_cpu_base ... CPU N hrtimer_cpu_base MONOTONIC base REALTIME base BOOTTIME base rb_root_cached T3 500ns T1 100ns T5 800ns T0 T2 rb_leftmost hrtimer 설계 특징 • per-CPU → 잠금 경합 최소화 • clock base별 별도 rbtree • rb_first_cached → 다음 만료 O(1) • ktime(나노초) 키 → 정밀한 정렬 타이머 마이그레이션 CPU 오프라인 시 타이머를 다른 CPU의 rbtree로 이전 (hrtimer_cpu_dying)

enqueue_hrtimer() 분석

/* kernel/time/hrtimer.c — 타이머 등록 */
static void enqueue_hrtimer(struct hrtimer *timer,
                            struct hrtimer_clock_base *base,
                            enum hrtimer_mode mode)
{
    /* timerqueue_add()는 내부적으로 rb_root_cached에 삽입 */
    timerqueue_add(&base->active, &timer->node);
    /* ... reprogram 등 ... */
}

/* kernel/time/timer_list.c — timerqueue 삽입 핵심 */
bool timerqueue_add(struct timerqueue_head *head,
                    struct timerqueue_node *node)
{
    struct rb_node **p = &head->rb_root.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct timerqueue_node *ptr;
    bool leftmost = true;

    while (*p) {
        parent = *p;
        ptr = rb_entry(parent, struct timerqueue_node, node);

        /* ktime 비교: expires 기준 */
        if (node->expires < ptr->expires) {
            p = &(*p)->rb_left;
        } else {
            p = &(*p)->rb_right;
            leftmost = false;
        }
    }

    rb_link_node(&node->node, parent, p);
    rb_insert_color_cached(&node->node, &head->rb_root, leftmost);

    return leftmost;  /* 새 타이머가 최솟값이면 true → HW reprogramming 필요 */
}
leftmost 반환값의 의미: timerqueue_add()true를 반환하면 새로 삽입된 타이머가 기존 최소 만료 시간보다 빠르다는 뜻입니다. 이 경우 하드웨어 타이머(APIC, HPET 등)를 새 만료 시간으로 재프로그래밍해야 합니다.

epoll rbtree 분석

epoll은 모니터링할 파일 디스크립터(fd)를 rb_root_cached로 관리합니다. fd 기반으로 정렬하여 O(log n) 삽입/삭제/검색을 제공합니다.

epoll: eventpoll.rbr (rb_root_cached) struct eventpoll rbr: rb_root_cached rdllist: list_head (ready list) fd=10 epitem fd=5 epitem fd=20 epitem fd=3 epitem fd=7 fd=25 비교 키 1차: file 포인터 2차: fd 번호 (같은 file도 다른 fd로 등록 가능) rb_leftmost

ep_insert() 코드 분석

/* fs/eventpoll.c — epoll_ctl(EPOLL_CTL_ADD) 핵심 */
static int ep_insert(struct eventpoll *ep,
                     const struct epoll_event *event,
                     struct file *tfile, int fd,
                     int full_check)
{
    struct epitem *epi;

    /* epitem 할당 */
    epi = kmem_cache_zalloc(epi_cache, GFP_KERNEL);
    if (unlikely(!epi))
        return -ENOMEM;

    /* 키 설정: file 포인터 + fd */
    epi->ffd.file = tfile;
    epi->ffd.fd = fd;
    epi->event = *event;
    epi->ep = ep;

    /* rbtree에 삽입 */
    ep_rbtree_insert(ep, epi);
    /* ... poll callback 등록, wakeup 설정 등 ... */
    return 0;
}

/* rbtree 삽입 — (file, fd) 쌍으로 비교 */
static void ep_rbtree_insert(struct eventpoll *ep,
                             struct epitem *epi)
{
    struct rb_node **p = &ep->rbr.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct epitem *epic;
    bool leftmost = true;

    while (*p) {
        parent = *p;
        epic = rb_entry(parent, struct epitem, rbn);

        /* ep_cmp_ffd(): file 포인터 비교, 같으면 fd 비교 */
        if (ep_cmp_ffd(&epi->ffd, &epic->ffd) > 0) {
            p = &(*p)->rb_right;
            leftmost = false;
        } else {
            p = &(*p)->rb_left;
        }
    }

    rb_link_node(&epi->rbn, parent, p);
    rb_insert_color_cached(&epi->rbn, &ep->rbr, leftmost);
}
epoll의 이중 자료구조: epoll은 등록된 fd를 rbtree(빠른 검색/삭제)로 관리하고, 이벤트가 발생한 fd를 linked list(rdllist, ready list)로 별도 관리합니다. epoll_wait()는 ready list에서만 이벤트를 수집하므로 O(이벤트 수)에 동작합니다.

ftrace/perf 실습

rbtree 연산을 실시간으로 추적하여 성능 병목과 동작을 분석할 수 있습니다.

ftrace로 rbtree 연산 추적

# rb_insert_color / rb_erase 함수 추적
echo 'rb_insert_color' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'rb_erase' >> /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

# 출력 예시:
#  -0   [001] d.h.  1234.567890: rb_insert_color <-enqueue_hrtimer
#  nginx-1234 [003] ....  1234.567891: rb_insert_color <-ep_insert
#  nginx-1234 [003] ....  1234.567895: rb_erase        <-ep_remove

# 호출 스택 포함 추적 (function_graph)
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'rb_insert_color' > /sys/kernel/debug/tracing/set_graph_function

# 정리
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer

perf probe로 CFS enqueue/dequeue 추적

# CFS enqueue/dequeue 동적 프로브 등록
perf probe --add '__enqueue_entity cfs_rq se'
perf probe --add '__dequeue_entity cfs_rq se'

# 10초간 이벤트 기록
perf record -e 'probe:__enqueue_entity' \
            -e 'probe:__dequeue_entity' \
            -a -- sleep 10

# 결과 분석
perf report

# perf stat으로 캐시 미스 측정
perf stat -e cache-misses,cache-references,instructions \
          -p $(pgrep -f workload) -- sleep 5

# 프로브 정리
perf probe --del 'probe:__enqueue_entity'
perf probe --del 'probe:__dequeue_entity'

bpftrace 원라이너

# rbtree 회전 횟수를 호출자별로 카운트
bpftrace -e 'kprobe:rb_insert_color { @insert[kstack(2)] = count(); }'

# rb_erase 호출 빈도 (프로세스별)
bpftrace -e 'kprobe:rb_erase { @erase[comm] = count(); }'

# CFS enqueue 시 vruntime 분포 히스토그램
bpftrace -e 'kprobe:__enqueue_entity {
    $se = (struct sched_entity *)arg1;
    @vruntime = hist($se->vruntime);
}'

# hrtimer 등록 빈도 (clock base별)
bpftrace -e 'kprobe:enqueue_hrtimer {
    @hrtimer[kstack(3)] = count();
}'
프로덕션 주의: rb_insert_color/rb_erase는 커널 전반에서 매우 빈번하게 호출됩니다. ftrace/bpftrace 추적 시 오버헤드가 커질 수 있으므로, 가능하면 특정 서브시스템의 wrapper 함수(__enqueue_entity, ep_insert 등)를 프로브 대상으로 선택하세요.

성능 특성과 튜닝

rbtree는 범용적으로 우수하지만, 워크로드에 따라 다른 자료구조가 더 적합할 수 있습니다. 아래 비교표와 분석을 통해 적절한 선택을 할 수 있습니다.

rbtree vs Maple Tree vs XArray 비교

특성rbtreeMaple TreeXArray
자료구조 유형이진 탐색 트리B-tree 변형 (6.1+)Radix tree 래퍼
검색O(log n)O(log n), 캐시 친화적O(log n), 키가 정수일 때 최적
삽입/삭제O(log n), 최대 2~3회전O(log n), 노드 분할/병합O(log n), 노드 분할/축소
범위 검색O(log n + k)O(log n + k), 더 빠름O(log n + k)
캐시 효율보통 (노드당 24B)우수 (높은 팬아웃)우수 (정수 키 특화)
RCU 지원제한적 (latched rbtree)네이티브 RCU-safe네이티브 RCU-safe
메모리 오버헤드낮음 (침투적)중간 (슬랩 할당)중간 (슬랩 할당)
키 유형임의 (사용자 비교)정수 범위정수 (unsigned long)
대표 사용처CFS, hrtimer, epollVMA (6.1+)페이지 캐시, IDR

캐시 미스 분석

# rbtree 연산의 캐시 미스 측정
perf stat -e L1-dcache-load-misses,L1-dcache-loads,\
LLC-load-misses,LLC-loads \
-p $(pgrep -f target_workload) -- sleep 10

# 출력 예시:
#  1,234,567  L1-dcache-load-misses  #  3.45% of all L1-dcache loads
#    567,890  LLC-load-misses        # 12.34% of all LLC loads
#
# rbtree 워크로드에서 LLC miss가 높으면:
# - 노드가 메모리에 분산되어 캐시 지역성이 낮음
# - Maple Tree나 B-tree 변형이 더 적합할 수 있음

rb_root vs rb_root_cached 선택 기준

기준rb_rootrb_root_cached
최솟값 접근 빈도드묾빈번 (스케줄러, 타이머 등)
메모리 오버헤드포인터 1개포인터 2개 (+8B)
삽입 코드rb_insert_color()rb_insert_color_cached() + leftmost 추적
삭제 코드rb_erase()rb_erase_cached()
rb_first() 복잡도O(log n)O(1)
적합한 경우범용, 최솟값 불필요우선순위 큐 패턴 (min-heap 대용)
선택 가이드: 최솟값을 주기적으로 조회하는 패턴(스케줄러의 pick_next, 타이머의 next_expiry)에서는 반드시 rb_root_cached를 사용하세요. 8바이트 추가 메모리로 O(log n) → O(1) 최적화를 얻을 수 있습니다.

rbtree vs 대안 자료구조 실전 선택

커널에서 자료구조를 선택할 때는 워크로드 특성, RCU 요구사항, 키 유형 등을 종합적으로 고려해야 합니다.

커널 자료구조 선택 플로우차트 정렬/검색이 필요한가? No list_head / hlist / hashtable Yes 키가 정수(unsigned long)? Yes RCU-safe 필요? Yes XArray No 범위 키? Yes Maple Tree No (복합 키) 쓰기 빈번? Yes rbtree 최솟값 O(1) 필요? Yes rb_root_cached No rb_root 마이그레이션 사례 VMA: rbtree → Maple Tree (커널 6.1) 이유: 범위 키(vm_start~vm_end), RCU-safe 필요, 캐시 친화성 향상 (B-tree 팬아웃) 결과: mmap/munmap 성능 향상, lock 경합 감소 (rcu_read_lock으로 VMA 순회 가능) CFS/hrtimer/epoll: rbtree 유지 이유: 복합 비교 키(vruntime/ktime/fd+file), 침투적 설계 장점, 쓰기 빈번한 패턴에서 rbtree가 여전히 최적

VMA 마이그레이션 상세

커널 6.1에서 VMA가 rbtree에서 Maple Tree로 마이그레이션된 핵심 이유는 다음과 같습니다:

관점rbtree (6.0 이하)Maple Tree (6.1+)
RCU 읽기불가 (회전 시 일관성 깨짐)네이티브 지원 (RCU-safe 순회)
범위 검색interval tree로 보완B-tree 구조로 자연스럽게 지원
잠금mmap_lock (rwsem)RCU로 읽기 잠금 제거 가능
캐시 효율노드당 24B, 분산 배치높은 팬아웃, 연속 배치
메모리침투적 (VMA 내 rb_node)외부 할당 (슬랩)
rbtree가 여전히 최적인 경우: (1) 비교 함수가 복잡하거나 복합 키를 사용하는 경우, (2) 침투적 설계로 메모리 할당을 피해야 하는 경우, (3) 쓰기(삽입/삭제)가 읽기보다 빈번한 경우. CFS, hrtimer, epoll, I/O 스케줄러 등은 이 조건에 해당하므로 rbtree를 계속 사용합니다.

Red-Black Tree와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.