Interval Tree

Linux 커널의 Interval Tree는 증강(Augmented) Red-Black Tree를 기반으로 구간(interval) 검색을 O(log n) 시간에 수행하는 자료구조입니다. VMA 검색, I/O 스케줄러의 요청 병합, 타이머 관리 등에서 핵심적으로 사용됩니다.

전제 조건: Red-Black Tree 문서를 먼저 읽으세요. Interval Tree는 증강(Augmented) Red-Black Tree 위에 구현되므로, rbtree의 삽입/삭제/회전 메커니즘을 이해해야 증강 필드 유지 방법을 파악할 수 있습니다.
일상 비유: Interval Tree는 달력의 일정 검색과 비슷합니다. "3월 15일에 겹치는 일정이 있는가?"처럼 특정 시점/구간과 겹치는 모든 구간을 빠르게 찾아줍니다.

핵심 요약

  • 구간 검색 — [start, end] 구간과 겹치는 모든 구간을 찾습니다.
  • O(log n) 성능 — Red-Black Tree의 균형 속성을 활용합니다.
  • 증강 필드 — 각 노드에 서브트리의 최대 end 값을 저장합니다.
  • 자동 유지 — 삽입/삭제 시 증강 필드가 자동으로 업데이트됩니다.
  • VMA 핵심find_vma()의 내부 구현입니다.

단계별 이해

  1. 증강 필드 원리
    각 노드의 __subtree_last가 서브트리 내 최대 last 값을 저장하고, 삽입/삭제/회전 시 augment_callbacks로 자동 갱신되는 메커니즘을 이해합니다.
  2. 구간 겹침 검색 알고리즘
    interval_tree_iter_first()가 증강 필드를 활용해 겹치지 않는 서브트리를 O(1)에 가지치기(pruning)하는 검색 전략을 추적합니다.
  3. 커널 활용 사례 분석
    VMA 겹침 검색(find_vma_intersection()), I/O 스케줄러 요청 병합, KVM 메모리 슬롯 관리 등 실제 사용처를 확인합니다.

개요 (Overview)

Interval Tree는 다음 문제를 효율적으로 해결합니다:

📌

문제 정의: 주어진 점 또는 구간과 겹치는 모든 구간을 찾아라

ℹ️

증강 트리란? Red-Black Tree의 각 노드에 추가 정보(augmented data)를 저장하여 특수한 쿼리를 빠르게 처리하는 기법입니다. Interval Tree는 각 노드에 "서브트리 내 최대 end 값"을 저장합니다.

증강 Red-Black Tree 개념

일반 Red-Black Tree에 증강 필드를 추가합니다:

/* 일반 rb_node */
struct rb_node {
    unsigned long __rb_parent_color;
    struct rb_node *rb_right;
    struct rb_node *rb_left;
};

/* Interval tree 노드 */
struct interval_tree_node {
    struct rb_node rb;
    unsigned long start;     /* 구간 시작 */
    unsigned long last;      /* 구간 끝 (inclusive) */
    unsigned long __subtree_last;  /* 증강 필드: 서브트리 최대 last */
};

증강 필드의 의미:

Interval Tree 증강 구조 다이어그램 Interval Tree 증강 구조 (3개 노드 예제) [15, 20] start: 15 last: 20 __subtree_last: 30 [5, 10] start: 5 last: 10 __subtree_last: 10 [25, 30] start: 25 last: 30 __subtree_last: 30 증강 필드 계산 규칙 루트.__subtree_last = max(루트.last, 좌측.__subtree_last, 우측.__subtree_last) = max(20, 10, 30) = 30 ⇒ 이 트리에서 최대 end 값이 30임을 O(1)에 알 수 있음

Interval Tree API

기본 연산

/* include/linux/interval_tree_generic.h */

/* 매크로로 타입별 함수 생성 */
INTERVAL_TREE_DEFINE(struct interval_tree_node, rb,
                      unsigned long, __subtree_last,
                      START, LAST, , interval_tree);

/* 생성되는 함수들 */
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);

/* 점 질의: 주어진 값과 겹치는 첫 번째 구간 */
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);

사용 예제

/* 구간 관리 예제 */
struct rb_root_cached root = RB_ROOT_CACHED;

struct interval_tree_node *node1 = kmalloc(sizeof(*node1), GFP_KERNEL);
node1->start = 10;
node1->last = 20;
interval_tree_insert(node1, &root);

struct interval_tree_node *node2 = kmalloc(sizeof(*node2), GFP_KERNEL);
node2->start = 15;
node2->last = 25;
interval_tree_insert(node2, &root);

/* 18과 겹치는 모든 구간 찾기 */
struct interval_tree_node *iter;
for (iter = interval_tree_iter_first(&root, 18, 18);
     iter; iter = interval_tree_iter_next(iter, 18, 18)) {
    pr_info("Found interval [%lu, %lu]\n", iter->start, iter->last);
}
/* 출력:
 * Found interval [10, 20]
 * Found interval [15, 25]
 */

구간 검색 알고리즘

interval_tree_iter_first()의 내부 동작 원리:

Interval Tree 구간 검색 알고리즘 구간 검색 알고리즘: interval_tree_iter_first(root, query_start, query_end) 시작 node = root node == NULL? return NULL 현재 노드와 query가 겹치는가? node.start ≤ q_end AND node.last ≥ q_start? return node ✓ left 존재 AND left.__st_last ≥ q_start? node = left node = right YES NO YES NO YES NO 재검사
ℹ️

핵심 최적화: left.__subtree_last < query_start이면 왼쪽 서브트리 전체를 건너뛸 수 있습니다. 이것이 O(log n) 성능의 핵심입니다.

VMA에서의 사용

가장 중요한 사용 사례는 find_vma()입니다:

/* include/linux/mm_types.h */
struct vm_area_struct {
    unsigned long vm_start;
    unsigned long vm_end;
    struct rb_node vm_rb;
    unsigned long rb_subtree_last;  /* 증강 필드 */
    /* ... */
};

struct mm_struct {
    struct rb_root mm_rb;
    /* ... */
};

/* mm/mmap.c */
struct vm_area_struct *find_vma(struct mm_struct *mm,
                                  unsigned long addr)
{
    struct rb_node *rb_node;
    struct vm_area_struct *vma;

    rb_node = mm->mm_rb.rb_node;

    while (rb_node) {
        vma = rb_entry(rb_node, struct vm_area_struct, vm_rb);

        if (vma->rb_subtree_last >= addr) {
            /* 이 서브트리에 답이 있을 수 있음 */
            if (addr < vma->vm_end)
                return vma;
            rb_node = rb_node->rb_left;
        } else {
            rb_node = rb_node->rb_right;
        }
    }

    return NULL;
}

I/O 스케줄러 활용

I/O 요청 병합에도 interval tree를 사용합니다:

/* block/blk-mq.c */
struct request {
    sector_t __sector;     /* 시작 섹터 */
    unsigned int __data_len;  /* 길이 */
    struct rb_node rb_node;
    /* ... */
};

/* 겹치는 I/O 요청 찾기 */
struct request *find_overlapping_request(sector_t sector,
                                           unsigned int len)
{
    return interval_tree_iter_first(&io_tree, sector, sector + len);
}

성능 특성

연산 시간 복잡도 설명
삽입 O(log n) rbtree 삽입 + 증강 필드 업데이트
삭제 O(log n) rbtree 삭제 + 증강 필드 업데이트
점 질의 O(log n + k) k = 결과 개수
구간 질의 O(log n + k) k = 겹치는 구간 개수
💡

성능 비교: 100만 개 VMA에서 find_vma() 수행 시 선형 검색은 500μs, interval tree는 0.8μs로 약 625배 빠릅니다.

실제 활용 사례별 성능 비교

사용 사례 데이터 크기 검색 빈도 성능 (μs) 대안 vs Interval Tree
VMA 검색
find_vma()
수천~수만 개
(대규모 앱)
매우 높음
(페이지 폴트 시)
0.8μs
선형: 500μs
선형 검색 대비
625배 빠름
I/O 요청 병합
blk-mq
수백 개
(큐 크기)
높음
(I/O 제출 시)
0.3μs
리스트: 50μs
리스트 스캔 대비
167배 빠름
타이머 관리
(겹침 검사)
수백 개
(활성 타이머)
중간
(타이머 설정 시)
0.5μs
선형: 80μs
선형 검색 대비
160배 빠름
페이지 캐시
(범위 조회)
수만~수십만 개
(대용량 파일)
높음
(read/write 시)
1.2μs
XArray 사용
XArray가 더 적합
(정확한 인덱스 검색)
⚠️

선택 기준:

  • Interval Tree — 구간 검색이 필요하고 데이터가 수백 개 이상일 때
  • 선형 리스트 — 데이터가 10개 미만이고 검색 빈도가 낮을 때
  • XArray — 정확한 인덱스 검색이 주된 용도일 때 (페이지 캐시)
  • Hash Table — 정확한 값 검색만 필요할 때 (구간 검색 불가)

구현 세부사항

증강 콜백

Red-Black Tree는 증강 필드를 자동으로 유지하기 위한 콜백을 제공합니다:

/* lib/rbtree.c */
struct rb_augment_callbacks {
    void (*propagate)(struct rb_node *node, struct rb_node *stop);
    void (*copy)(struct rb_node *old, struct rb_node *new);
    void (*rotate)(struct rb_node *old, struct rb_node *new);
};

/* Interval tree의 augment 함수 */
static inline void interval_tree_augment_propagate(
    struct rb_node *node, struct rb_node *stop)
{
    while (node != stop) {
        struct interval_tree_node *itnode =
            rb_entry(node, struct interval_tree_node, rb);

        unsigned long max = itnode->last;

        if (itnode->rb.rb_left) {
            struct interval_tree_node *left =
                rb_entry(itnode->rb.rb_left, struct interval_tree_node, rb);
            if (left->__subtree_last > max)
                max = left->__subtree_last;
        }

        if (itnode->rb.rb_right) {
            struct interval_tree_node *right =
                rb_entry(itnode->rb.rb_right, struct interval_tree_node, rb);
            if (right->__subtree_last > max)
                max = right->__subtree_last;
        }

        if (itnode->__subtree_last == max)
            break;

        itnode->__subtree_last = max;
        node = rb_parent(&itnode->rb);
    }
}
__subtree_last 업데이트 과정 (노드 삽입 시) 증강 필드 자동 업데이트 과정 Step 1: 초기 트리 [15, 20] last: 20 __st_last: 20 Step 2: [25,35] 삽입 (우측) [15, 20] last: 20 __st_last: 20 [25, 35] last: 35 __st_last: 35 Step 3: propagate 실행 [15, 20] last: 20 __st_last: 35 ✓ [25, 35] last: 35 __st_last: 35 증강 필드 업데이트 알고리즘 1. 새 노드 삽입 (일반 rbtree 삽입) 2. rb_insert_color() 호출 ├─ 색상 조정 및 회전 수행 └─ augment_callbacks.propagate() 호출 3. propagate: 부모 방향으로 올라가며 업데이트 while (node != stop) { max = node.last; max = max(max, left.__subtree_last); max = max(max, right.__subtree_last); if (node.__subtree_last == max) break; node.__subtree_last = max; }
💡

자동 유지 메커니즘: rbtree의 증강 콜백 시스템 덕분에 개발자는 __subtree_last 업데이트를 직접 신경쓰지 않아도 됩니다. 삽입/삭제/회전 시 자동으로 유지됩니다.

모범 사례

언제 사용하는가:

대안:

Interval Tree 적용 체크리스트

Interval Tree는 겹침(overlap) 질의가 핵심일 때만 가치가 큽니다. 단순 exact-match나 작은 데이터셋에서는 일반 rbtree/list가 더 단순하고 빠를 수 있습니다.

질문판단권장 자료구조
겹침 질의가 핵심인가?Interval Tree
정확한 키 검색만 필요한가?Hash Table / rbtree
범위 삽입/삭제가 빈번한가?Interval Tree + lock/RCU 설계
실무 팁: interval 노드의 __subtree_last 갱신 누락은 겹침 검색 누락으로 이어집니다. 삽입/삭제 경로에서 augment 콜백 호출 여부를 반드시 확인하세요.

augmented max 전파 메커니즘 심화

Interval Tree의 핵심은 __subtree_last 값이 삽입, 삭제, 회전 시마다 정확하게 전파(propagate)되는 메커니즘입니다. 이 섹션에서는 전파 과정을 단계별로 분석합니다.

전파 콜백 상세 분석

augment_callbacks 구조체는 세 가지 콜백을 정의합니다:

/* include/linux/interval_tree_generic.h */
/* INTERVAL_TREE_DEFINE 매크로 내부에서 생성되는 propagate 콜백 */

/* RB_DECLARE_CALLBACKS_MAX 매크로로 propagate 구현 */
static inline void
ITPREFIX##_augment_propagate(struct rb_node *rb, struct rb_node *stop)
{
    while (rb != stop) {
        ITSTRUCT *node = rb_entry(rb, ITSTRUCT, ITRB);
        ITTYPE subtree_last = ITLAST(node);

        /* 왼쪽 자식의 __subtree_last와 비교 */
        if (node->ITRB.rb_left) {
            ITSTRUCT *left = rb_entry(node->ITRB.rb_left,
                                         ITSTRUCT, ITRB);
            if (left->ITSUBTREE > subtree_last)
                subtree_last = left->ITSUBTREE;
        }

        /* 오른쪽 자식의 __subtree_last와 비교 */
        if (node->ITRB.rb_right) {
            ITSTRUCT *right = rb_entry(node->ITRB.rb_right,
                                          ITSTRUCT, ITRB);
            if (right->ITSUBTREE > subtree_last)
                subtree_last = right->ITSUBTREE;
        }

        /* 조기 종료: 값이 변하지 않으면 상위 전파 불필요 */
        if (node->ITSUBTREE == subtree_last)
            break;

        node->ITSUBTREE = subtree_last;
        rb = rb_parent(&node->ITRB);
    }
}
ℹ️

조기 종료 최적화: propagate 콜백에서 __subtree_last 값이 변하지 않으면 즉시 루프를 종료합니다. 이는 대부분의 경우 삽입/삭제된 노드에서 루트까지 전체를 순회하지 않고 중간에 멈출 수 있음을 의미합니다.

__subtree_last 전파: [12,40] 삽입 시 경로 전파 [20, 30] __st_last: 30→40 [10, 15] __st_last: 15→40 [35, 38] __st_last: 38 [12, 40] NEW __st_last: 40 propagate propagate 전파 과정 단계별 분석 Step 1: [12,40] 삽입 → __subtree_last = 40 (자기 자신의 last) Step 2: 부모 [10,15]로 전파 → max(15, 40) = 40 → __subtree_last 갱신 Step 3: 루트 [20,30]로 전파 → max(30, 40, 38) = 40 → __subtree_last 갱신 Stop: 루트에 도달하여 전파 종료 (rb_parent == NULL이므로 stop) 핵심: [10,15] 노드에서 이미 __subtree_last=40이면 propagate는 조기 종료되지 않음 (값이 변경되었으므로) 하지만 우측 서브트리 [35,38]은 전혀 방문하지 않음 → O(log n) 보장

회전 시 증강 필드 재계산

Red-Black Tree 회전은 부모-자식 관계를 변경하므로, 증강 필드도 재계산해야 합니다:

/* rotate 콜백: 회전 후 old 노드와 new 노드의 증강 필드 재계산 */
static inline void
ITPREFIX##_augment_rotate(struct rb_node *rb_old,
                            struct rb_node *rb_new)
{
    ITSTRUCT *old = rb_entry(rb_old, ITSTRUCT, ITRB);
    ITSTRUCT *new = rb_entry(rb_new, ITSTRUCT, ITRB);

    /* new가 old의 부모 위치를 대체함 */
    new->ITSUBTREE = old->ITSUBTREE;

    /* old는 이제 new의 자식 → old의 증강 필드 재계산 */
    ITTYPE subtree_last = ITLAST(old);
    if (old->ITRB.rb_left) {
        ITSTRUCT *left = rb_entry(old->ITRB.rb_left,
                                     ITSTRUCT, ITRB);
        if (left->ITSUBTREE > subtree_last)
            subtree_last = left->ITSUBTREE;
    }
    if (old->ITRB.rb_right) {
        ITSTRUCT *right = rb_entry(old->ITRB.rb_right,
                                      ITSTRUCT, ITRB);
        if (right->ITSUBTREE > subtree_last)
            subtree_last = right->ITSUBTREE;
    }
    old->ITSUBTREE = subtree_last;
}
💡

회전의 핵심: 회전 후 new 노드는 old의 서브트리를 포함하므로 new->__subtree_last = old->__subtree_last로 복사하고, old만 자식 기반으로 재계산합니다. 이 O(1) 최적화가 전체 회전을 O(1)에 유지합니다.

가지치기(Pruning) 최적화

검색 시 __subtree_last를 활용한 가지치기는 Interval Tree의 핵심 성능 비결입니다:

가지치기 최적화: query=[50,60] 검색 시 건너뛰기 [30, 35] __st_last: 45 < 50 [10, 20] __st_last: 25 [5, 8] [15, 25] [40, 45] __st_last: 45 PRUNED! 3개 노드 검사 생략 검색 경로 → 가지치기 판단 로직 1. 루트 [30,35]: __subtree_last(45) < query_start(50) → 왼쪽 서브트리 전체 건너뜀 2. 루트 자체도 겹치지 않음: node.last(35) < query_start(50) 3. 오른쪽으로 이동: [40,45] 검사 → 겹치지 않음 (45 < 50) 결과: 전체 5개 노드 중 2개만 검사 (60% 절약) → O(log n) 보장

구현 내부 구조 분석

Interval Tree의 내부 구현은 include/linux/interval_tree_generic.h에 정의된 INTERVAL_TREE_DEFINE 매크로를 중심으로 이루어집니다. 이 매크로가 타입별 함수를 자동 생성하는 메커니즘을 분석합니다.

interval_tree_node 메모리 레이아웃

