Min-Heap (lib/min_heap)

리눅스 커널의 범용 최소 힙(min-heap) 자료구조를 심층 분석합니다. include/linux/min_heap.h에 정의된 DEFINE_MIN_HEAP 매크로(Macro), min_heap_callbacks 비교자 패턴, push/pop/peek/heapify API의 내부 구현을 추적하고, sift-up과 sift-down 알고리즘의 단계별 동작을 시각화합니다. perf_event 멀티플렉싱, hrtimer 타이머(Timer) 큐, SCHED_DEADLINE EDF 스케줄링, CFS 대역폭(Bandwidth) 제어 등 커널 서브시스템에서의 실전 활용 사례와 rbtree 대비 장단점 비교까지 포괄합니다.

전제 조건: 연결 리스트(Linked List), Red-Black Tree 문서를 먼저 읽으세요. min-heap은 커널의 다른 자료구조(list, rbtree)와 비교하여 특정 사용 사례에 최적화된 구조이므로, 이들의 기본 개념을 먼저 이해해야 합니다.
일상 비유: min-heap은 병원 응급실 대기 시스템과 같습니다. 환자가 도착하면 중증도에 따라 대기 목록에 삽입되고, 의료진은 항상 가장 긴급한 환자(최솟값)를 즉시 확인(peek)하여 치료합니다. 새 환자가 오면 중증도 순서에 맞게 자리를 찾아가고(push + sift-up), 치료가 끝나면 다음 긴급 환자가 자동으로 맨 앞에 올라옵니다(pop + sift-down).

핵심 요약

  • O(1) 최솟값 조회 — 배열의 첫 번째 원소(data[0])가 항상 최솟값입니다. min_heap_peek()는 포인터 하나를 반환합니다.
  • O(log n) 삽입/삭제 — push는 배열 끝에 추가 후 sift-up, pop은 root와 마지막 원소를 교환 후 sift-down합니다.
  • O(n) 배열→힙 변환min_heapify_all()은 역순 sift-down으로 선형 시간에 임의 배열을 힙으로 변환합니다.
  • 배열 기반 캐시(Cache) 친화적 — 포인터 체이싱 없이 연속 메모리에 원소를 저장하여 L1/L2 캐시 히트율이 높습니다.
  • 커널 전역 활용 — perf_event 멀티플렉싱, hrtimer 타이머 큐, SCHED_DEADLINE EDF 스케줄링 등에서 사용됩니다.

단계별 이해

  1. 힙 속성 이해
    완전 이진 트리에서 부모 노드가 항상 자식 이하인 불변식을 파악합니다.
  2. 배열 인덱싱 규칙 파악
    parent(i)=(i-1)/2, left(i)=2i+1, right(i)=2i+2 매핑(Mapping)을 이해합니다.
  3. 구조체(Struct)와 콜백(Callback) 분석
    struct min_heap의 data/nr/size 필드와 min_heap_callbacks의 less/swp 함수 포인터를 분석합니다.
  4. 핵심 연산 추적
    sift-up(push), sift-down(pop), heapify의 단계별 동작을 시각적으로 추적합니다.
  5. 커널 활용 사례 학습
    perf, hrtimer, SCHED_DEADLINE에서의 실전 사용 패턴을 분석합니다.
커널 소스 위치: include/linux/min_heap.h, lib/min_heap.c (테스트). min-heap API는 커널 v5.12에서 lib/min_heap.h로 최초 도입되었으며, v6.5에서 include/linux/min_heap.h로 이동하고 DEFINE_MIN_HEAP 매크로가 추가되었습니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

힙 속성 (Min-Heap Property)

최소 힙(min-heap)은 완전 이진 트리(complete binary tree)힙 순서 속성(heap-order property)을 결합한 자료구조입니다. 두 가지 불변식이 동시에 성립합니다.

형태 속성 (Shape Property)

트리는 항상 완전 이진 트리입니다. 마지막 레벨을 제외한 모든 레벨이 가득 차 있고, 마지막 레벨의 노드는 왼쪽부터 채워집니다. 이 속성 덕분에 배열로 표현할 때 빈 공간이 발생하지 않습니다.

순서 속성 (Heap-Order Property)

모든 노드 i에 대해 heap[parent(i)] ≤ heap[i]가 성립합니다. 즉 부모 노드의 값은 항상 자식 노드의 값 이하입니다. 이 불변식에 의해 루트 노드(data[0])는 항상 전체 힙의 최솟값입니다.

속성정의보장 내용
완전 이진 트리마지막 레벨 제외 모든 레벨이 포화배열 기반 표현 가능, 높이 = ⌊log n⌋
Min-Heap 순서parent(i) ≤ idata[0]이 항상 최솟값
형제 간 무관좌우 자식 간 순서 없음정렬 불필요, 삽입/삭제 단순화
Max-Heap과의 관계: 비교 함수의 방향만 반전하면 max-heap이 됩니다. 리눅스 커널은 비교자(less)를 콜백으로 받으므로, 동일한 코드로 min-heap과 max-heap을 모두 구현할 수 있습니다.

힙 속성이 깨지는 경우는 두 가지뿐입니다. (1) 새 원소가 부모보다 작을 때(push 후) → sift-up으로 복원, (2) 루트가 자식보다 클 때(pop 후) → sift-down으로 복원. 모든 연산은 이 두 가지 복원 경로 중 하나를 사용합니다.

배열 기반 트리 표현

완전 이진 트리는 배열의 인덱스만으로 부모-자식 관계를 표현할 수 있습니다. 포인터가 필요 없어 메모리 오버헤드(Overhead)가 없고, 연속 메모리 배치로 캐시 성능이 우수합니다.

인덱스 공식

관계공식예시 (i=2)
부모parent(i) = (i - 1) / 2parent(2) = 0
왼쪽 자식left(i) = 2 * i + 1left(2) = 5
오른쪽 자식right(i) = 2 * i + 2right(2) = 6

이 공식들은 비트 연산으로 최적화됩니다. (i - 1) >> 1은 부모 인덱스, (i << 1) | 1은 왼쪽 자식, (i << 1) + 2는 오른쪽 자식입니다. 커널의 min_heap 구현은 이 산술을 직접 사용합니다.

배열 인덱스 ↔ 완전 이진 트리 매핑 트리 표현 3 [0] 7 [1] 5 [2] 12 [3] 9 [4] 8 [5] 15 [6] 배열 표현 (data[]) [0] 3 [1] 7 [2] 5 [3] 12 [4] 9 [5] 8 [6] 15 연속 메모리 배치 → L1/L2 캐시 히트율 극대화 (포인터 체이싱 없음)
완전 이진 트리의 각 노드가 배열 인덱스에 1:1 매핑됩니다. parent(i)=(i-1)/2 공식으로 포인터 없이 탐색합니다
캐시 효율: 힙의 높이 h에서 레벨 k에는 최대 2k개의 노드가 있습니다. 64바이트 캐시 라인(Cache Line)에 8바이트 원소 8개가 들어가므로, 상위 3개 레벨(15개 노드)이 2개 캐시 라인에 수용됩니다. rbtree에서는 각 노드가 별도 할당되어 포인터 체이싱마다 캐시 미스가 발생합니다.

min_heap 구조체

커널의 min-heap은 제네릭 매크로 기반으로 구현됩니다. DEFINE_MIN_HEAP 매크로가 타입별 힙 구조체를 생성하고, 공통 API가 void *와 원소 크기를 통해 동작합니다.

/* include/linux/min_heap.h */

#define DEFINE_MIN_HEAP(type, name)   \
struct name {                           \
    int nr;                              \
    int size;                            \
    type *data;                          \
    type preallocated[];                 \
}

/* 프리할당 없는 기본 힙 */
typedef DEFINE_MIN_HEAP(void, min_heap) min_heap_t;

/* 프리할당 원소를 포함하는 정적 힙 */
#define DEFINE_MIN_HEAP_STATIC(type, name, count) \
struct name {                                     \
    int nr;                                        \
    int size;                                      \
    type *data;                                    \
    type preallocated[count];                      \
}
필드타입설명
nrint현재 힙에 저장된 원소 개수
sizeintdata 배열이 수용할 수 있는 최대 원소 개수
datatype *힙 원소를 담는 배열 포인터 (preallocated 또는 외부 할당)
preallocated[]type[]유연 배열 멤버(FAM)로 인라인 저장소 제공
struct min_heap 메모리 레이아웃 DEFINE_MIN_HEAP_STATIC (인라인) nr = 5 size = 8 *data preallocated[0..7] (유연 배열 멤버) data → preallocated (동일 구조체 내) 추가 할당 없음, 스택/정적에 적합 DEFINE_MIN_HEAP (동적) nr = 5 size = N *data preallocated[0] (비어 있음) kmalloc/vmalloc으로 할당된 버퍼 data[0] data[1] data[2] ... data[N-1] size가 런타임에 결정될 때 사용
프리할당(정적) 힙은 구조체 내부에 배열을 포함하고, 동적 힙은 외부 버퍼(Buffer)를 data 포인터로 참조합니다

초기화 패턴

/* 정적 프리할당 힙 (스택에 선언) */
DEFINE_MIN_HEAP_STATIC(u64, timer_heap, 16);
struct timer_heap heap = {
    .nr   = 0,
    .size = 16,
    .data = heap.preallocated,
};

/* 동적 할당 힙 */
DEFINE_MIN_HEAP(u64, event_heap);
struct event_heap heap;
heap.nr   = 0;
heap.size = max_events;
heap.data = kmalloc_array(max_events, sizeof(u64), GFP_KERNEL);
주의: data 포인터를 preallocated로 설정할 때는 size가 프리할당 배열 크기를 초과하지 않아야 합니다. 초과하면 배열 경계를 벗어나 메모리 손상이 발생합니다.