interval_tree_node 메모리 레이아웃 (64-bit) struct interval_tree_node struct rb_node rb __rb_parent_color (8 bytes) — 부모 포인터 + 색상 비트 rb_right (8 bytes) — 오른쪽 자식 포인터 rb_left (8 bytes) — 왼쪽 자식 포인터 start (8 bytes) — 구간 시작 (정렬 키) last (8 bytes) — 구간 끝 (inclusive) __subtree_last (8 bytes) — 서브트리 최대 last (증강) +0 +8 +16 +24 +32 +40 rb_entry() / container_of() rb_node *rb = ...; struct interval_tree_node *node = rb_entry(rb, struct interval_tree_node, rb); → (struct interval_tree_node *)((char*)rb - 0) struct rb_root_cached rb_root.rb_node — 루트 노드 포인터 rb_leftmost — 최소 start 노드 캐싱 메모리 크기 요약 interval_tree_node: 48 bytes (rb_node 24B + start 8B + last 8B + __subtree_last 8B) rb_root_cached: 16 bytes (rb_node* 8B + rb_leftmost* 8B) — leftmost 캐싱으로 최소값 O(1) 접근

INTERVAL_TREE_DEFINE 매크로 확장

INTERVAL_TREE_DEFINE 매크로는 6개의 핵심 함수를 자동 생성합니다:

/* include/linux/interval_tree_generic.h */
/*
 * INTERVAL_TREE_DEFINE(ITSTRUCT, ITRB, ITTYPE, ITSUBTREE,
 *                      ITSTART, ITLAST, ITSTATIC, ITPREFIX)
 *
 * 매크로 인자 설명:
 *   ITSTRUCT  — 노드 구조체 타입 (struct interval_tree_node)
 *   ITRB      — rb_node 필드명 (rb)
 *   ITTYPE    — 구간 값 타입 (unsigned long)
 *   ITSUBTREE — 증강 필드명 (__subtree_last)
 *   ITSTART   — start 접근 매크로
 *   ITLAST    — last 접근 매크로
 *   ITSTATIC  — 함수 접두 지정자 (static 등)
 *   ITPREFIX  — 생성 함수 접두사 (interval_tree)
 */

/* 기본 Interval Tree 정의 (lib/interval_tree.c) */
#define START(node) ((node)->start)
#define LAST(node)  ((node)->last)

INTERVAL_TREE_DEFINE(struct interval_tree_node, rb,
    unsigned long, __subtree_last,
    START, LAST, static inline, interval_tree)

/* 매크로 확장 결과 (생성되는 함수들): */
/*
 * interval_tree_insert(node, root)
 * interval_tree_remove(node, root)
 * interval_tree_iter_first(root, start, last)
 * interval_tree_iter_next(node, start, last)
 * interval_tree_subtree_search(node, start, last)  -- 내부용
 * interval_tree_augment_propagate/copy/rotate       -- 콜백
 */
ℹ️

커스텀 정의: 매크로의 ITSTART/ITLAST 인자를 변경하면 vm_area_struct처럼 start/last 이름이 다른 구조체에도 Interval Tree를 적용할 수 있습니다. 예: anon_vma_interval_tree.

iter_first / iter_next 내부 동작

interval_tree_iter_first()는 트리에서 질의 구간과 겹치는 첫 번째 노드를 찾고, interval_tree_iter_next()는 그 다음 겹치는 노드를 순회합니다:

/* interval_tree_iter_first 의사코드 (매크로 확장) */
struct interval_tree_node *
interval_tree_iter_first(struct rb_root_cached *root,
                           unsigned long start, unsigned long last)
{
    struct interval_tree_node *node, *leftmost;
    struct rb_node *rb;

    if (!root->rb_root.rb_node)
        return NULL;

    /* 루트에서 시작하여 겹치는 노드 탐색 */
    rb = root->rb_root.rb_node;
    node = rb_entry(rb, struct interval_tree_node, rb);

    /* subtree_search: 증강 필드 기반 가지치기 탐색 */
    while (true) {
        /* 이 서브트리에 겹치는 구간이 없으면 종료 */
        if (node->__subtree_last < start)
            return NULL;

        /* 왼쪽 서브트리에 겹치는 구간이 있을 수 있음 */
        if (node->rb.rb_left) {
            struct interval_tree_node *left =
                rb_entry(node->rb.rb_left,
                         struct interval_tree_node, rb);
            if (left->__subtree_last >= start) {
                node = left;
                continue;
            }
        }

        /* 현재 노드가 겹치는지 확인 */
        if (node->start <= last)  /* 겹침 조건 */
            return node;

        /* 오른쪽 서브트리로 이동 */
        if (node->rb.rb_right) {
            node = rb_entry(node->rb.rb_right,
                             struct interval_tree_node, rb);
        } else {
            return NULL;
        }
    }
}

/* interval_tree_iter_next: in-order 순회로 다음 겹치는 노드 찾기 */
struct interval_tree_node *
interval_tree_iter_next(struct interval_tree_node *node,
                          unsigned long start, unsigned long last)
{
    struct rb_node *rb = node->rb.rb_right, *prev;

    while (true) {
        /* 오른쪽 서브트리에서 겹치는 노드 탐색 */
        if (rb) {
            node = rb_entry(rb, struct interval_tree_node, rb);
            if (node->__subtree_last >= start) {
                /* subtree_search로 재귀적 탐색 */
                return interval_tree_subtree_search(
                    node, start, last);
            }
        }

        /* in-order successor 방향으로 이동 */
        do {
            rb = rb_parent(&node->rb);
            if (!rb)
                return NULL;
            prev = &node->rb;
            node = rb_entry(rb, struct interval_tree_node, rb);
            rb = node->rb.rb_right;
        } while (prev == rb);

        /* start 기준 정렬이므로 query.last 초과 시 종료 */
        if (node->start > last)
            return NULL;

        /* 현재 노드가 겹치면 반환 */
        if (node->last >= start)
            return node;
    }
}
⚠️

iter_next의 종료 조건: node->start > last이면 이후 모든 노드의 start도 last보다 크므로 (in-order 순서) 더 이상 겹치는 구간이 없습니다. 이 조건이 O(log n + k) 복잡도에서 k 이후 빠르게 종료되는 핵심입니다.

VMA 겹침 탐지 심화

Virtual Memory Area(VMA) 관리에서 Interval Tree는 주소 공간 겹침 탐지의 핵심 메커니즘입니다. 이 섹션에서는 find_vma_intersection()과 익명 VMA(anon_vma)의 interval tree 활용을 분석합니다.

VMA 주소 공간: find_vma_intersection(mm, 0x3000, 0x5000) 0x0000 0xFFFF 코드 영역 [0x1000, 0x2000) 힙 영역 [0x2000, 0x4000) HIT! 공유 라이브러리 [0x4000, 0x6000) HIT! 스택 [0x7000, 0x8000) Query: [0x3000, 0x5000) find_vma_intersection() 겹침 판정 겹침 조건: vma->vm_start < end_addr AND vma->vm_end > start_addr VMA [0x1000,0x2000): 0x1000 < 0x5000 AND 0x2000 > 0x3000? → 0x2000 > 0x3000 = FALSE VMA [0x2000,0x4000): 0x2000 < 0x5000 AND 0x4000 > 0x3000? → TRUE (겹침!) VMA [0x4000,0x6000): 0x4000 < 0x5000 AND 0x6000 > 0x3000? → TRUE (겹침!) Interval Tree 내부 구조 (start 기준 정렬) [0x4000, 0x6000) __st_last: 0x8000 [0x1000, 0x2000) __st_last: 0x4000 [0x2000, 0x4000) __st_last: 0x4000 [0x7000, 0x8000) __st_last: 0x8000

find_vma_intersection() 코드 분석

/* include/linux/mm.h */
static inline struct vm_area_struct *
find_vma_intersection(struct mm_struct *mm,
                       unsigned long start_addr,
                       unsigned long end_addr)
{
    struct vm_area_struct *vma;

    /* find_vma: addr 이상의 vm_end를 가진 첫 VMA */
    vma = find_vma(mm, start_addr);

    /* 겹침 판정:
     * vma->vm_start < end_addr  (VMA 시작이 질의 끝 이전)
     * vma->vm_end > start_addr  (find_vma 보장)
     */
    if (vma && vma->vm_start < end_addr)
        return vma;

    return NULL;
}

anon_vma Interval Tree

익명 페이지(anonymous page)의 역방향 매핑(reverse mapping)에서도 Interval Tree가 핵심적으로 사용됩니다:

/* include/linux/rmap.h */
struct anon_vma_chain {
    struct vm_area_struct *vma;
    struct anon_vma *anon_vma;
    struct list_head same_vma;       /* VMA별 체인 */
    struct rb_node rb;               /* interval tree 노드 */
    unsigned long rb_subtree_last;   /* 증강 필드 */
};

/* mm/rmap.c - anon_vma 전용 Interval Tree 정의 */
INTERVAL_TREE_DEFINE(struct anon_vma_chain, rb,
    unsigned long, rb_subtree_last,
    avc_start_pgoff, avc_last_pgoff,
    static inline, anon_vma_interval_tree)

/* 페이지 회수 시 역방향 매핑 검색 */
static bool page_referenced_anon(
    struct folio *folio,
    struct mem_cgroup *memcg,
    unsigned long *vm_flags)
{
    struct anon_vma *anon_vma = folio->mapping;
    struct anon_vma_chain *avc;
    pgoff_t pgoff_start, pgoff_end;

    /* 이 페이지를 매핑하는 모든 VMA를 interval tree로 검색 */
    pgoff_start = folio->index;
    pgoff_end = pgoff_start + folio_nr_pages(folio) - 1;

    anon_vma_interval_tree_foreach(avc,
        &anon_vma->rb_root, pgoff_start, pgoff_end) {

        struct vm_area_struct *vma = avc->vma;
        /* vma에서 페이지 참조 여부 확인 */
        page_referenced_one(folio, vma, ...);
    }
}
ℹ️

역방향 매핑의 핵심: 하나의 물리 페이지가 fork()를 통해 여러 프로세스에 공유될 수 있습니다. 페이지 회수(reclaim) 시 이 페이지를 매핑하는 모든 VMA를 찾아야 하는데, anon_vma_interval_tree가 이를 O(log n + k)에 수행합니다. 선형 검색이면 fork가 많은 서버에서 심각한 지연이 발생합니다.

Reverse Mapping 흐름

/* 페이지 회수 전체 흐름에서 interval tree 역할 */

/* 1. kswapd가 메모리 부족 감지 */
kswapd()
  → balance_pgdat()
    → shrink_node()
      → shrink_folio_list()
        → folio_referenced()
          → folio_referenced_anon()
            → anon_vma_interval_tree_foreach()  /* ← Interval Tree 검색 */page_referenced_one()
                → ptep_clear_flush_young()  /* PTE accessed 비트 확인 */

/* 2. interval tree가 없다면?
 * → anon_vma에 연결된 모든 VMA를 선형 탐색
 * → fork 수가 많을수록 O(n) 비용 급증
 * → 페이지 회수 지연 → OOM 위험 증가
 */

DRM/GPU 드라이버 활용

DRM(Direct Rendering Manager) 서브시스템에서는 GPU 메모리 주소 공간 관리에 Interval Tree를 활용합니다. VRAM 할당, GEM 버퍼 객체 관리, TTM(Translation Table Manager) 등에서 핵심적으로 사용됩니다.

DRM drm_mm: GPU VRAM 주소 공간 관리 GPU VRAM (예: 8GB) 0x0 0x200000000 프레임버퍼 A (128MB) 텍스처 B (64MB) 컴퓨트 버퍼 C (256MB) 커서 D (32MB) hole hole hole struct drm_mm_node unsigned long start — 할당 시작 주소 unsigned long size — 할당 크기 struct rb_node rb — interval tree 노드 unsigned long __subtree_last — 증강 필드 struct drm_mm (메모리 매니저) interval_tree (할당 노드) → 겹침 검사, 주소 범위 검색 hole_stack (빈 공간 관리) → Best-fit / First-fit 할당 head_node (연결 리스트) → 전체 노드 순회 DRM Interval Tree 활용 사례 GEM 버퍼 객체: GPU 커맨드 제출 시 버퍼 주소 범위 충돌 검사 TTM 관리: VRAM↔시스템 메모리 이동(eviction) 시 겹치는 할당 검색 VM 바인딩: GPU 페이지 테이블 매핑 범위 관리 및 충돌 감지 디스플레이: 스캔아웃 버퍼 핀(pin) 시 VRAM 범위 예약 및 보호

drm_mm 코드 분석

/* drivers/gpu/drm/drm_mm.c */

/* drm_mm 전용 interval tree 정의 */
#define START(node) ((node)->start)
#define LAST(node)  ((node)->start + (node)->size - 1)

INTERVAL_TREE_DEFINE(struct drm_mm_node, rb,
    u64, __subtree_last,
    START, LAST, static inline, drm_mm_interval_tree)

/* 할당된 노드 삽입 */
static void drm_mm_interval_tree_add_node(
    struct drm_mm_node *hole_node,
    struct drm_mm_node *node)
{
    struct drm_mm *mm = node->mm;

    node->__subtree_last = LAST(node);
    drm_mm_interval_tree_insert(node, &mm->interval_tree);
}

/* 범위 겹침 검사: 새 할당 전 충돌 확인 */
struct drm_mm_node *
__drm_mm_interval_first(const struct drm_mm *mm,
                         u64 start, u64 last)
{
    return drm_mm_interval_tree_iter_first(
        (struct rb_root_cached *)&mm->interval_tree,
        start, last);
}

/* GPU 드라이버에서의 사용 예 (i915) */
static int i915_gem_evict_something(
    struct i915_address_space *vm,
    u64 min_size, u64 alignment)
{
    struct drm_mm_node *node;

    /* interval tree로 요청 범위와 겹치는 모든 할당 찾기 */
    drm_mm_for_each_node_in_range(node, &vm->mm,
                                    target_start, target_end) {
        /* 겹치는 버퍼를 VRAM에서 시스템 메모리로 이동(evict) */
        i915_gem_object_unbind(node->bo, ...);
    }
}
💡

GPU 메모리 관리의 특수성: GPU VRAM은 CPU 메모리와 달리 파편화(fragmentation)가 심하고, 대용량 연속 할당이 필요합니다. Interval Tree로 빈 공간과 할당 영역을 효율적으로 추적하면, eviction 대상 선택을 O(log n + k)에 수행할 수 있어 GPU 성능에 직접적인 영향을 줍니다.

GPU 메모리 eviction 흐름

GPU VRAM이 부족할 때, DRM 서브시스템은 interval tree를 활용하여 eviction 대상을 효율적으로 선택합니다:

/* drivers/gpu/drm/drm_mm.c */

/* 범위 내 모든 할당 노드 순회 매크로 */
#define drm_mm_for_each_node_in_range(node__, mm__, start__, end__)  \
    for (node__ = __drm_mm_interval_first((mm__), (start__),     \
                                            (end__) - 1);       \
         node__ && node__->start < (end__);                      \
         node__ = list_next_entry(node__, node_list))

/* eviction 예: VRAM 공간 확보 */
static int drm_mm_scan_add_block(
    struct drm_mm_scan *scan,
    struct drm_mm_node *node)
{
    struct drm_mm *mm = scan->mm;
    struct drm_mm_node *hole;
    u64 adj_start, adj_end;

    /* node를 evict한다고 가정하고 결과 hole 크기 계산 */
    adj_start = node->start;
    adj_end = node->start + node->size;

    /* 인접 hole과 병합 가능 여부 확인 */
    hole = list_prev_entry(node, node_list);
    if (hole->allocated == 0)
        adj_start = hole->start;

    hole = list_next_entry(node, node_list);
    if (hole->allocated == 0)
        adj_end = hole->start + hole->size;

    /* 충분한 공간이 확보되는지 확인 */
    if (adj_end - adj_start >= scan->size) {
        scan->hit_start = adj_start;
        scan->hit_end = adj_end;
        return true;
    }

    return false;
}

/* 전체 eviction 흐름 */
/*
 * 1. drm_mm_scan_init() — 스캔 초기화 (요청 크기, 정렬)
 * 2. 우선순위별로 evict 후보 순회:
 *    for_each_candidate:
 *      drm_mm_scan_add_block(scan, node)
 *      → interval tree에서 node 주변 hole 검사
 *      → 충분한 연속 공간 확보 시 true 반환
 * 3. drm_mm_scan_remove_block() — evict 대상 확정
 * 4. 실제 eviction 수행 (GPU 메모리 → 시스템 메모리)
 * 5. drm_mm_insert_node() — 새 할당 수행
 */

GEM/TTM 버퍼 객체 관리

구성요소 Interval Tree 역할 주요 연산
GEM (Graphics Execution Manager) GPU 주소 공간에서 버퍼 객체 위치 추적 할당/해제/겹침 검사
TTM (Translation Table Manager) VRAM ↔ 시스템 메모리 이동 시 범위 관리 eviction 대상 선택
GPU VM (가상 메모리) GPU 페이지 테이블 매핑 범위 관리 VA 범위 충돌 감지
Fence/Sync 동기화 범위 추적 (간접 사용) 의존성 범위 검색
ℹ️

드라이버별 구현: i915(Intel), amdgpu(AMD), nouveau(NVIDIA)는 각각 drm_mm을 기반으로 자체 메모리 관리자를 구현합니다. 공통적으로 drm_mm_insert_node_in_range()로 특정 범위 내에서 빈 공간을 검색하며, 이때 interval tree가 O(log n + k) 검색을 보장합니다.

io_uring 활용

io_uring은 고성능 비동기 I/O 인터페이스로, 등록된 버퍼(registered buffers)의 범위 관리에 interval tree 개념을 활용합니다.

고정 버퍼 관리

io_uring에서 IORING_REGISTER_BUFFERS로 등록된 버퍼는 커널이 미리 핀(pin)해두어 I/O 시마다 페이지 테이블 잠금과 GUP(get_user_pages)를 생략할 수 있습니다. 이때 버퍼 간 겹침 검사와 범위 관리가 필요합니다:

/* io_uring/rsrc.c */

/* 등록 버퍼 구조체 */
struct io_mapped_ubuf {
    u64    ubuf;       /* 사용자 공간 버퍼 주소 */
    u64    ubuf_end;   /* 버퍼 끝 주소 */
    unsigned int nr_bvecs;
    struct bio_vec bvec[];  /* 핀된 페이지 정보 */
};

/* 고정 버퍼 등록 흐름 */
static int io_sqe_buffers_register(
    struct io_ring_ctx *ctx,
    void __user *arg, unsigned int nr_args)
{
    struct io_mapped_ubuf *imu;
    int i;

    for (i = 0; i < nr_args; i++) {
        struct iovec iov;

        /* 사용자 공간 버퍼 정보 복사 */
        copy_from_user(&iov, arg + i, sizeof(iov));

        /* 버퍼 범위 설정 */
        imu->ubuf = (u64)iov.iov_base;
        imu->ubuf_end = imu->ubuf + iov.iov_len;

        /* 페이지 핀(pin): GUP으로 물리 페이지 고정 */
        io_buffer_account_pin(ctx, imu, ...);
    }
}

/* I/O 제출 시 등록 버퍼 검색 */
static struct io_mapped_ubuf *
io_buffer_select(struct io_ring_ctx *ctx,
                  u64 buf_addr, size_t len,
                  unsigned int buf_index)
{
    struct io_mapped_ubuf *imu;

    /* 인덱스로 직접 접근 (O(1)) */
    imu = ctx->user_bufs[buf_index];

    /* 범위 검증: 요청 범위가 등록 버퍼 내에 있는지 확인 */
    if (buf_addr < imu->ubuf ||
        buf_addr + len > imu->ubuf_end)
        return ERR_PTR(-EFAULT);

    return imu;
}
ℹ️

io_uring과 interval tree의 관계: io_uring의 고정 버퍼는 인덱스 기반 O(1) 접근을 주로 사용하지만, 내부적으로 등록된 버퍼들의 페이지 범위가 VMA interval tree와 상호작용합니다. 예를 들어, io_pin_pages()에서 GUP으로 페이지를 핀할 때 해당 주소 범위의 VMA를 interval tree로 검색합니다.

io_uring ↔ VMA Interval Tree 상호작용

/* io_uring이 VMA interval tree를 사용하는 경로 */

/* 1. 버퍼 등록 시 페이지 핀 */
io_sqe_buffers_register()
  → io_buffer_account_pin()
    → pin_user_pages()           /* GUP */__get_user_pages()
        → find_vma(mm, start)    /* ← interval tree 검색! */follow_page_mask()

/* 2. 직접 I/O 시 버퍼 매핑 확인 */
io_read() / io_write()
  → io_import_iovec()
    → io_buffer_select()        /* 등록 버퍼 인덱스 참조 */kiocb_set_rw_flags()
  → call_read_iter() / call_write_iter()
    → direct_IO()
      → bio_iov_iter_get_pages()  /* 이미 핀된 페이지 사용 */
💡

성능 영향: io_uring의 고정 버퍼는 등록 시점에 한 번만 GUP(→VMA interval tree 검색)을 수행하고, 이후 I/O마다 VMA 검색을 생략합니다. 초당 수백만 IOPS 환경에서 이 최적화는 CPU 오버헤드를 10-30% 줄여줍니다.

리소스 등록/해제 생명주기

io_uring의 리소스(버퍼, 파일) 등록과 해제 과정에서 VMA interval tree와의 상호작용을 단계별로 분석합니다:

/* io_uring 리소스 관리 생명주기 */

/* 1. 등록 (IORING_REGISTER_BUFFERS) */
io_uring_register()
  → io_sqe_buffers_register(ctx, arg, nr_args)
    → for each buffer:
        → io_buffer_validate(iov)      /* 크기/정렬 검증 */io_sqe_buffer_register(ctx, iov, &imu)
          → pin_user_pages_fast(ubuf, nr_pages, FOLL_WRITE, pages)
            /* ↑ 내부에서 find_vma() → interval tree 검색 */bio_vec 배열에 핀된 페이지 정보 저장
        → ctx->user_bufs[i] = imu

/* 2. I/O 수행 시 (zero-copy 경로) */
io_submit_sqe()
  → io_read() / io_write()
    → io_import_fixed(req, rw, iter, issue_flags)
      → imu = ctx->user_bufs[req->buf_index]  /* O(1) 인덱스 접근 */iov_iter_bvec(iter, rw, imu->bvec, ...)
      /* ↑ VMA interval tree 검색 생략! (이미 핀됨) */

/* 3. 해제 (IORING_UNREGISTER_BUFFERS) */
io_uring_register()
  → io_sqe_buffers_unregister(ctx)
    → for each registered buffer:
        → io_buffer_unmap(ctx, &imu)
          → unpin_user_pages(pages, nr_pages)
          → kfree(imu)

io_uring 버퍼 등록 성능 비교

시나리오 VMA 검색 횟수 I/O당 오버헤드 초당 IOPS (NVMe)
미등록 버퍼 (일반 read/write) 매 I/O마다 1회 ~2μs (GUP) ~800K
등록 버퍼 (IORING_REGISTER_BUFFERS) 등록 시 1회만 ~0.1μs (인덱스 참조) ~1.5M
고정 파일 (IORING_REGISTER_FILES) 등록 시 VMA 무관 ~0.05μs (fdget 생략) ~2M
⚠️

주의: 등록 버퍼의 핀된 페이지는 해제될 때까지 시스템 메모리에 고정됩니다. 대량의 버퍼를 등록하면 메모리 압박(memory pressure)이 증가하고, 페이지 회수(reclaim) 대상에서 제외되므로 OOM 위험이 높아집니다. RLIMIT_MEMLOCK 제한을 적절히 설정해야 합니다.

KVM 메모리 슬롯 관리

KVM(Kernel-based Virtual Machine)에서는 게스트 물리 주소(GPA)와 호스트 가상 주소(HVA) 간의 매핑을 관리하기 위해 interval tree 기반의 메모리 슬롯 구조를 사용합니다.

KVM 메모리 슬롯: GPA → HVA 매핑 (Interval Tree 기반) Guest Physical Address (GPA) Space 0x0 슬롯 0: RAM (1GB) GPA: 0x0 ~ 0x40000000 슬롯 1: MMIO APIC 영역 슬롯 2: RAM (3GB, 4G 경계 위) GPA: 0x100000000 ~ 0x1C0000000 슬롯 3: PCI BAR 매핑 userspace_addr userspace_addr Host Virtual Address (HVA) Space — QEMU 프로세스 mmap 영역 mmap'd region struct kvm_memslots gfn_tree (rb_root) — GPA 기반 interval tree hva_tree (rb_root_cached) — HVA 기반 interval tree id_to_index[] — 슬롯 ID로 직접 접근 node_idx — 활성 슬롯 인덱스 배열 struct kvm_memory_slot base_gfn — 게스트 프레임 번호 시작 npages — 슬롯 크기 (페이지 단위) userspace_addr — 호스트 가상 주소 (HVA) gfn_node[2] — GPA interval tree 노드 hva_node[2] — HVA interval tree 노드 dirty_bitmap — 더티 페이지 추적 GPA 검색 흐름 (VM exit → 메모리 슬롯 검색) EPT violation → kvm_mmu_page_fault() → kvm_vcpu_gfn_to_memslot() → search_memslots(gfn_tree, gfn) → O(log n) interval tree 검색

KVM 메모리 슬롯 코드 분석

/* include/linux/kvm_host.h */
struct kvm_memory_slot {
    gfn_t base_gfn;          /* Guest Frame Number 시작 */
    unsigned long npages;     /* 페이지 수 */
    unsigned long *dirty_bitmap;
    unsigned long userspace_addr;  /* HVA */
    u32 flags;
    short id;

    /* GPA 기반 interval tree 노드 */
    struct {
        struct rb_node gfn_node;
        gfn_t gfn_subtree_last;   /* 증강 필드 */
    } gfn_node[2];  /* [0]: active, [1]: inactive */

    /* HVA 기반 interval tree 노드 */
    struct {
        struct rb_node hva_node;
        unsigned long hva_subtree_last;
    } hva_node[2];
};

/* virt/kvm/kvm_main.c */
/* GPA → 메모리 슬롯 검색 */
static inline struct kvm_memory_slot *
search_memslots(struct kvm_memslots *slots, gfn_t gfn)
{
    struct rb_node *node;

    /* gfn_tree에서 interval tree 검색 */
    node = slots->gfn_tree.rb_node;
    while (node) {
        struct kvm_memory_slot *slot;

        slot = container_of(node, struct kvm_memory_slot,
                              gfn_node[slots->node_idx].gfn_node);

        if (gfn < slot->base_gfn)
            node = node->rb_left;
        else if (gfn >= slot->base_gfn + slot->npages)
            node = node->rb_right;
        else
            return slot;  /* 찾음! */
    }
    return NULL;
}