min_heap_callbacks: 비교자와 스왑(Swap)

min-heap의 동작은 비교 함수스왑 함수 두 콜백에 의해 결정됩니다. 이를 통해 동일한 힙 코드가 임의의 타입에 대해 동작합니다.

/* include/linux/min_heap.h */
struct min_heap_callbacks {
    bool (*less)(const void *lhs, const void *rhs, void *args);
    void (*swp)(void *lhs, void *rhs, void *args);
};
콜백역할NULL 시 기본 동작
less두 원소의 순서 비교 (true면 lhs가 더 작음)memcmp 기반 바이트 비교
swp두 원소의 위치 교환memcpy 기반 범용 교환 (3회 복사)

콜백 구현 예시

/* 정수 비교: 가장 단순한 형태 */
static bool u64_less(const void *lhs, const void *rhs, void *args)
{
    return *(const u64 *)lhs < *(const u64 *)rhs;
}

/* 구조체 비교: 특정 필드를 키로 사용 */
static bool event_less(const void *lhs, const void *rhs, void *args)
{
    const struct perf_event *a = *((const struct perf_event **)lhs);
    const struct perf_event *b = *((const struct perf_event **)rhs);
    return a->weight < b->weight;
}

/* 커스텀 스왑: 인덱스 역추적이 필요한 경우 */
static void event_swp(void *lhs, void *rhs, void *args)
{
    struct perf_event **a = lhs, **b = rhs;
    swap(*a, *b);
    /* 스왑 후 인덱스 갱신 */
    (*a)->heap_index = a - (struct perf_event **)args;
    (*b)->heap_index = b - (struct perf_event **)args;
}

static const struct min_heap_callbacks event_callbacks = {
    .less = event_less,
    .swp  = event_swp,
};
성능 팁: swp를 NULL로 설정하면 커널이 memcpy 기반 범용 스왑을 사용합니다. 작은 타입(8바이트 이하)에서는 충분하지만, 구조체가 크거나 부가 작업(인덱스 갱신)이 필요하면 커스텀 swp를 구현하세요.

push 연산 (삽입)

min_heap_push()는 새 원소를 힙에 삽입합니다. 배열의 마지막 위치에 원소를 추가한 후, sift-up 연산으로 힙 속성을 복원합니다.

알고리즘

  1. heap->nrheap->size와 같으면 실패 (힙이 가득 참) — -ENOMEM에 해당하는 에러 경로
  2. 새 원소를 data[nr] 위치에 복사
  3. nr++
  4. 삽입 위치(nr - 1)에서 sift-up 시작: 부모와 비교하여 새 원소가 더 작으면 교환, 루트에 도달하거나 부모 이상이면 중단
/* include/linux/min_heap.h — 단순화된 push 구현 */
static inline int min_heap_push(min_heap_t *heap,
        const void *element, size_t elem_size,
        const struct min_heap_callbacks *func, void *args)
{
    void *data = heap->data;
    int pos;

    if (heap->nr >= heap->size)
        return -1;  /* 힙 가득 참 */

    /* 배열 끝에 삽입 */
    pos = heap->nr++;
    memcpy(data + pos * elem_size, element, elem_size);

    /* sift-up: 부모보다 작으면 교환하며 올라감 */
    min_heap_sift_up(heap, elem_size, func, args, pos);

    return 0;
}

sift-up 상세

/* sift-up: 새 원소를 올바른 위치로 올림 */
static inline void min_heap_sift_up(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func,
        void *args, int pos)
{
    void *data = heap->data;
    int parent;

    while (pos > 0) {
        parent = (pos - 1) / 2;
        if (!func->less(data + pos * elem_size,
                        data + parent * elem_size, args))
            break;  /* 부모 이하 → 힙 속성 만족 */
        func->swp(data + pos * elem_size,
                  data + parent * elem_size, args);
        pos = parent;
    }
}
push(2): sift-up 과정 Step 1: 배열 끝에 삽입 3 7 5 9 2 NEW Step 2: 2 < 7 → swap 3 2 5 9 7 Step 3: 2 < 3 → swap 2 3 5 9 7 완료! 루트에 도달 배열 변화 Step 1: 3 7 5 9 2 Step 2: 3 2 5 9 7 Step 3: 2 3 5 9 7 최악 O(log n) 교환, 최선 O(1) (이미 순서 맞음) sift-up 경로: 리프 → 루트 (트리 높이만큼 최대 비교/교환)
push(2) 실행 시 배열 끝에 삽입 후 부모와 비교하며 sift-up합니다. 2회 교환으로 루트에 도달합니다

pop 연산 (최솟값 제거)

min_heap_pop()는 힙에서 최솟값(루트)을 제거합니다. 마지막 원소를 루트로 이동시킨 후, sift-down 연산으로 힙 속성을 복원합니다.

알고리즘

  1. heap->nr이 0이면 힙이 비어 있음
  2. data[0](루트)과 data[nr - 1](마지막 원소)을 교환
  3. nr-- (마지막 위치의 원래 루트를 논리적으로 제거)
  4. 루트 위치(0)에서 sift-down 시작: 자식 중 더 작은 것과 비교, 자식보다 크면 교환, 리프에 도달하거나 자식 이하이면 중단
/* include/linux/min_heap.h — 단순화된 pop 구현 */
static inline void min_heap_pop(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func, void *args)
{
    if (heap->nr <= 0)
        return;

    /* 루트와 마지막 원소 교환 */
    heap->nr--;
    if (func->swp)
        func->swp(heap->data,
                  heap->data + heap->nr * elem_size, args);
    else
        __min_heap_swap(heap->data,
                        heap->data + heap->nr * elem_size,
                        elem_size);

    /* 루트에서 sift-down */
    min_heap_sift_down(heap, elem_size, func, args, 0);
}

sift-down 상세

/* sift-down: 루트를 올바른 위치로 내림 */
static inline void min_heap_sift_down(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func,
        void *args, int pos)
{
    void *data = heap->data;
    int left, right, smallest;

    while (1) {
        left     = 2 * pos + 1;
        right    = 2 * pos + 2;
        smallest = pos;

        if (left < heap->nr &&
            func->less(data + left * elem_size,
                       data + smallest * elem_size, args))
            smallest = left;

        if (right < heap->nr &&
            func->less(data + right * elem_size,
                       data + smallest * elem_size, args))
            smallest = right;

        if (smallest == pos)
            break;  /* 힙 속성 만족 */

        func->swp(data + pos * elem_size,
                  data + smallest * elem_size, args);
        pos = smallest;
    }
}
pop(): sift-down 과정 Step 1: root ↔ last 9 (was last) 3 5 7 Step 2: 9 > min(3,5) → swap(9,3) 3 9 5 7 Step 3: 9 > 7 → swap(9,7) 3 7 5 9 완료! 리프에 도달 sift-down 핵심 규칙 각 단계에서 현재 노드와 두 자식 중 최솟값을 비교 → 자식이 더 작으면 교환 최악 O(log n) 교환 (루트→리프), 최선 O(1) (이미 순서 맞음) 왜 항상 더 작은 자식과 교환하는가? 더 큰 자식과 교환하면 새로운 부모가 나머지 자식보다 클 수 있어 힙 속성이 깨집니다. 더 작은 자식이 부모가 되어야 양쪽 모두 만족합니다.
pop() 후 마지막 원소(9)가 루트로 이동하고, 더 작은 자식과 교환하며 sift-down합니다

peek 연산: O(1) 최솟값 조회

min_heap_peek()는 힙에서 최솟값을 제거하지 않고 조회만 합니다. 힙 속성에 의해 최솟값은 항상 data[0]에 있으므로, 단순히 첫 번째 원소의 포인터를 반환합니다.

/* include/linux/min_heap.h */
static inline void *min_heap_peek(min_heap_t *heap)
{
    return heap->nr ? heap->data : NULL;
}
특성
시간 복잡도O(1) — 인덱스 0 직접 접근
반환값void * — 첫 번째 원소의 포인터 (호출자가 타입 캐스팅)
빈 힙NULL 반환
부수 효과없음 — 힙 상태 변경 없음
반환된 포인터의 수명: min_heap_peek()가 반환한 포인터는 다음 push/pop/heapify 호출 시 무효화(Invalidation)될 수 있습니다. sift 과정에서 data[0]의 내용이 교환되기 때문입니다. 값이 필요하면 즉시 복사하세요.
/* 안전한 peek 패턴 */
const u64 *p = min_heap_peek(&heap);
if (p) {
    u64 min_val = *p;  /* 즉시 복사 */
    /* 이후 min_val 사용 */
}

/* 위험한 패턴: pop 후 이전 peek 포인터 사용 */
const u64 *p = min_heap_peek(&heap);
min_heap_pop(&heap, ...);
/* p가 가리키는 값이 변경됨 — 버그! */

heapify: O(n) 배열→힙 변환

min_heapify_all()은 임의 순서의 배열을 유효한 힙으로 변환합니다. 개별 push를 n번 반복하면 O(n log n)이지만, heapify는 역순 sift-down으로 O(n) 시간에 완료됩니다.

알고리즘

/* include/linux/min_heap.h — heapify 구현 */
static inline void min_heapify_all(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func, void *args)
{
    int i;

    /* 마지막 비-리프 노드부터 역순으로 sift-down
     * 리프 노드(인덱스 nr/2 ~ nr-1)는 이미 유효한 힙 */
    for (i = heap->nr / 2 - 1; i >= 0; i--)
        min_heap_sift_down(heap, elem_size, func, args, i);
}