/* HVA 변경 통지 (mmu_notifier) 시 HVA interval tree 활용 */
static int kvm_mmu_notifier_invalidate_range_start(
    struct mmu_notifier *mn,
    const struct mmu_notifier_range *range)
{
    struct kvm *kvm = container_of(mn, struct kvm, mmu_notifier);
    struct kvm_memory_slot *slot;

    /* HVA 범위와 겹치는 모든 메모리 슬롯 찾기 */
    kvm_for_each_memslot_in_hva_range(kvm,
        range->start, range->end, slot) {
        /* 해당 슬롯의 SPT(Shadow Page Table) 무효화 */
        kvm_mmu_invalidate_range(kvm, slot,
            range->start, range->end);
    }
}
⚠️

이중 interval tree: KVM은 GPA와 HVA에 각각 별도의 interval tree를 유지합니다. GPA tree는 VM exit 처리(EPT violation) 시 사용하고, HVA tree는 호스트 메모리 이벤트(mmu_notifier) 처리 시 사용합니다. gfn_node[2]/hva_node[2]의 배열 인덱스 2는 active/inactive 슬롯 세트를 위한 것으로, 슬롯 업데이트 시 RCU 없이도 안전한 전환을 가능하게 합니다.

메모리 슬롯 생명주기

KVM 메모리 슬롯의 생성부터 삭제까지 interval tree와의 상호작용을 분석합니다:

/* virt/kvm/kvm_main.c */

/* 메모리 슬롯 추가/변경 */
static int kvm_set_memslot(
    struct kvm *kvm,
    struct kvm_memory_slot *new,
    enum kvm_mr_change change)
{
    struct kvm_memslots *slots;

    /* 1. 슬롯 충돌 검사 (interval tree 기반) */
    if (change == KVM_MR_CREATE) {
        struct kvm_memory_slot *existing;

        /* GPA 범위가 기존 슬롯과 겹치는지 확인 */
        kvm_for_each_memslot_in_gfn_range(&iter,
            slots, new->base_gfn,
            new->base_gfn + new->npages) {
            existing = iter.slot;
            if (existing->id != new->id)
                return -EEXIST;  /* 충돌! */
        }
    }

    /* 2. inactive 슬롯에 변경 적용 */
    kvm_swap_active_memslots(kvm, ...);

    /* 3. interval tree에서 이전 슬롯 제거 */
    if (change == KVM_MR_DELETE || change == KVM_MR_MOVE)
        kvm_erase_gfn_node(slots, old);

    /* 4. interval tree에 새 슬롯 삽입 */
    if (change != KVM_MR_DELETE)
        kvm_insert_gfn_node(slots, new);

    /* 5. 활성 슬롯 전환 (RCU 유사 메커니즘) */
    kvm_swap_active_memslots(kvm, ...);

    return 0;
}

KVM에서 interval tree 성능 영향

이벤트 빈도 Interval Tree 역할 성능 영향
EPT violation 매우 높음 (페이지 폴트) GPA→슬롯 검색 VM exit 지연의 5-10%
MMIO 접근 높음 (디바이스 I/O) MMIO 슬롯 식별 에뮬레이션 시작 지연
mmu_notifier 중간 (호스트 메모리 이벤트) HVA→슬롯 역검색 SPT 무효화 범위 결정
슬롯 변경 매우 낮음 (설정 변경) 삽입/삭제 무시 가능

ftrace/perf 디버깅 실습

Interval Tree 관련 커널 동작을 실시간으로 추적하고 성능을 측정하는 방법을 다룹니다.

ftrace로 interval_tree 함수 추적

# interval_tree 관련 함수 목록 확인
cat /sys/kernel/debug/tracing/available_filter_functions | grep interval_tree

# function tracer 설정
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo function > /sys/kernel/debug/tracing/current_tracer

# interval_tree 함수들만 필터링
echo 'interval_tree_*' > /sys/kernel/debug/tracing/set_ftrace_filter

# function graph 모드로 호출 관계까지 확인
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 'interval_tree_insert interval_tree_remove' \
  > /sys/kernel/debug/tracing/set_graph_function

# 추적 시작
echo 1 > /sys/kernel/debug/tracing/tracing_on

# ... 워크로드 실행 (mmap/munmap 호출 등) ...

# 결과 확인
cat /sys/kernel/debug/tracing/trace

# 출력 예시:
#  2)               |  interval_tree_insert() {
#  2)   0.450 us    |    rb_insert_augmented_cached();
#  2)   1.230 us    |  }
#  1)   0.380 us    |  interval_tree_iter_first();
#  1)   0.210 us    |  interval_tree_iter_next();

bpftrace로 VMA interval tree 관찰

#!/usr/bin/env bpftrace
/* vma_interval_tree_operations.bt
 * VMA interval tree 삽입/삭제 추적
 */

/* find_vma 호출 빈도 및 지연 측정 */
kprobe:find_vma
{
    @start[tid] = nsecs;
    @find_vma_count = count();
}

kretprobe:find_vma
/@start[tid]/
{
    $duration = nsecs - @start[tid];
    @find_vma_latency_ns = hist($duration);
    delete(@start[tid]);
}

/* VMA 삽입/삭제 시 프로세스 정보 */
kprobe:vma_interval_tree_insert
{
    printf("INSERT vma pid=%d comm=%s\n", pid, comm);
}

kprobe:vma_interval_tree_remove
{
    printf("REMOVE vma pid=%d comm=%s\n", pid, comm);
}

/* 10초 후 통계 출력 */
interval:s:10
{
    printf("\n--- find_vma 지연 분포 (ns) ---\n");
    print(@find_vma_latency_ns);
    printf("총 호출 횟수: ");
    print(@find_vma_count);
}

perf를 이용한 성능 측정

# perf probe로 interval_tree_iter_first에 동적 트레이스포인트 추가
perf probe -a 'interval_tree_iter_first root start last'
perf probe -a 'interval_tree_iter_first%return $retval'

# 10초간 샘플링
perf record -e probe:interval_tree_iter_first \
           -e probe:interval_tree_iter_first__return \
           -ag -- sleep 10

# 결과 분석
perf report --stdio

# perf stat으로 간단한 통계
perf stat -e cache-misses,cache-references,instructions \
         -e 'probe:interval_tree_iter_first' \
         -- stress-ng --vm 4 --vm-bytes 1G --timeout 10s

# 출력 예시:
#  Performance counter stats:
#    12,345    cache-misses     # 2.3% of all cache refs
#   536,890    cache-references
# 8,234,567    instructions
#     4,567    probe:interval_tree_iter_first

# 프로브 정리
perf probe -d 'interval_tree_iter_first*'
💡

실무 팁: bpftrace의 hist()로 find_vma 지연 분포를 확인하면, 대부분의 호출이 1μs 미만이지만 간헐적으로 10μs 이상 걸리는 경우(캐시 미스, 트리 깊이가 깊은 경우)를 발견할 수 있습니다. 이런 outlier가 tail latency의 원인이 됩니다.

성능 분석과 최적화

Interval Tree의 이론적 복잡도와 실제 성능 특성을 심화 분석합니다.

O(log n + k) 검색 복잡도 분석 n (노드 수) 검색 시간 10 100 1K 10K 100K 1M O(n) O(log n) O(log n+k) 250x 차이 선형 리스트 검색 O(n) Interval Tree 검색 O(log n) +k 결과 반환 O(log n + k)

복잡도 분해

O(log n + k) 복잡도는 두 단계로 분해됩니다:

O(log n + k) 복잡도 분해: 첫 번째 매칭 + 결과 순회 Phase 1: O(log n) — iter_first() 레벨 0 레벨 1 pruned pruned FOUND! 트리 높이만큼 탐색 h = floor(2*log2(n+1)) n = 1,000 → h ≈ 20 n = 100,000 → h ≈ 34 n = 1,000,000 → h ≈ 40 Phase 2: O(k) — iter_next() 결과 1 ✓ 결과 2 ✓ 결과 3 ✓ STOP in-order 순회로 k개 결과 반환 종료 조건: node.start > query.last k = 0 → 즉시 종료 (O(1)) k = 5 → 5개 노드만 추가 방문 k = n → 전체 순회 (최악의 경우) 실제로 k ≪ n이므로 매우 효율적

자료구조 비교 분석

자료구조 삽입 삭제 점 질의 구간 질의 메모리 캐시 효율
Interval Tree O(log n) O(log n) O(log n) O(log n + k) 48B/node 보통
선형 리스트 O(1) O(1) O(n) O(n) 16B/node 좋음 (순차)
정렬 배열 O(n) O(n) O(log n) O(log n + k) 16B/entry 매우 좋음
해시 테이블 O(1) O(1) O(1) 불가능 16B/bucket 보통
세그먼트 트리 O(log n) O(log n) O(log n) O(log n + k) 4n bytes 좋음 (배열)

캐시 동작 분석

Interval Tree의 실제 성능은 캐시 효율에 크게 의존합니다:

/* 캐시 동작 분석 */

/* 1. interval_tree_node는 48바이트 → L1 캐시라인(64B)에 1개 적합 */
/*    트리 탐색 시 매 레벨마다 캐시라인 1개 로드 */