O(n) 시간 복잡도 증명

직관적으로 O(n log n)처럼 보이지만, 각 레벨에서의 sift-down 비용이 다릅니다.

레벨 (아래서부터)노드 수sift-down 최대 깊이총 비용
0 (리프)n/20 (건너뜀)0
1n/41n/4
2n/82n/4
kn/2k+1kkn/2k+1
log n (루트)1log nlog n

총 비용은 ∑k=1log n kn/2k+1 = n ⋅ ∑k=1 k/2k+1 = n ⋅ 1 = O(n)입니다. 핵심 통찰은 노드가 가장 많은 하위 레벨의 sift-down 깊이가 가장 짧다는 것입니다.

heapify: 역순 sift-down으로 O(n) 변환 Before: 임의 배열 9 3 7 5 1 8 2 heapify After: 유효한 Min-Heap 1 3 2 5 9 8 7 처리 순서 (마지막 비-리프부터) i=2: node[2]=7 sift-down 1단계 i=1: node[1]=3 sift-down 2단계 i=0: node[0]=9 sift-down 2단계 완료! 비용 분석: 리프(n/2개)는 건너뜀 → 나머지 n/2개만 처리 하위 레벨 노드: 많지만 sift 깊이 짧음 | 상위 레벨 노드: 적지만 sift 깊이 깊음 총합 = O(n) (기하급수적 감소로 수렴)
heapify는 마지막 비-리프 노드(i=2)부터 루트(i=0)까지 역순으로 sift-down을 수행합니다
heapify vs 반복 push: n개의 원소를 정렬되지 않은 배열에서 힙으로 만들 때, 반복 push는 O(n log n)이고 heapify는 O(n)입니다. 커널에서는 초기화 시 배열이 이미 존재하는 경우가 많으므로 min_heapify_all()을 선호합니다.

임의 원소 삭제

min_heap_del()은 인덱스로 지정된 임의 위치의 원소를 힙에서 제거합니다. pop이 항상 루트(인덱스 0)를 제거하는 것과 달리, 임의 인덱스 삭제는 sift-up 또는 sift-down이 모두 필요할 수 있습니다.

알고리즘

  1. 삭제할 인덱스 i의 원소를 마지막 원소(data[nr-1])로 교체
  2. nr--
  3. 교체된 원소가 부모보다 작으면 sift-up, 자식보다 크면 sift-down
/* include/linux/min_heap.h — 임의 삭제 */
static inline void min_heap_del(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func,
        void *args, int pos)
{
    void *data = heap->data;

    if (pos >= heap->nr)
        return;

    /* 마지막 원소와 교환 */
    heap->nr--;
    if (pos != heap->nr) {
        if (func->swp)
            func->swp(data + pos * elem_size,
                      data + heap->nr * elem_size, args);
        else
            __min_heap_swap(data + pos * elem_size,
                            data + heap->nr * elem_size,
                            elem_size);

        /* 교체된 값에 따라 sift-up 또는 sift-down */
        if (pos > 0 && func->less(data + pos * elem_size,
                data + ((pos - 1) / 2) * elem_size, args))
            min_heap_sift_up(heap, elem_size, func, args, pos);
        else
            min_heap_sift_down(heap, elem_size, func, args, pos);
    }
}
인덱스 관리: 임의 삭제를 사용하려면 각 원소가 자신의 힙 인덱스를 알아야 합니다. 커스텀 swp 콜백에서 교환 시 인덱스를 갱신하는 패턴이 일반적입니다. perf_event의 heap_index 필드가 대표적 예시입니다.

API 레퍼런스

include/linux/min_heap.h에 정의된 전체 API를 정리합니다. 모든 함수는 static inline으로 헤더에서 인라인 확장됩니다.

API시간 복잡도설명
min_heap_push()O(log n)원소 삽입 + sift-up
min_heap_pop()O(log n)최솟값 제거 + sift-down
min_heap_peek()O(1)최솟값 조회 (제거 없음)
min_heap_full()O(1)nr >= size 검사
min_heap_empty()O(1)nr == 0 검사
min_heapify_all()O(n)배열을 힙으로 변환
min_heap_del()O(log n)임의 인덱스 삭제
min_heap_sift_up()O(log n)지정 위치에서 상향 복원
min_heap_sift_down()O(log n)지정 위치에서 하향 복원

헬퍼 매크로

/* 힙 상태 검사 */
static inline bool min_heap_full(min_heap_t *heap)
{
    return heap->nr >= heap->size;
}

static inline bool min_heap_empty(min_heap_t *heap)
{
    return heap->nr == 0;
}

/* 타입 안전 래퍼 매크로 (v6.5+) */
#define __min_heap_cast(_heap)    \
    (min_heap_t *)(void *)(_heap)

#define __min_heap_elem_size(_heap)   \
    sizeof(*(_heap)->data)
인라인 최적화: 모든 API가 static inline으로 정의되어 함수 호출 오버헤드가 없습니다. 비교자와 스왑 함수도 인라인으로 전개되면, 전체 push/pop 경로가 단일 함수 본체로 컴파일됩니다. 이는 perf_event처럼 hot path에서 중요한 최적화입니다.

perf_event 스케줄링

perf 서브시스템은 min-heap의 가장 대표적인 커널 내 사용자입니다. 하드웨어 PMU(Performance Monitoring Unit)의 카운터 수는 제한적이므로, 다수의 perf_event를 멀티플렉싱(time-sharing)해야 합니다. min-heap은 가장 짧은 타임슬라이스(Time Slice)를 가진 이벤트를 O(1)로 찾는 데 사용됩니다.

멀티플렉싱 메커니즘

PMU에 동시에 장착할 수 있는 이벤트 수가 nhw개이고, 사용자가 요청한 이벤트가 nreq개일 때 nreq > nhw이면 멀티플렉싱이 필요합니다. 각 이벤트에 가중치(weight)를 부여하고, min-heap으로 가장 적게 실행된 이벤트를 빠르게 선택합니다.

/* kernel/events/core.c — perf_event 멀티플렉싱 관련 */

/* 이벤트 힙 비교: 더 적게 실행된 이벤트가 우선 */
static bool perf_event_less(const void *lhs,
                             const void *rhs, void *args)
{
    struct perf_event *a = *(struct perf_event **)lhs;
    struct perf_event *b = *(struct perf_event **)rhs;

    /* 타임슬라이스가 짧은(덜 실행된) 이벤트가 우선 */
    return a->tstamp_running < b->tstamp_running;
}

/* rotation 시 힙에서 다음 이벤트 선택 */
static void perf_rotate_context(struct perf_cpu_context *cpuctx)
{
    struct perf_event **min_event;

    min_event = min_heap_peek(&cpuctx->heap);
    if (min_event) {
        /* 가장 적게 실행된 이벤트를 PMU에 장착 */
        perf_event_enable(*min_event);
        min_heap_pop(&cpuctx->heap, ...);
    }
}
perf_event Min-Heap 기반 멀티플렉싱 PMU (HW Counters) Counter 0 Counter 1 Counter 2 Fixed CTR n_hw = 4 (동시 장착 가능) Min-Heap (대기 이벤트) evt5 t=120 evt7 t=350 evt6 t=200 evt8 t=500 evt9 t=480 n_req - n_hw = 5 대기중 peek() → 장착 회전 시 push 시간축 (Rotation) T=0: evt1~4 장착 T=100: rotation tick → peek() = evt5 (t=120) → evt1을 힙에 push → evt5를 PMU에 장착 공정한 시간 분배! peek()는 O(1)로 즉시 다음 장착 대상을 결정 rbtree를 사용하면 O(log n)이지만, 힙은 최솟값만 필요한 이 패턴에 최적
PMU 카운터가 부족하면 min-heap으로 실행 시간이 가장 짧은 이벤트를 O(1)로 선택하여 공정하게 멀티플렉싱합니다

hrtimer 통합

고해상도 타이머(hrtimer) 서브시스템은 나노초 정밀도의 타이머를 관리합니다. 여러 타이머 중 가장 빠른 만료 시간을 찾는 것이 핵심 연산이며, 이는 min-heap의 peek()에 해당합니다.

hrtimer에서의 힙 활용

hrtimer는 전통적으로 rbtree를 사용했지만, 최근 커널에서는 특정 경로에서 min-heap 패턴을 활용합니다. 만료 시간이 가장 이른 타이머가 항상 루트에 있으므로, 다음 인터럽트(Interrupt) 프로그래밍에 필요한 시간을 O(1)로 결정합니다.

/* hrtimer 만료 시간 비교 */
static bool timer_less(const void *lhs,
                       const void *rhs, void *args)
{
    const struct hrtimer *a = *((const struct hrtimer **)lhs);
    const struct hrtimer *b = *((const struct hrtimer **)rhs);

    /* ktime_t 비교: 더 이른 만료 시간이 우선 */
    return ktime_before(a->_softexpires, b->_softexpires);
}

/* 다음 인터럽트 시간 결정 */
ktime_t hrtimer_get_next_event(void)
{
    struct hrtimer **next = min_heap_peek(&timer_heap);
    if (next)
        return (*next)->_softexpires;
    return KTIME_MAX;
}
hrtimer Min-Heap 타이머 큐 Timer Min-Heap T+5ms timer_A T+12ms timer_B T+8ms timer_C T+50ms T+30ms HW Timer next_irq = T+5ms peek() IRQ 발생 시 1. pop() → timer_A 2. timer_A 콜백 실행 3. peek() → T+8ms 4. HW 타이머 재프로그래밍 → timer_C가 새 루트 O(1) peek()으로 다음 만료 시간을 즉시 결정 → HW 타이머 프로그래밍 최소화 타이머 추가/제거는 O(log n)이지만, "다음은 언제?"는 항상 O(1)
min-heap의 루트가 항상 가장 빠른 만료 시간을 보유하므로 HW 타이머 프로그래밍이 O(1)로 결정됩니다

SCHED_DEADLINE (EDF) 스케줄러(Scheduler)

Earliest Deadline First (EDF) 스케줄링 정책을 구현하는 SCHED_DEADLINE 클래스는 각 태스크(Task)의 절대 데드라인을 기준으로 스케줄링합니다. 데드라인이 가장 이른 태스크를 빠르게 선택하는 것이 핵심이며, 이는 min-heap의 전형적인 사용 사례입니다.

dl_rq에서의 데드라인 관리

dl_rq(deadline run queue)는 rb_root_cached를 사용하여 rbtree의 leftmost 캐시로 O(1) 최솟값 접근을 제공합니다. 이 패턴은 사실상 rbtree 위에 min-heap의 peek() 의미론을 구현한 것입니다.

/* kernel/sched/deadline.c */
struct dl_rq {
    struct rb_root_cached root;  /* leftmost 캐시 포함 */
    u64   earliest_dl;            /* 캐시된 최소 데드라인 */
};

/* 가장 이른 데드라인 태스크 선택 */
static struct sched_dl_entity *pick_earliest_dl_entity(
        struct dl_rq *dl_rq)
{
    struct rb_node *left = rb_first_cached(&dl_rq->root);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_dl_entity, rb_node);
}
min-heap vs rb_root_cached: SCHED_DEADLINE이 rbtree를 사용하는 이유는 임의 삭제와 범위 쿼리가 빈번하기 때문입니다. min-heap은 임의 삭제에 인덱스 추적이 필요하지만, rbtree는 노드 포인터로 O(log n) 삭제가 가능합니다. 두 자료구조의 선택은 사용 패턴에 따라 결정됩니다.
연산min-heaprb_root_cachedEDF 빈도
최솟값 조회O(1)O(1) (leftmost 캐시)매우 높음 (매 tick)
삽입O(log n)O(log n)높음 (태스크 wake-up)
최솟값 삭제O(log n)O(log n)높음 (dispatch)
임의 삭제O(log n) + 인덱스 추적O(log n) 포인터 기반보통 (preemption)
순회불가 (정렬 아님)O(n) 정렬순드물게

CFS 대역폭 제어

CFS(Completely Fair Scheduler)의 대역폭 제어(bandwidth throttling)는 cgroup별 CPU 사용량을 제한합니다. 스로틀(throttle) 타이머의 관리에서 가장 이른 만료 시간을 추적하는 데 min-heap 패턴이 적용됩니다.

/* kernel/sched/fair.c — CFS 대역폭 관련 */
struct cfs_bandwidth {
    ktime_t           period;
    u64               quota;
    u64               runtime;
    struct hrtimer    period_timer;
    struct hrtimer    slack_timer;
    /* ... */
};

/* 스로틀된 cgroup 중 가장 빨리 해제될 것을 찾기 위해
 * 타이머 힙에서 peek()로 다음 unthrottle 시점 결정 */
static void do_sched_cfs_period_timer(
        struct cfs_bandwidth *cfs_b)
{
    /* 만료된 타이머 처리 후 다음 타이머 프로그래밍 */
    hrtimer_forward_now(&cfs_b->period_timer,
                        cfs_b->period);
}
실전 시나리오: 컨테이너(Container) 환경에서 100개의 cgroup이 각각 CPU 쿼터를 가지고 있을 때, 100개의 period_timer 중 가장 이른 것을 O(1)로 찾는 것은 스케줄러 latency에 직접 영향을 미칩니다.

메모리 레이아웃 분석

min-heap의 메모리 효율은 배열 기반 구조에서 비롯됩니다. 포인터 오버헤드 없이 원소만 연속 배치되므로, 같은 개수의 원소를 저장할 때 rbtree나 linked list보다 메모리 사용량이 적습니다.

인라인 vs 동적 할당

할당 방식메모리 위치오버헤드적합한 상황
DEFINE_MIN_HEAP_STATIC구조체 내부 (스택/정적)0 (FAM)크기가 컴파일 시 결정, 소규모
DEFINE_MIN_HEAP + kmallocslab 할당slab 헤더크기가 런타임에 결정, 대규모
외부 배열 참조호출자가 관리없음기존 배열을 힙으로 사용
자료구조별 메모리 비교 (n=7, element=8B) Min-Heap (배열) nr(4B) + size(4B) + *data(8B) = 16B data[7] = 7 x 8B = 56B 총: 72 bytes rbtree (노드) rb_root(8B) + leftmost(8B) = 16B 7 x (rb_node(24B) + 8B) = 224B 총: 240 bytes Sorted List list_head(16B) 7 x (list_head(16B) + 8B) = 168B 총: 184 bytes 캐시 라인 분석 (64B 캐시 라인) Min-Heap: 2 캐시 라인 rbtree: 4~7 캐시 라인 List: 3~7 캐시 라인 min-heap은 연속 메모리이므로 프리페치(prefetch)가 효과적이고, rbtree/list는 포인터 체이싱으로 인한 캐시 미스가 빈번합니다
동일 원소 수(7개, 각 8바이트)에서 min-heap이 메모리와 캐시 효율 모두 우수합니다

캐시 라인 정렬

/* 캐시 라인 정렬된 힙 선언 */
struct aligned_heap {
    DEFINE_MIN_HEAP_STATIC(u64, heap_data, 64);
} __aligned(L1_CACHE_BYTES);

/* per-CPU 힙: 각 CPU의 L1 캐시에 고정 */
static DEFINE_PER_CPU(struct aligned_heap, cpu_heap);

복잡도 분석

min-heap의 시간/공간 복잡도를 정리하고, 다른 우선순위(Priority) 큐 구현과 비교합니다.

시간 복잡도

연산최선최악평균설명
peekO(1)O(1)O(1)data[0] 직접 접근
pushO(1)O(log n)O(1)**평균적으로 상위까지 올라가지 않음
popO(1)O(log n)O(log n)sift-down은 대부분 바닥까지
heapifyO(n)O(n)O(n)역순 sift-down의 기하급수 합
delete(i)O(1)O(log n)O(log n)sift-up 또는 sift-down
full/emptyO(1)O(1)O(1)정수 비교
push의 평균 O(1): 무작위 입력에서 새 원소가 트리의 하위 레벨에 머무를 확률이 높습니다. 레벨 k에서 멈출 확률은 1/2k이므로, 평균 sift-up 횟수는 ∑k=0 k/2k = 2입니다. 따라서 push의 amortized 비용은 O(1)입니다.

공간 복잡도

항목min-heaprbtreesorted array
원소당 오버헤드024B (rb_node)0
헤더 크기16B (nr+size+data)16B (rb_root_cached)8B (size+count)
총 메모리 (n개)16 + n × elem_size16 + n × (elem_size + 24)8 + n × elem_size
동적 할당선택적노드별 필수선택적

rbtree vs min-heap 비교

커널에서 우선순위 기반 자료구조를 선택할 때 rbtree와 min-heap은 가장 대표적인 후보입니다. 각각의 강점과 약점을 체계적으로 비교합니다.

rbtree vs Min-Heap: 사용 시점 가이드 Min-Heap 선호 최솟값/최댓값만 필요 (peek 빈번) 순회(traversal)가 불필요 메모리 절약 중요 (포인터 없음) 캐시 성능 중시 (연속 메모리) O(n) heapify로 일괄 초기화 가능 원소 수가 고정/소규모 (~100 이하) 예: perf_event, 타이머, 이벤트 큐 rbtree 선호 정렬순 순회가 필요 노드 포인터로 O(log n) 임의 삭제 범위 쿼리 (lower_bound ~ upper_bound) 동적 크기 변경 (재할당 없음) 중복 키 허용 + 안정적 순서 유지 대규모 데이터 (수천 이상) 예: CFS, VMA, inode 캐시 두 자료구조는 상호 배타적이 아니라 보완적입니다
최솟값만 필요한 패턴은 min-heap, 정렬순 순회나 범위 쿼리가 필요하면 rbtree를 선택합니다

마이크로벤치마크 경향

시나리오min-heaprbtree승자
n=16, push/pop 반복~15ns~45nsmin-heap (3x)
n=1024, push/pop 반복~80ns~150nsmin-heap (1.9x)
n=1024, 임의 삭제 빈번~120ns~160nsmin-heap (1.3x)
n=10000, 순회 필요O(n log n) 정렬O(n)rbtree
n=10000, 범위 쿼리전수 검색O(log n + k)rbtree
실전 선택 기준: "가장 작은/큰 것 하나만 반복적으로 꺼내야 하는가?" → min-heap. "정렬된 순서로 접근하거나 특정 범위를 찾아야 하는가?" → rbtree. 확실하지 않으면 rbtree로 시작하고, 프로파일링(Profiling) 결과 peek()가 hot path이면 min-heap으로 전환하세요.

구현 패턴

min-heap을 커널 코드에서 사용할 때의 타입 안전성, 인라인 최적화, 비교자 커스터마이징 패턴을 정리합니다.

타입 안전 래퍼 패턴

/* 타입 안전한 힙 정의 */
struct timer_entry {
    u64  expires;
    void (*callback)(void *);
    void *data;
};