/* 2. 깊이 h = O(log n)이므로:
 *    n =    1,000 → h ≈ 10 → 캐시라인 10개 (640B)
 *    n =  100,000 → h ≈ 17 → 캐시라인 17개 (1,088B)
 *    n = 1,000,000 → h ≈ 20 → 캐시라인 20개 (1,280B)
 *    → L1 캐시(32-48KB)에 충분히 들어감
 */

/* 3. 자주 접근되는 루트 근처 노드는 캐시에 남아있을 확률 높음 */
/*    → 실제 캐시 미스는 하위 레벨에서만 발생 */

/* 4. prefetch 최적화 가능 */
static inline void interval_tree_search_prefetch(
    struct rb_node *node)
{
    /* 다음 레벨 노드를 미리 캐시에 로드 */
    if (node->rb_left)
        prefetch(node->rb_left);
    if (node->rb_right)
        prefetch(node->rb_right);
}

벤치마크 방법론

# perf stat으로 interval tree 성능 측정

# 1. VMA 집약적 워크로드 실행 + 캐시 통계 수집
perf stat -e cycles,instructions,cache-misses,cache-references \
         -e LLC-load-misses,LLC-store-misses \
         -e dTLB-load-misses \
         -- stress-ng --vm 8 --vm-bytes 2G --vm-method all --timeout 30s

# 2. find_vma 핫스팟 분석
perf record -g -e cycles -- stress-ng --vm 4 --timeout 10s
perf report --stdio --symbol-filter=find_vma

# 3. 커널 모듈 벤치마크 (직접 측정)
# → 아래 "커스텀 Interval Tree 구현" 섹션의 벤치마크 모듈 참조

# 출력 예시 (100K VMA, 1M 검색):
# interval_tree_iter_first: avg 780ns, p99 2.1us
# 선형 검색:                 avg 48us,  p99 95us
# 비율:                      ~61x 차이
ℹ️

실제 벤치마크 팁: perf statLLC-load-misses를 주의 깊게 관찰하세요. Interval Tree 검색에서 L3 캐시 미스가 높다면, 노드가 메모리에 흩어져 있어 TLB 미스까지 유발할 수 있습니다. NUMA_INTERLEAVE 환경에서는 원격 노드 접근까지 추가되어 지연이 2-3배 증가할 수 있습니다.

커스텀 Interval Tree 구현

INTERVAL_TREE_DEFINE 매크로를 사용하여 커널 모듈에서 자체 Interval Tree를 구현하는 방법을 단계별로 안내합니다.

Step 1: 노드 구조체 정의

/* my_interval_tree.h */
#include <linux/interval_tree_generic.h>
#include <linux/rbtree_augmented.h>

/* 커스텀 구간 노드 */
struct my_interval_node {
    struct rb_node rb;           /* rbtree 노드 */
    unsigned long start;         /* 구간 시작 */
    unsigned long last;          /* 구간 끝 (inclusive) */
    unsigned long __subtree_last; /* 증강 필드 */

    /* 사용자 데이터 */
    void *data;
    u32 flags;
};

Step 2: 매크로로 함수 생성

/* my_interval_tree.c */
#include "my_interval_tree.h"

/* start/last 접근 매크로 */
#define MY_START(node) ((node)->start)
#define MY_LAST(node)  ((node)->last)

/* Interval Tree 함수 생성
 * 생성되는 함수:
 *   my_itree_insert(node, root)
 *   my_itree_remove(node, root)
 *   my_itree_iter_first(root, start, last)
 *   my_itree_iter_next(node, start, last)
 */
INTERVAL_TREE_DEFINE(struct my_interval_node, rb,
    unsigned long, __subtree_last,
    MY_START, MY_LAST,
    static, my_itree)

Step 3: 커널 모듈 구현

/* my_interval_module.c — 완전한 커널 모듈 예제 */
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/ktime.h>
#include "my_interval_tree.h"

static struct rb_root_cached my_tree = RB_ROOT_CACHED;

/* 구간 노드 생성 및 삽입 */
static struct my_interval_node *
my_interval_add(unsigned long start, unsigned long last,
                 void *data)
{
    struct my_interval_node *node;

    node = kmalloc(sizeof(*node), GFP_KERNEL);
    if (!node)
        return ERR_PTR(-ENOMEM);

    node->start = start;
    node->last = last;
    node->data = data;
    node->flags = 0;

    my_itree_insert(node, &my_tree);

    pr_info("inserted [%lu, %lu]\n", start, last);
    return node;
}

/* 겹치는 구간 검색 */
static int my_interval_query(unsigned long start,
                              unsigned long last)
{
    struct my_interval_node *node;
    int count = 0;

    for (node = my_itree_iter_first(&my_tree, start, last);
         node;
         node = my_itree_iter_next(node, start, last)) {

        pr_info("  overlap: [%lu, %lu] data=%p\n",
                node->start, node->last, node->data);
        count++;
    }

    pr_info("query [%lu, %lu]: %d overlaps found\n",
            start, last, count);
    return count;
}

/* 벤치마크: 삽입 + 검색 성능 측정 */
static void my_interval_benchmark(int n)
{
    struct my_interval_node **nodes;
    ktime_t start, end;
    s64 insert_ns, search_ns;
    int i;

    nodes = kmalloc_array(n, sizeof(*nodes), GFP_KERNEL);
    if (!nodes)
        return;

    /* 삽입 벤치마크 */
    start = ktime_get();
    for (i = 0; i < n; i++) {
        nodes[i] = my_interval_add(
            i * 100,          /* start */
            i * 100 + 50,     /* last */
            NULL);
    }
    end = ktime_get();
    insert_ns = ktime_to_ns(ktime_sub(end, start));

    /* 검색 벤치마크 */
    start = ktime_get();
    for (i = 0; i < n; i++) {
        my_itree_iter_first(&my_tree,
            i * 100 + 25, i * 100 + 25);
    }
    end = ktime_get();
    search_ns = ktime_to_ns(ktime_sub(end, start));

    pr_info("Benchmark (n=%d):\n", n);
    pr_info("  Insert: %lld ns total, %lld ns/op\n",
            insert_ns, insert_ns / n);
    pr_info("  Search: %lld ns total, %lld ns/op\n",
            search_ns, search_ns / n);

    /* 정리 */
    for (i = 0; i < n; i++) {
        my_itree_remove(nodes[i], &my_tree);
        kfree(nodes[i]);
    }
    kfree(nodes);
}

static int __init my_interval_init(void)
{
    pr_info("my_interval_tree module loaded\n");

    /* 테스트 */
    my_interval_add(10, 20, NULL);
    my_interval_add(15, 25, NULL);
    my_interval_add(30, 40, NULL);

    my_interval_query(18, 18);  /* [10,20], [15,25] 찾음 */
    my_interval_query(35, 35);  /* [30,40] 찾음 */

    /* 벤치마크: 10K 노드 */
    my_interval_benchmark(10000);

    return 0;
}

static void __exit my_interval_exit(void)
{
    pr_info("my_interval_tree module unloaded\n");
}