/* 전용 힙 타입 선언 */
DEFINE_MIN_HEAP_STATIC(struct timer_entry, timer_heap_t, 32);

/* 타입 안전 API 래퍼 */
static inline int timer_heap_push(struct timer_heap_t *heap,
        const struct timer_entry *entry)
{
    return min_heap_push(
        __min_heap_cast(heap),
        entry,
        sizeof(*entry),
        &timer_callbacks,
        NULL);
}

static inline struct timer_entry *timer_heap_peek(
        struct timer_heap_t *heap)
{
    return (struct timer_entry *)min_heap_peek(
        __min_heap_cast(heap));
}

인라인 최적화

min-heap API는 모두 static inline이므로, 비교자와 스왑 함수도 static inline으로 정의하면 컴파일러가 전체 경로를 단일 함수로 인라인 확장합니다.

/* 컴파일러가 인라인 확장하는 전체 경로:
 *
 * min_heap_push()
 *   └─ min_heap_sift_up()
 *        └─ less()        ← 인라인
 *        └─ swp()         ← 인라인 (또는 memcpy)
 *
 * 결과: 함수 호출 0회, 루프와 비교/교환만 남음
 */

/* GCC __attribute__((always_inline))으로 강제 인라인 */
static __always_inline bool my_less(const void *a,
        const void *b, void *args)
{
    return *(const u64 *)a < *(const u64 *)b;
}

복합 키 비교

/* 2차 정렬 키를 사용하는 비교자 */
static bool task_deadline_less(const void *lhs,
                                const void *rhs, void *args)
{
    const struct task_entry *a = lhs;
    const struct task_entry *b = rhs;

    /* 1차 키: 데드라인 */
    if (a->deadline != b->deadline)
        return a->deadline < b->deadline;

    /* 2차 키: 우선순위 (동률 시) */
    return a->priority < b->priority;
}

디버깅(Debugging)

min-heap 관련 버그는 주로 힙 불변식 위반, 경계 조건 오류, 비교자 논리 오류에서 발생합니다.

힙 불변식 검증

/* 디버그 빌드용 힙 불변식 검증 함수 */
static void min_heap_verify(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func, void *args)
{
    int i;

    for (i = 1; i < heap->nr; i++) {
        int parent = (i - 1) / 2;
        if (func->less(heap->data + i * elem_size,
                       heap->data + parent * elem_size, args)) {
            WARN(1, "min_heap invariant violated: "
                 "data[%d] < data[%d] (parent)\n",
                 i, parent);
        }
    }
    WARN_ON(heap->nr < 0 || heap->nr > heap->size);
}

일반적 버그 패턴

버그증상원인해결
비교자 방향 반전최댓값이 루트에 위치less()에서 > 사용비교 방향 확인
peek 후 pop 미수행같은 원소 반복 처리peek()가 제거하지 않음pop() 명시 호출
size 초과 push버퍼 오버플로(Buffer Overflow)full() 체크 누락push 반환값 확인
swp에서 인덱스 미갱신임의 삭제 시 잘못된 위치커스텀 swp 불완전swp에서 heap_index 갱신
비교자 비일관성간헐적 힙 속성 위반NaN, 오버플로 비교엄격한 약순서 보장
Min-Heap 디버그 흐름도 힙 동작 이상 발견 min_heap_verify() 실행 WARN 발생 통과 힙 불변식 위반 less() 검증 엄격한 약순서? swp() 검증 인덱스 갱신? 로직 오류 의심 경계 조건 nr/size 범위? 동시성 락 없이 접근? 디버깅 도구 ftrace (min_heap_push/pop 추적) | KASAN (버퍼 오버플로) | lockdep (동시 접근) | printk 덤프
힙 이상 시 불변식 검증을 먼저 수행하고, 결과에 따라 비교자/스왑/경계/동시성을 단계적으로 점검합니다

ftrace를 활용한 추적

# min_heap_push/pop 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/enable
echo 'p:push_probe min_heap_push' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/push_probe/enable
cat /sys/kernel/debug/tracing/trace_pipe
/* 힙 상태 덤프 함수 */
static void min_heap_dump(min_heap_t *heap, size_t elem_size)
{
    int i;
    pr_info("min_heap: nr=%d size=%d\n", heap->nr, heap->size);
    for (i = 0; i < heap->nr; i++) {
        u64 *val = heap->data + i * elem_size;
        pr_info("  [%d] = %llu (parent=%d)\n",
                i, *val, i ? (i - 1) / 2 : -1);
    }
}
동시성 주의: min-heap은 내부적으로 락을 제공하지 않습니다. 멀티코어 환경에서는 호출자가 spinlock이나 mutex로 보호해야 합니다. KASAN과 KCSAN을 활성화하면 동시 접근을 탐지할 수 있습니다.

관련 커널 설정

min-heap 자체는 별도 CONFIG 옵션 없이 항상 사용 가능하지만, 관련 서브시스템과 디버깅 도구의 설정을 정리합니다.

CONFIG 옵션영향설명
CONFIG_PERF_EVENTSperf_event 힙perf 멀티플렉싱에서 min-heap 활용
CONFIG_HIGH_RES_TIMERShrtimer 힙고해상도 타이머 큐 관리
CONFIG_SCHED_DEADLINEEDF 스케줄러데드라인 기반 태스크 우선순위
CONFIG_KASAN메모리 접근 검증힙 배열 경계 초과 탐지
CONFIG_KCSAN데이터 레이스 탐지락 없는 동시 접근 경고
CONFIG_FTRACE함수 추적min_heap API 호출 추적(Call Trace)
CONFIG_DEBUG_OBJECTS객체 수명 추적힙에 저장된 객체의 use-after-free 탐지

발전 역사

리눅스 커널의 min-heap 구현은 여러 서브시스템에서 독립적으로 구현되던 힙 코드를 통합 라이브러리로 정리한 결과입니다.

버전변경 사항
v5.12lib/min_heap.h로 최초 도입, perf 서브시스템의 힙 코드를 범용화
v5.15min_heap_del() 추가, 임의 인덱스 삭제 지원
v6.1콜백 구조체에 args 파라미터 추가
v6.5include/linux/min_heap.h로 이동, DEFINE_MIN_HEAP 매크로 도입
v6.7DEFINE_MIN_HEAP_STATIC 추가, 프리할당 배열 지원
v6.9타입 안전 래퍼 매크로 개선, __min_heap_cast 도입

고급 사용 패턴

decrease-key 연산

min-heap의 decrease-key는 특정 원소의 키를 감소시킨 후 힙 속성을 복원합니다. Dijkstra 알고리즘 등에서 핵심적으로 사용되며, 커널에서는 타이머 재설정에 해당합니다.

/* decrease-key 패턴: 키 감소 후 sift-up */
static void min_heap_decrease_key(min_heap_t *heap,
        size_t elem_size,
        const struct min_heap_callbacks *func,
        void *args, int pos, const void *new_val)
{
    /* 새 값이 현재 값보다 작은지 확인 */
    BUG_ON(!func->less(new_val,
            heap->data + pos * elem_size, args));

    /* 값 갱신 */
    memcpy(heap->data + pos * elem_size, new_val, elem_size);

    /* 키가 감소했으므로 sift-up만 필요 */
    min_heap_sift_up(heap, elem_size, func, args, pos);
}

Heap Sort 패턴

/* 힙 정렬: heapify 후 반복 pop */
static void heap_sort(void *data, int n, size_t elem_size,
        const struct min_heap_callbacks *func, void *args)
{
    min_heap_t heap = {
        .data = data,
        .nr   = n,
        .size = n,
    };

    /* O(n) 힙 구축 */
    min_heapify_all(&heap, elem_size, func, args);

    /* O(n log n) 정렬: 반복적으로 최솟값 추출 */
    while (heap.nr > 1)
        min_heap_pop(&heap, elem_size, func, args);

    /* 결과: data[]가 내림차순 정렬됨
     * (min-heap pop은 끝에서부터 채우므로) */
}

k-way 병합

/* k개의 정렬된 스트림을 병합하는 패턴 */
struct merge_entry {
    u64 value;      /* 현재 값 */
    int stream_id;  /* 출처 스트림 */
    int index;      /* 스트림 내 위치 */
};

/* k-way merge: 각 스트림의 현재 최솟값을 힙에 유지
 * peek() → 전체 최솟값, pop() → 해당 스트림에서 다음 값 push */

테스트

커널은 lib/test_min_heap.c에 min-heap 자체 테스트를 포함합니다. KUnit 프레임워크 기반으로 push/pop/heapify/delete의 정확성을 검증합니다.

# min-heap 테스트 모듈 빌드 및 실행
make -C /lib/modules/$(uname -r)/build M=lib \
    CONFIG_TEST_MIN_HEAP=m modules
insmod lib/test_min_heap.ko
dmesg | tail -20

# KUnit으로 실행 (v6.0+)
./tools/testing/kunit/kunit.py run min_heap

테스트 케이스

테스트검증 내용
test_heapify역순 배열 → heapify → 불변식 검증
test_push_pop무작위 삽입 후 반복 pop → 정렬순 추출 확인
test_del임의 인덱스 삭제 후 불변식 유지 확인
test_empty빈 힙에서 pop/peek → NULL/no-op 확인
test_full가득 찬 힙에서 push → 실패 확인

대안 자료구조

min-heap 외에 커널에서 사용되는 우선순위 기반 자료구조를 비교합니다.

자료구조peekpushpop임의 삭제커널 사용처
min-heapO(1)O(log n)O(log n)O(log n)*perf, 타이머
rbtreeO(1)**O(log n)O(log n)O(log n)CFS, VMA, I/O
sorted listO(1)O(n)O(1)O(1)***소규모 타이머
timer wheelO(1)O(1)O(1)†O(1)저해상도 타이머
skip listO(1)O(log n)O(1)O(log n)미사용

* 인덱스 추적 필요 | ** rb_root_cached 사용 시 | *** 노드 포인터 보유 시 | † 슬롯 그라뉼래리티

동시성 고려사항

min-heap은 내부적으로 어떤 동기화 메커니즘도 제공하지 않습니다. 멀티코어 환경에서는 호출자가 반드시 적절한 락으로 보호해야 합니다.

락 선택 가이드

컨텍스트권장 락이유예시
프로세스(Process) 컨텍스트만mutex슬립(Sleep) 가능, 오버헤드 낮음사용자 공간(User Space) 요청 처리
인터럽트 컨텍스트spin_lock_irqsaveIRQ 비활성화 필수hrtimer 콜백 내 힙 접근
softirq 컨텍스트spin_lock_bhbottom-half 비활성화네트워크 패킷(Packet) 우선순위 큐
per-CPU 힙local_lock + preempt_disableCPU 마이그레이션 방지per-CPU 타이머 힙
/* 인터럽트 안전한 힙 접근 패턴 */
struct safe_heap {
    spinlock_t lock;
    DEFINE_MIN_HEAP_STATIC(u64, heap, 64);
};

static void safe_push(struct safe_heap *sh, u64 val)
{
    unsigned long flags;

    spin_lock_irqsave(&sh->lock, flags);
    min_heap_push(__min_heap_cast(&sh->heap),
                  &val, sizeof(val), &callbacks, NULL);
    spin_unlock_irqrestore(&sh->lock, flags);
}

static bool safe_pop(struct safe_heap *sh, u64 *out)
{
    unsigned long flags;
    u64 *p;

    spin_lock_irqsave(&sh->lock, flags);
    p = min_heap_peek(__min_heap_cast(&sh->heap));
    if (p) {
        *out = *p;
        min_heap_pop(__min_heap_cast(&sh->heap),
                     sizeof(*p), &callbacks, NULL);
    }
    spin_unlock_irqrestore(&sh->lock, flags);

    return p != NULL;
}
TOCTOU 주의: peek()pop() 사이에 다른 CPU가 힙을 변경할 수 있습니다. 반드시 동일한 임계 영역(Critical Section) 내에서 peek와 pop을 수행하세요.

락 없는 peek 최적화

특수한 경우, peek만 수행하는 읽기 경로는 READ_ONCE()로 보호할 수 있습니다. 단, 이는 원소가 원자적(Atomic)으로 읽을 수 있는 크기(8바이트 이하)이고, 힙 자체의 불변식 위반이 허용되는 경우에만 가능합니다.

/* 락 없는 대략적 최솟값 조회 (근사적 결과 허용 시) */
static u64 approx_min(struct safe_heap *sh)
{
    int nr = READ_ONCE(sh->heap.nr);
    if (nr > 0)
        return READ_ONCE(sh->heap.data[0]);
    return U64_MAX;
}
/* 주의: 반환값은 실제 최솟값과 다를 수 있음
 * heuristic/hint 용도로만 사용 */

실전 코드 사례

커널 소스에서 min-heap이 실제로 사용되는 대표적인 코드 경로를 분석합니다.

perf_event_context의 힙

/* kernel/events/core.c */
struct perf_event_context {
    /* ... */
    struct min_heap_struct {
        int nr;
        int size;
        struct perf_event **data;
        struct perf_event *preallocated[0];
    } heap;
    /* ... */
};

/* 컨텍스트 생성 시 힙 초기화 */
static void perf_event_ctx_init(struct perf_event_context *ctx,
                                 int max_events)
{
    ctx->heap.nr   = 0;
    ctx->heap.size = max_events;
    ctx->heap.data = kmalloc_array(max_events,
            sizeof(struct perf_event *), GFP_KERNEL);
}

bcache의 힙

블록 캐시 서브시스템 bcache는 가비지 컬렉션에서 가장 오래된 버킷을 추적하는 데 자체 힙 구현을 사용합니다. 이는 커널 범용 min_heap API 도입 이전에 작성되어 독자적인 매크로를 유지합니다.

/* drivers/md/bcache/bset.h — bcache 자체 힙 */
#define heap_peek(h)    ((h)->data[0])
#define heap_full(h)    ((h)->used == (h)->size)

/* GC 대상 선택: 가장 적게 사용된 버킷 */
static void invalidate_buckets_lru(struct cache *ca)
{
    while (!heap_empty(&ca->heap)) {
        struct bucket *b = heap_peek(&ca->heap);
        /* 가장 오래된 버킷 무효화 */
        heap_pop(&ca->heap, ...);
        __bch_invalidate_one_bucket(ca, b);
    }
}

thermal 서브시스템의 정렬

thermal 서브시스템에서 쿨링 디바이스 선택 시, 제한된 수의 후보 중 가장 효율적인 것을 선택하는 패턴에서 min-heap 방식의 비교가 활용됩니다.

이벤트 폴링(Polling) 시나리오

다중 소스에서 이벤트를 수집하고 타임스탬프 순서로 처리해야 하는 시나리오에서 min-heap은 자연스러운 선택입니다.

/* 다중 소스 이벤트 정렬 처리 */
struct event {
    u64  timestamp;
    u32  source_id;
    u32  type;
    void *payload;
};

static bool event_ts_less(const void *a, const void *b,
                          void *args)
{
    return ((const struct event *)a)->timestamp <
           ((const struct event *)b)->timestamp;
}

/* 각 소스에서 이벤트를 읽어 힙에 삽입
 * → peek()로 전역 최소 타임스탬프 결정
 * → pop() 후 해당 소스에서 다음 이벤트 push */
static void process_events_ordered(struct event_source *sources,
                                   int n_sources)
{
    DEFINE_MIN_HEAP_STATIC(struct event, ev_heap, 64);
    struct ev_heap heap = { .nr = 0, .size = 64,
                           .data = heap.preallocated };
    const struct min_heap_callbacks cbs = { .less = event_ts_less };
    int i;

    /* 각 소스의 첫 이벤트를 힙에 push */
    for (i = 0; i < n_sources; i++) {
        struct event e;
        if (source_read(&sources[i], &e) == 0)
            min_heap_push(__min_heap_cast(&heap),
                          &e, sizeof(e), &cbs, NULL);
    }

    /* 타임스탬프 순서로 처리 */
    while (heap.nr > 0) {
        struct event *min = min_heap_peek(
            __min_heap_cast(&heap));
        handle_event(min);

        int src = min->source_id;
        min_heap_pop(__min_heap_cast(&heap),
                     sizeof(struct event), &cbs, NULL);

        /* 해당 소스에서 다음 이벤트 push */
        struct event next;
        if (source_read(&sources[src], &next) == 0)
            min_heap_push(__min_heap_cast(&heap),
                          &next, sizeof(next), &cbs, NULL);
    }
}
통합 추세: 커널 6.x에서 범용 min_heap.h가 안정화되면서, bcache 등 독자적 힙 구현을 범용 API로 전환하는 패치(Patch)가 진행 중입니다. 새 코드에서는 반드시 include/linux/min_heap.h를 사용하세요.

성능 튜닝

min-heap의 성능을 극대화하기 위한 튜닝 포인트를 정리합니다.

원소 크기 최적화

sift-up/sift-down 과정에서 원소를 교환(swap)할 때 memcpy가 사용됩니다. 원소 크기가 크면 교환 비용이 증가하므로, 큰 구조체를 직접 저장하는 대신 포인터를 저장하는 것이 일반적입니다.

저장 방식원소 크기swap 비용cache 효과적합한 경우
값 직접 저장원래 크기높음 (memcpy)우수 (연속)8~16바이트 이하
포인터 저장8바이트낮음 (포인터 swap)보통 (간접 참조)큰 구조체
/* 포인터 힙: 큰 구조체에 적합 */
DEFINE_MIN_HEAP_STATIC(struct large_obj *, ptr_heap, 128);

/* 값 힙: 작은 타입에 적합 */
DEFINE_MIN_HEAP_STATIC(u64, val_heap, 128);

분기 예측(Branch Prediction) 힌트

/* likely/unlikely로 hot path 최적화 */
static inline int optimized_push(min_heap_t *heap,
        const void *elem, size_t sz,
        const struct min_heap_callbacks *f, void *args)
{
    /* 힙이 가득 찬 경우는 드묾 */
    if (unlikely(heap->nr >= heap->size))
        return -1;

    int pos = heap->nr++;
    memcpy(heap->data + pos * sz, elem, sz);

    /* 대부분의 삽입은 sift-up 없이 끝남 */
    if (likely(pos == 0 || !f->less(
            heap->data + pos * sz,
            heap->data + ((pos - 1) / 2) * sz, args)))
        return 0;

    min_heap_sift_up(heap, sz, f, args, pos);
    return 0;
}

프리페치 최적화

/* sift-down 시 다음 레벨 프리페치 */
while (1) {
    int left  = 2 * pos + 1;
    int right = 2 * pos + 2;

    /* 다다음 레벨 프리페치 (2 레벨 ahead) */
    if (4 * pos + 3 < heap->nr)
        prefetch(data + (4 * pos + 3) * elem_size);

    /* ... 비교 및 교환 ... */
}

모범 사례