module_init(my_interval_init);
module_exit(my_interval_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Custom Interval Tree Example");

Step 4: 검증 및 테스트

/* 커스텀 interval tree 단위 테스트 */
static void test_basic_operations(void)
{
    struct rb_root_cached root = RB_ROOT_CACHED;
    struct my_interval_node *n1, *n2, *n3, *found;

    pr_info("=== Basic Operations Test ===\n");

    /* 삽입 테스트 */
    n1 = my_interval_add(10, 20, NULL);
    n2 = my_interval_add(15, 25, NULL);
    n3 = my_interval_add(30, 40, NULL);

    /* 점 질의 테스트 */
    found = my_itree_iter_first(&root, 12, 12);
    BUG_ON(!found);  /* [10,20] 찾아야 함 */
    BUG_ON(found->start != 10);

    /* 겹침 없는 질의 */
    found = my_itree_iter_first(&root, 26, 29);
    BUG_ON(found != NULL);  /* 결과 없어야 함 */

    /* 다중 결과 질의 */
    int count = 0;
    for (found = my_itree_iter_first(&root, 18, 18);
         found;
         found = my_itree_iter_next(found, 18, 18))
        count++;
    BUG_ON(count != 2);  /* [10,20]과 [15,25] 두 개 */

    /* 삭제 테스트 */
    my_itree_remove(n2, &root);
    found = my_itree_iter_first(&root, 22, 22);
    BUG_ON(found != NULL);  /* [15,25] 삭제되었으므로 */

    /* 경계값 테스트 */
    found = my_itree_iter_first(&root, 20, 20);
    BUG_ON(!found);  /* [10,20] inclusive이므로 겹침 */

    found = my_itree_iter_first(&root, 10, 10);
    BUG_ON(!found);  /* 시작점도 inclusive */

    /* 정리 */
    my_itree_remove(n1, &root);
    my_itree_remove(n3, &root);
    kfree(n1); kfree(n2); kfree(n3);

    pr_info("=== All tests passed ===\n");
}

/* 스트레스 테스트: 대량 삽입/삭제/검색 */
static void test_stress(int n)
{
    struct rb_root_cached root = RB_ROOT_CACHED;
    struct my_interval_node **nodes;
    int i, total_found = 0;

    nodes = kvmalloc_array(n, sizeof(*nodes), GFP_KERNEL);
    if (!nodes) return;

    /* 겹치는 구간 대량 삽입 */
    for (i = 0; i < n; i++) {
        nodes[i] = kmalloc(sizeof(*nodes[i]), GFP_KERNEL);
        nodes[i]->start = i * 10;
        nodes[i]->last = i * 10 + 50;  /* 겹침 발생 */
        my_itree_insert(nodes[i], &root);
    }

    /* 범위 검색 스트레스 */
    for (i = 0; i < n; i++) {
        struct my_interval_node *it;
        for (it = my_itree_iter_first(&root, i*10+5, i*10+5);
             it;
             it = my_itree_iter_next(it, i*10+5, i*10+5))
            total_found++;
    }

    pr_info("stress test: %d nodes, %d total overlaps found\n",
            n, total_found);

    /* 정리 */
    for (i = 0; i < n; i++) {
        my_itree_remove(nodes[i], &root);
        kfree(nodes[i]);
    }
    kvfree(nodes);
}

Makefile

# Makefile
obj-m += my_interval_module.o

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

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

clean:
	make -C $(KDIR) M=$(PWD) clean

# 사용법:
# make
# sudo insmod my_interval_module.ko
# dmesg | tail -20
# sudo rmmod my_interval_module
⚠️

주의사항: 커널 모듈에서 Interval Tree 사용 시 반드시 동시성 보호를 해야 합니다. 위 예제는 단일 스레드 초기화에서만 동작합니다. 실제 운영 코드에서는 rwlock, mutex, 또는 RCU를 사용해 삽입/삭제/검색의 동시 접근을 보호해야 합니다.

커스텀 구현 시 흔한 실수

실수 증상 원인 해결
증강 필드 미초기화 검색 시 겹치는 노드 누락 __subtree_last를 삽입 전에 설정하지 않음 INTERVAL_TREE_DEFINE 매크로가 자동 처리하므로 별도 초기화 불필요
LAST 매크로 off-by-one 경계값에서 겹침 누락 LAST(node)가 exclusive 값을 반환 Interval Tree는 inclusive이므로 last = end - 1 사용
삭제 후 노드 재사용 커널 패닉 (dangling pointer) 삭제된 노드의 rb_node가 여전히 참조됨 삭제 후 RB_CLEAR_NODE() 호출, 재삽입 전 확인
동시성 보호 누락 간헐적 크래시, 데이터 손상 삽입/삭제 중 검색 수행 rwlock/mutex/RCU 사용
메모리 누수 slab 메모리 증가 삭제 후 kfree() 미호출 제거 후 반드시 노드 메모리 해제
💡

디버깅 팁: CONFIG_DEBUG_RBTREE=y를 활성화하면 Red-Black Tree 속성 위반(증강 필드 불일치 포함)을 런타임에 감지할 수 있습니다. 개발 중에는 반드시 이 옵션을 켜두세요.

Interval Tree 연산 상태 머신

Interval Tree의 삽입, 검색, 삭제 연산을 상태 머신 관점에서 분석합니다. 특히 동시 접근 패턴과 오류 처리를 중점적으로 다룹니다.

Interval Tree 연산 상태 머신 IDLE INSERT: rb_insert start 기준 위치 탐색 COLOR_FIX rb_insert_augmented AUGMENT_PROPAGATE __subtree_last 갱신 SEARCH: subtree_check __subtree_last >= start? OVERLAP_CHECK node.start <= q.last? FOUND / NEXT 결과 반환 또는 계속 탐색 DELETE: rb_erase 노드 제거 REBALANCE 색상 조정 + 회전 AUGMENT_UPDATE 영향받는 경로 갱신 DONE insert() iter_first() remove() next 동시 접근 패턴 (Concurrency Patterns) rwlock 패턴 (VMA): mmap_read_lock() → iter_first/iter_next (읽기 다수) mmap_write_lock() → insert/remove (쓰기 소수) RCU 패턴 (KVM memslot): rcu_read_lock() → search_memslots() (매우 빈번한 읽기) slots_lock + 복제 → 슬롯 업데이트 (매우 드문 쓰기) mutex 패턴 (DRM): struct_mutex/drm_mm_lock → insert/remove/search 직렬화

오류 처리 패턴

/* Interval Tree 사용 시 일반적인 오류 처리 패턴 */

/* 1. 메모리 할당 실패 */
struct my_interval_node *node;
node = kmalloc(sizeof(*node), GFP_KERNEL);
if (!node)
    return -ENOMEM;
/* 실패 시 rollback 필요 없음 (삽입 전이므로) */

/* 2. 중복 삽입 방지 */
static int safe_interval_insert(
    struct rb_root_cached *root,
    struct my_interval_node *new_node)
{
    struct my_interval_node *existing;

    /* 정확히 같은 구간이 이미 있는지 확인 */
    existing = my_itree_iter_first(root,
        new_node->start, new_node->last);

    while (existing) {
        if (existing->start == new_node->start &&
            existing->last == new_node->last) {
            pr_warn("duplicate interval [%lu, %lu]\n",
                    new_node->start, new_node->last);
            return -EEXIST;
        }
        existing = my_itree_iter_next(existing,
            new_node->start, new_node->last);
    }

    my_itree_insert(new_node, root);
    return 0;
}

/* 3. 안전한 삭제 (이중 삭제 방지) */
static void safe_interval_remove(
    struct rb_root_cached *root,
    struct my_interval_node *node)
{
    /* RB_EMPTY_NODE 확인으로 이중 삭제 방지 */
    if (RB_EMPTY_NODE(&node->rb)) {
        pr_warn("node already removed\n");
        return;
    }

    my_itree_remove(node, root);
    RB_CLEAR_NODE(&node->rb);  /* 빈 노드로 표시 */
}

RCU 기반 동시 접근 패턴

/* RCU를 활용한 읽기-쓰기 동시성 패턴 */

struct my_interval_tree {
    struct rb_root_cached root;
    struct rw_semaphore rwsem;  /* 쓰기 보호 */
};

/* 읽기 경로: RCU 보호 (빠른 경로) */
static struct my_interval_node *
my_interval_search_rcu(struct my_interval_tree *tree,
                        unsigned long point)
{
    struct my_interval_node *node;

    rcu_read_lock();
    node = my_itree_iter_first(&tree->root, point, point);
    /* 주의: rcu_read_unlock() 후 node 사용 불가
     * → 필요한 데이터를 미리 복사하거나
     * → 참조 카운트 증가 필요 */
    rcu_read_unlock();

    return node;
}

/* 쓰기 경로: rwsem 보호 (느린 경로) */
static int my_interval_insert_locked(
    struct my_interval_tree *tree,
    struct my_interval_node *node)
{
    down_write(&tree->rwsem);
    my_itree_insert(node, &tree->root);
    up_write(&tree->rwsem);

    /* RCU grace period 대기 (이전 읽기 완료 보장) */
    synchronize_rcu();

    return 0;
}
💡

동시성 선택 가이드:

  • 읽기 >> 쓰기 → RCU + rwsem (KVM memslot 패턴)
  • 읽기 ≈ 쓰기 → rwlock (VMA mmap_lock 패턴)
  • 단일 경쟁점 → mutex (DRM struct_mutex 패턴)
  • 인터럽트 컨텍스트 → spinlock + local_irq_save

lockdep 활용 디버깅

Interval Tree 동시성 문제를 lockdep으로 검증하는 방법입니다:

/* lockdep 어노테이션 예제 */
struct my_interval_tree {
    struct rb_root_cached root;
    struct rw_semaphore rwsem;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
};

/* 검색 시 읽기 잠금 검증 */
static struct my_interval_node *
my_interval_search_safe(struct my_interval_tree *tree,
                         unsigned long start,
                         unsigned long last)
{
    /* lockdep: rwsem을 읽기 잠금했는지 검증 */
    lockdep_assert_held_read(&tree->rwsem);

    return my_itree_iter_first(&tree->root, start, last);
}

/* 삽입 시 쓰기 잠금 검증 */
static void my_interval_insert_safe(
    struct my_interval_tree *tree,
    struct my_interval_node *node)
{
    /* lockdep: rwsem을 쓰기 잠금했는지 검증 */
    lockdep_assert_held_write(&tree->rwsem);

    my_itree_insert(node, &tree->root);
}

상태 머신 요약

연산 상태 전이 증강 갱신 동시성 요구
insert IDLE → INSERT → COLOR_FIX → PROPAGATE → DONE 삽입 노드 → 루트 쓰기 잠금
remove IDLE → DELETE → REBALANCE → AUGMENT → DONE 대체 노드 → 루트 쓰기 잠금
iter_first IDLE → SUBTREE_CHECK → OVERLAP → FOUND/NULL 없음 (읽기 전용) 읽기 잠금 / RCU
iter_next FOUND → IN_ORDER → OVERLAP → FOUND/STOP 없음 (읽기 전용) 읽기 잠금 / RCU