안티패턴: min-heap을 정렬된 컨테이너로 사용하지 마세요. 형제 노드 간에는 순서가 없으므로, "두 번째로 작은 값"을 찾으려면 data[1]과 data[2]를 모두 확인해야 합니다. 정렬순 접근이 필요하면 rbtree를 사용하세요.

코드 리뷰 체크리스트

항목확인 사항
초기화nr=0, size 설정, data 포인터 유효
비교자엄격한 약순서(strict weak ordering) 보장
스왑커스텀 swp에서 모든 부가 데이터(인덱스 등) 갱신
경계push 전 full() 검사, pop 전 empty() 검사
수명peek() 포인터 사용 범위가 다음 변경 전인지
동시성모든 접근이 동일한 락으로 보호
해제동적 할당 시 kfree(heap.data) 호출
오버플로nr과 size가 int 범위 내인지 (대규모 힙)
커널 코딩 스타일(Coding Style): min-heap 관련 변수명은 관례적으로 heap을 사용하고, 비교자 구조체는 *_callbacks 접미사를 붙입니다. 타입별 힙은 *_heap_t 또는 *_heap 접미사를 사용합니다.

종합 요약 테이블

특성
헤더 파일include/linux/min_heap.h
도입 버전v5.12 (lib/min_heap.h), v6.5 (include/linux/min_heap.h)
자료구조배열 기반 완전 이진 트리
핵심 불변식parent(i) ≤ child(i) (min-heap property)
peekO(1) — data[0]
push/popO(log n) — sift-up / sift-down
heapifyO(n) — 역순 sift-down
메모리 오버헤드0 (원소 외 포인터 없음)
캐시 성능우수 (연속 메모리, 프리페치 가능)
동시성호출자 책임 (내장 락 없음)
커널 사용처perf_event, hrtimer, bcache, SCHED_DEADLINE

min_heap API 상세

커널 v6.5 이후 include/linux/min_heap.h에 정의된 min-heap API는 제네릭 매크로(Generic Macro) 기반으로 타입 안전성(Type Safety)을 보장합니다. 각 API의 내부 동작과 사용 패턴을 상세히 분석합니다.

DEFINE_MIN_HEAP 매크로

/* include/linux/min_heap.h */

/* 타입별 min_heap 정의 매크로 */
#define DEFINE_MIN_HEAP(type, name)          \
struct name {                                   \
    int nr;         /* 현재 원소 수 */        \
    int size;       /* 최대 용량 */          \
    type *data;     /* 원소 배열 포인터 */   \
    type preallocated[0]; /* 인라인 저장소 */ \
}

/* 사용 예: u64 타입의 min_heap */
DEFINE_MIN_HEAP(u64, my_u64_heap);

/* 스택에 인라인 저장소 포함 선언 */
DECLARE_MIN_HEAP(u64, 64) my_heap;
/* → nr=0, size=64, data=preallocated 배열 */

min_heap_push() 내부 동작

/* push 동작 단계 */
bool min_heap_push(struct min_heap *heap,
                   const void *element,
                   const struct min_heap_callbacks *funcs,
                   void *args)
{
    /* 1. 용량 초과 검사 */
    if (heap->nr >= heap->size)
        return false;  /* 실패: 힙 가득 참 */

    /* 2. 배열 끝에 원소 복사 */
    memcpy(heap->data + heap->nr * elem_size,
           element, elem_size);
    heap->nr++;

    /* 3. sift-up: 힙 속성 복원 */
    min_heap_sift_up(heap, heap->nr - 1, funcs, args);

    return true;
}

min_heap_pop() 내부 동작

/* pop 동작 단계 */
bool min_heap_pop(struct min_heap *heap,
                  const struct min_heap_callbacks *funcs,
                  void *args)
{
    if (heap->nr <= 0)
        return false;

    /* 1. 마지막 원소를 root(index 0)에 복사 */
    heap->nr--;
    memcpy(heap->data, heap->data + heap->nr * elem_size,
           elem_size);

    /* 2. sift-down: 힙 속성 복원 */
    min_heap_sift_down(heap, 0, funcs, args);

    return true;
}

min_heap_peek() — O(1) 최솟값 접근

/* peek: 단순 배열 첫 번째 원소 포인터 반환 */
static inline void *min_heap_peek(struct min_heap *heap)
{
    return heap->nr > 0 ? heap->data : NULL;
}

/* 사용 예 */
u64 *min_val = min_heap_peek(&my_heap);
if (min_val)
    pr_info("minimum = %llu\n", *min_val);

API 전체 요약

API복잡도반환설명
min_heap_push()O(log n)bool (성공/실패)원소 삽입 + sift-up
min_heap_pop()O(log n)bool (성공/실패)최솟값 제거 + sift-down
min_heap_peek()O(1)void * (또는 NULL)최솟값 포인터 반환 (제거하지 않음)
min_heapify_all()O(n)void임의 배열을 힙으로 변환
min_heap_sift_up()O(log n)void지정 인덱스에서 위로 이동
min_heap_sift_down()O(log n)void지정 인덱스에서 아래로 이동
min_heap_del()O(log n)bool임의 인덱스 원소 삭제
min_heap_full()O(1)bool힙 가득 찼는지 확인
min_heap_empty()O(1)bool힙 비었는지 확인

커스텀 비교 함수 작성

min_heap_callbackslessswp 함수 포인터를 통해 임의 타입의 비교와 교환을 커스터마이즈할 수 있습니다.

min_heap_callbacks 구조체

struct min_heap_callbacks {
    int elem_size;                        /* 원소 크기 (바이트) */
    bool (*less)(const void *lhs,
                 const void *rhs,
                 void *args);             /* 비교 함수 */
    void (*swp)(void *lhs,
                void *rhs,
                void *args);              /* 교환 함수 (NULL 가능) */
};

비교 함수 예제

/* 예제 1: 단순 u64 비교 */
static bool u64_less(const void *lhs, const void *rhs, void *args)
{
    return *(const u64 *)lhs < *(const u64 *)rhs;
}

/* 예제 2: 구조체 필드 기준 비교 */
struct task_entry {
    u64  deadline;     /* 정렬 기준 */
    pid_t pid;
    char  comm[16];
};

static bool task_less(const void *lhs, const void *rhs, void *args)
{
    const struct task_entry *a = lhs;
    const struct task_entry *b = rhs;
    return a->deadline < b->deadline;
}

/* 예제 3: 복합 키 비교 (deadline 동일 시 pid로 구분) */
static bool task_less_compound(const void *lhs, const void *rhs,
                               void *args)
{
    const struct task_entry *a = lhs;
    const struct task_entry *b = rhs;

    if (a->deadline != b->deadline)
        return a->deadline < b->deadline;
    return a->pid < b->pid;
}

/* 콜백 등록 */
static const struct min_heap_callbacks task_funcs = {
    .elem_size = sizeof(struct task_entry),
    .less = task_less_compound,
    .swp = NULL,   /* NULL이면 기본 memcpy swap 사용 */
};

커스텀 swap 함수

swp이 NULL이면 memcpy 기반 기본 교환이 사용됩니다. 원소에 부가 인덱스를 관리해야 하는 경우(perf_event 등) 커스텀 swap이 필요합니다:

/* perf_event 스타일: 위치 추적이 필요한 swap */
struct indexed_entry {
    u64 key;
    int *heap_index;    /* 역참조: 원소가 힙 내 몇 번째인지 */
};

static void indexed_swap(void *lhs, void *rhs, void *args)
{
    struct indexed_entry *a = lhs;
    struct indexed_entry *b = rhs;
    struct indexed_entry tmp;

    /* 1. 원소 교환 */
    tmp = *a;
    *a = *b;
    *b = tmp;

    /* 2. 역참조 인덱스 업데이트 */
    if (a->heap_index)
        *a->heap_index = ((char *)a - (char *)heap_base) / sizeof(*a);
    if (b->heap_index)
        *b->heap_index = ((char *)b - (char *)heap_base) / sizeof(*b);
}

커널 서브시스템 활용 상세

min-heap은 커널의 여러 핵심 서브시스템에서 우선순위 큐(Priority Queue)로 사용됩니다. 각 사용처의 구체적 패턴을 분석합니다.

perf_event 멀티플렉싱 상세

PMU(Performance Monitoring Unit) 카운터 수보다 모니터링할 이벤트가 많으면, perf는 min-heap을 사용하여 이벤트를 시분할 스케줄링합니다:

단계동작min-heap 역할
1이벤트 등록push()로 이벤트를 가중치(weight) 기준 삽입
2라운드 시작peek()로 다음 스케줄링할 이벤트 그룹 선택
3카운터 할당가중치 가장 낮은(가장 적게 실행된) 이벤트 우선
4라운드 종료pop() + 가중치 업데이트 + push()
5이벤트 해제del()로 임의 위치 삭제

타이머 큐 활용

hrtimer 서브시스템은 rbtree를 기본 사용하지만, bcache와 같은 하위 시스템에서는 min-heap으로 만료 시간 기반 우선순위 큐를 구현합니다:

/* bcache 타이머 힙 사용 패턴 (drivers/md/bcache) */
struct bch_timer {
    u64 expires;         /* 만료 시간 (jiffies 또는 ktime) */
    void (*fn)(struct bch_timer *);
};

/* 비교: 가장 빨리 만료되는 타이머가 root */
static bool timer_less(const void *a, const void *b, void *args)
{
    return ((struct bch_timer *)a)->expires <
           ((struct bch_timer *)b)->expires;
}

/* 다음 만료 시간 O(1) 조회 */
struct bch_timer *next = min_heap_peek(&timer_heap);
if (next && time_after_eq(jiffies, next->expires)) {
    next->fn(next);
    min_heap_pop(&timer_heap, &timer_funcs, NULL);
}

CFS 런큐에서의 가능성

현재 CFS는 rbtree(rb_root_cached)를 사용하지만, EEVDF(Earliest Eligible Virtual Deadline First) 스케줄러로의 전환에서 min-heap이 논의되었습니다:

기준rbtree (CFS 현재)min-heap (대안)
pick_next 비용O(1) (rb_root_cached)O(1) (peek)
enqueue 비용O(log n)O(log n)
dequeue 비용O(log n)O(log n)
임의 원소 삭제O(log n) — 직접 접근O(n) — 인덱스 검색 필요 (또는 역참조)
순회O(n) — 정렬 순서정렬 순서 보장 안 됨
채택 결정유지미채택 (임의 삭제 비효율)
min-heap 커널 사용처 맵 min_heap API include/linux/min_heap.h perf_event 멀티플렉싱 PMU 카운터 시분할, 가중치 기반 스케줄링 bcache 타이머/IO 스케줄링 SSD 캐시 만료, IO 우선순위 관리 SCHED_DEADLINE (EDF) 절대 데드라인 기준, 가장 급한 태스크 선택 커스텀 커널 모듈 이벤트 큐, 작업 스케줄링, 우선순위 관리 CFS 대역폭 제어 cgroup 스로틀링, 런타임 할당 관리 네트워크 서브시스템 패킷 스케줄링, QoS 우선순위 큐
min-heap API를 사용하는 주요 커널 서브시스템

인라인 최적화와 제네릭 매크로

min-heap API는 헤더 파일에 static inline으로 정의되어 호출 오버헤드가 없습니다. 컴파일러가 비교 함수까지 인라인(Inline)하면 함수 포인터 호출 비용도 제거됩니다.

인라인 최적화 이점

최적화효과조건
함수 인라인호출 오버헤드 제거 (~5ns/call)static inline 정의
비교 함수 인라인간접 호출 제거, 분기 예측 향상less 함수가 같은 TU에 정의
상수 전파elem_size가 컴파일 타임 상수로 전파DEFINE_MIN_HEAP 사용
루프 언롤링sift-up/down 루프 최적화작은 힙 크기 (컴파일 타임 결정)
SIMD 벡터화memcpy swap 벡터화elem_size가 레지스터 크기 배수

타입 안전성 패턴

/* v6.5+ 타입 안전 매크로 */
DEFINE_MIN_HEAP(u64, u64_heap);

static struct u64_heap my_heap = {
    .data = (u64[128]){},
    .nr = 0,
    .size = 128,
};

/* 타입 불일치 시 컴파일 오류 발생 (v6.5+) */
u64 val = 42;
min_heap_push(&my_heap, &val, &u64_funcs, NULL);

/* 잘못된 타입 — 컴파일 경고/오류 */
u32 wrong = 42;
/* min_heap_push(&my_heap, &wrong, &u64_funcs, NULL); */
/* → 크기 불일치로 데이터 손상 위험 */

실전 커널 모듈 예제

min-heap API를 사용하는 완전한 커널 모듈 예제입니다. 우선순위 기반 작업 큐를 구현합니다.

/*
 * min_heap_demo.c -- min-heap 기반 우선순위 큐 데모
 *
 * 빌드: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
 * 로드: insmod min_heap_demo.ko
 * 로그: dmesg | grep heap_demo
 */

#include <linux/module.h>
#include <linux/min_heap.h>
#include <linux/random.h>

MODULE_LICENSE("GPL");

struct work_item {
    u32 priority;    /* 낮을수록 높은 우선순위 */
    u32 work_id;
};

static bool work_less(const void *a, const void *b, void *args)
{
    return ((const struct work_item *)a)->priority <
           ((const struct work_item *)b)->priority;
}

static const struct min_heap_callbacks work_funcs = {
    .elem_size = sizeof(struct work_item),
    .less = work_less,
    .swp = NULL,
};

#define HEAP_CAPACITY 64
static struct work_item heap_storage[HEAP_CAPACITY];
static struct min_heap work_heap = {
    .data = heap_storage,
    .nr = 0,
    .size = HEAP_CAPACITY,
};

static int __init heap_demo_init(void)
{
    struct work_item item, *top;
    int i;

    /* 20개 랜덤 우선순위 작업 삽입 */
    for (i = 0; i < 20; i++) {
        item.priority = get_random_u32() % 1000;
        item.work_id = i;
        min_heap_push(&work_heap, &item, &work_funcs, NULL);
        pr_info("heap_demo: pushed work_id=%u prio=%u\n",
                item.work_id, item.priority);
    }

    /* 최우선 작업 확인 */
    top = min_heap_peek(&work_heap);
    if (top)
        pr_info("heap_demo: top priority: id=%u prio=%u\n",
                top->work_id, top->priority);

    /* 우선순위 순서대로 5개 처리 */
    for (i = 0; i < 5 && work_heap.nr > 0; i++) {
        top = min_heap_peek(&work_heap);
        pr_info("heap_demo: processing id=%u prio=%u\n",
                top->work_id, top->priority);
        min_heap_pop(&work_heap, &work_funcs, NULL);
    }

    pr_info("heap_demo: %d items remaining\n", work_heap.nr);
    return 0;
}

static void __exit heap_demo_exit(void)
{
    work_heap.nr = 0;
    pr_info("heap_demo: cleaned up\n");
}

module_init(heap_demo_init);
module_exit(heap_demo_exit);

힙 vs 다른 자료구조 심층 비교

우선순위 큐가 필요할 때 min-heap, rbtree, 정렬 리스트 중 어떤 것을 선택해야 하는지 구체적 기준을 제시합니다.

기준min-heaprbtree (rb_root_cached)정렬 리스트 (list_head)
최솟값 조회O(1) peekO(1) rb_first_cachedO(1) list_first
삽입O(log n)O(log n)O(n) — 위치 검색
최솟값 제거O(log n) popO(log n) eraseO(1) list_del
임의 원소 삭제O(n) 검색 + O(log n)O(log n) 직접O(1) 직접
정렬 순서 순회X (배열은 부분 정렬)O(n) 순회O(n) 순회
캐시 효율우수 (연속 배열)보통 (분산 노드)나쁨 (포인터 체이싱)
메모리 오버헤드0 (인라인 배열)24B/노드16B/노드
동적 크기X (고정 크기 배열)O (동적)O (동적)
적합 시나리오원소 수 제한, 최솟값 위주범용, 임의 삭제 빈번원소 수 적음 (<100)
우선순위 큐 자료구조 선택 플로우차트 우선순위 기반 처리가 필요? Yes 최대 원소 수를 미리 알 수 있는가? Yes 임의 삭제 필요? No min-heap Yes rb_root_cached No 원소 수 <100? Yes 정렬 리스트 No rbtree min-heap: 고정 크기 + 최솟값 위주 | rbtree: 동적 크기 + 임의 접근 | 정렬 리스트: 소규모 + 단순
커널 우선순위 큐 자료구조 선택 가이드: 워크로드 특성에 따른 최적 선택
선택 요약: 원소 수가 제한적이고 최솟값 추출이 주 연산이면 min-heap, 원소 수가 동적이고 임의 삭제가 빈번하면 rbtree, 원소가 매우 적으면(<100) 정렬 리스트가 최적입니다. perf_event는 카운터 수가 제한적이므로 min-heap, CFS는 임의 태스크 디스패치가 필요하므로 rbtree를 사용합니다.

힙 동시성 보호 패턴

min-heap API에는 내장 동기화가 없으므로, 멀티코어 환경에서는 호출자가 적절한 보호를 제공해야 합니다.

락 선택 가이드

/* 패턴 1: spinlock (인터럽트 컨텍스트 포함) */
static DEFINE_SPINLOCK(heap_lock);

void safe_push(struct work_item *item)
{
    unsigned long flags;
    spin_lock_irqsave(&heap_lock, flags);
    min_heap_push(&work_heap, item, &work_funcs, NULL);
    spin_unlock_irqrestore(&heap_lock, flags);
}

/* 패턴 2: per-CPU 힙 (경합 최소화) */
static DEFINE_PER_CPU(struct min_heap, percpu_heap);
static DEFINE_PER_CPU(struct work_item[64], percpu_storage);

void percpu_push(struct work_item *item)
{
    struct min_heap *heap;

    preempt_disable();
    heap = this_cpu_ptr(&percpu_heap);
    min_heap_push(heap, item, &work_funcs, NULL);
    preempt_enable();
}

/* 패턴 3: RCU + 복사-교체 (읽기 빈번) */
/* min-heap의 배열 특성 때문에 RCU 직접 적용은 어려움.
 * 전체 힙을 복사 후 교체하는 패턴이 필요하며,
 * 원소 수가 적을 때만 실용적. */

에러 처리 패턴

상황API 동작처리 방법
push 시 힙 가득 참false 반환로그 출력 + 백프레셔 또는 확장
pop 시 힙 비어 있음false 반환대기 또는 기본값 사용
peek 시 힙 비어 있음NULL 반환NULL 검사 필수
del 시 잘못된 인덱스정의되지 않음인덱스 범위 검사 필수
고정 크기 제한: min-heap은 배열 기반이므로 런타임 크기 확장이 기본적으로 불가합니다. 최대 원소 수를 설계 시점에 결정해야 합니다. 동적 크기가 필요하면 rbtree(rb_root_cached)를 사용하거나, 힙 배열을 krealloc()로 확장하는 래퍼를 작성하세요.

min-heap과 관련된 다른 자료구조 및 서브시스템을 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

참고 자료