Sorting (lib/sort & lib/list_sort)

리눅스 커널이 제공하는 두 가지 범용 정렬 API를 심층 분석합니다. lib/sort.c의 in-place heapsort는 배열을 O(n log n) 최악 보장으로 정렬하고, lib/list_sort.c의 bottom-up merge sort는 연결 리스트(Linked List)를 안정 정렬합니다. quicksort를 배제한 설계 이유, 비교자(comparator) 작성 패턴, swap 최적화 전략, 2:1 비율 병합의 수학적 근거, readdir/extent/slab/네트워크 등 커널 전반의 실제 호출 사례, 성능 벤치마크와 구현 역사까지 포괄합니다.

전제 조건: 연결 리스트 문서를 먼저 읽으세요. list_sort()는 커널 이중 연결 리스트를 직접 조작하므로, list_head의 구조와 순회 매크로(Macro)(list_for_each_entry 등)를 이해해야 합니다.
일상 비유: 커널의 정렬은 도서관 사서의 책 정리와 같습니다. sort()는 책꽂이(배열) 안에서 책을 직접 교환(swap)하며 정리하는 방식이고 — 추가 공간이 필요 없지만 같은 제목의 책 순서가 바뀔 수 있습니다(불안정). list_sort()는 카드 뭉치(리스트)를 반으로 나누어 각각 정렬한 뒤 합치는 방식이고 — 같은 제목의 책은 원래 순서를 유지합니다(안정).

핵심 요약

  • 두 가지 정렬 API — 배열용 sort()(heapsort)와 리스트용 list_sort()(merge sort). 자료구조에 맞는 것을 선택합니다.
  • quicksort 배제 — 최악 O(n²), 재귀 스택 소비, 공격 가능한 pivot 선택 문제로 2006년 커널에서 제거되었습니다.
  • 비교자 주도 — 정렬 로직은 범용이고, 호출자가 비교 함수(cmp_func_t)를 전달하여 정렬 기준을 결정합니다.
  • swap 최적화sort()는 word 크기 정렬된 요소에 대해 word-at-a-time swap을 수행하여 바이트 단위보다 4~8배 빠릅니다.
  • 안정성 차이sort()는 불안정(unstable), list_sort()는 안정(stable). 안정 정렬이 필요하면 반드시 list_sort()를 사용합니다.

단계별 이해

  1. 정렬 개념 복습
    비교 기반 정렬의 하한(O(n log n))과 안정성 개념을 확인합니다.
  2. heapsort 원리 이해
    max-heap 구성 후 루트 추출 반복으로 정렬하는 과정을 시각화합니다.
  3. merge sort 원리 이해
    분할-병합 패턴과 리스트 특화 bottom-up 방식의 차이를 파악합니다.
  4. API 사용법 학습
    sort(), sort_r(), list_sort()의 시그니처와 비교자 작성 패턴을 익힙니다.
  5. 실전 호출 사례 분석
    readdir, extent 정렬, slab freelist, 네트워크 등 커널 내 호출 지점을 탐색합니다.
소스 위치: lib/sort.c (heapsort 배열 정렬), lib/list_sort.c (merge sort 리스트 정렬), include/linux/sort.h, include/linux/list_sort.h. 관련 헤더의 자체 테스트는 lib/test_sort.c, lib/test_list_sort.c에 있습니다.

커널 정렬 개요

커널 vs 사용자 공간(User Space) 정렬: 사용자 공간의 qsort(3)는 재귀 깊이, 스택 크기, 최악 시간에 대해 느슨한 제약을 가집니다. 커널은 (1) 스택이 8~16KB로 극도로 제한되고, (2) 동적 메모리 할당이 실패할 수 있으며, (3) 인터럽트(Interrupt) 컨텍스트에서도 호출될 수 있어, 이 세 가지 제약이 알고리즘 선택을 결정합니다.

리눅스 커널은 사용자 공간의 qsort(3)와 달리, 두 가지 범용 정렬 함수를 제공합니다. 각각은 대상 자료구조에 최적화되어 있습니다.

항목sort() — lib/sort.clist_sort() — lib/list_sort.c
자료구조연속 배열 (void *base)이중 연결 리스트
알고리즘Heapsort (sift-down)Bottom-up merge sort
시간 복잡도O(n log n) 최악 보장O(n log n) 최악 보장
공간 복잡도O(1) in-placeO(1) 추가 (포인터 재연결)
안정성불안정 (unstable)안정 (stable)
비교자cmp_func_t / cmp_r_func_tlist_cmp_func_t
도입 시점2.6.x (qsort 대체, 2006)2.6.x (2009, George Spelvin 재작성 2019)
설계 원칙: 커널 정렬 API는 범용성예측 가능성을 우선합니다. 최악 시간이 보장되어야 하고, 재귀 깊이가 제한되어야 하며, 추가 메모리 할당 없이 동작해야 합니다. 이 세 가지 제약이 알고리즘 선택을 결정합니다.

두 함수는 모두 비교 함수(comparator)를 콜백(Callback)으로 받아 정렬 기준을 결정합니다. 정렬 로직 자체는 비교 독립적이므로, 하나의 구현으로 정수, 포인터, 구조체(Struct) 등 모든 타입에 대응합니다.

사용 빈도와 선택 기준

최근 메인라인 계열에서 두 API의 사용 경향을 보면:

기준sort()list_sort()
호출 사이트 수~110곳~75곳
주요 사용처파일시스템(Filesystem), ACPI, perfDRM, 네트워크, PM
선택 이유데이터가 배열에 이미 존재데이터가 리스트로 관리됨
전형적 규모수십~수천 개수십~수백 개
배열 vs 리스트 전환: 데이터가 리스트에 있지만 캐시(Cache) 성능이 중요하면, 배열로 복사 → sort() → 리스트 재구성 패턴이 더 빠를 수 있습니다. 반대로 데이터가 배열에 있지만 안정 정렬이 필요하면, 임시 리스트로 변환 → list_sort() → 배열에 복사할 수 있습니다. 두 경우 모두 추가 O(n) 시간이 소요됩니다.

왜 Quicksort가 아닌가

사용자 공간에서 가장 널리 쓰이는 quicksort가 커널에서 배제된 이유는 세 가지입니다.

문제QuicksortHeapsort (커널 선택)
최악 시간O(n²) — 정렬된/역순 입력O(n log n) — 항상 보장
스택 사용O(n) 최악 재귀 깊이O(1) — 반복(iterative)
공격 표면pivot 선택 조작 가능입력 패턴 무관
Quicksort vs Heapsort 비교 최악 시간 복잡도, 스택 사용량, 안정성 비교 다이어그램 Quicksort vs Heapsort: 커널 관점 비교 Quicksort (배제됨) 최악 O(n²) - 정렬된 입력에서 발생 재귀 깊이 O(n) - 커널 스택 8~16KB 초과 pivot 조작 → DoS 공격 가능 불안정(unstable) 정렬 2006년 커널에서 제거 Heapsort (선택됨) 항상 O(n log n) - 입력 무관 스택 O(1) - 반복 구현, 재귀 없음 입력 패턴 독립 - 공격 표면 없음 in-place O(1) 추가 메모리 lib/sort.c 현재 구현
커널 스택 제약: 커널 스택은 아키텍처에 따라 8KB(x86-32) 또는 16KB(x86-64)입니다. quicksort의 최악 재귀 깊이 O(n)은 수천 개 요소만으로도 스택 오버플로(Stack Overflow)우를 유발할 수 있습니다. 이는 단순 성능 문제가 아니라 시스템 안정성 문제입니다.

introsort 대안 검토

introsort(quicksort + heapsort 하이브리드, C++ STL의 std::sort)도 검토 대상이었습니다:

항목IntrosortHeapsort (커널 선택)
평균 성능quicksort 수준 (최고)quicksort보다 ~20% 느림
최악 보장O(n log n) (heapsort 폴백)O(n log n)
코드 복잡도높음 (3개 알고리즘 결합)낮음 (단일 알고리즘)
스택 사용O(log n) (재귀 제한)O(1)
유지보수복잡간단

커널에서 introsort를 선택하지 않은 이유: (1) 코드 복잡도 대비 성능 이득이 미미하고, (2) 커널의 정렬 대상은 대부분 수백~수천 개로 작아 평균 성능 차이가 미미하며, (3) 단순한 구현이 버그 위험을 줄입니다.

초기 커널(2.6.11 이전)에는 lib/qsort.c가 존재했지만, Matt Mackall이 2006년에 heapsort로 교체했습니다. 교체 커밋 메시지에는 다음과 같은 근거가 명시되었습니다:

"A fast, small, non-recursive O(nlog n) sort for the Linux kernel.

This performs a heapsort. Unlike the textbook version, it is iterative rather than recursive, and uses a single heapify() pass to build the initial heap.

The comparison function should return negative/zero/positive for less/equal/greater, like strcmp."

Heapsort 알고리즘

왜 heapsort인가: 커널에서 heapsort가 선택된 이유는 단순합니다. (1) 최악 O(n log n) — 어떤 입력이든 보장, (2) in-place O(1) 추가 메모리, (3) 반복(iterative) 구현으로 스택 사용 최소화. introsort(quicksort + heapsort 하이브리드)도 후보였지만, 코드 복잡도 대비 이득이 적어 순수 heapsort가 채택되었습니다.

lib/sort.c의 heapsort는 두 단계로 동작합니다:

  1. Build-heap (heapify) — 배열을 max-heap으로 구성합니다. 하위 절반의 노드부터 역순으로 sift-down을 수행합니다.
  2. Extract-max — 루트(최댓값)와 마지막 요소를 교환한 뒤, 힙 크기를 줄이고 루트에서 sift-down합니다. n-1회 반복하면 정렬 완료입니다.
Heapsort 알고리즘 단계 build-heap과 extract-max 반복 과정을 시각화 lib/sort.c Heapsort 동작 과정 Phase 1: Build Max-Heap 42 35 28 12 7 parent[i] ≥ child[2i+1], child[2i+2] Phase 2: Extract-Max × (n-1) 42 35 28 12 7 swap(root, last) sift-down(root) → 힙 복원 7 35 28 12 42 Sift-down (do_swap) 핵심 루프 1. child = 2*parent + 1 (왼쪽 자식) 2. if (child+1 < n && cmp(child+1, child) > 0) child++ (오른쪽이 더 크면 선택) 3. if (cmp(parent, child) ≥ 0) break (힙 속성 만족) 4. swap(parent, child); parent = child (한 단계 내려감) 비교 횟수: 최대 2&lfloor;log⊂2; n&rfloor; per sift-down → 총 ≤ 2n log⊂2; n

커널 구현의 핵심 최적화는 sift-down에 있습니다. 교과서적 heapsort는 부모-자식을 매번 교환하지만, 커널은 "빈 자리"를 아래로 밀어내며 최종 위치에서 한 번만 기록합니다.

/* lib/sort.c — sift-down 핵심 (간략화) */
static void sift_down(void *base, size_t i, size_t n,
                       size_t size, cmp_func_t cmp, swap_func_t swap)
{
    size_t child;
    while (i * 2 + 1 < n) {
        child = i * 2 + 1;
        /* 오른쪽 자식이 더 크면 선택 */
        if (child + 1 < n &&
            cmp(base + (child + 1) * size,
                base + child * size) > 0)
            child++;
        /* 힙 속성 만족 시 종료 */
        if (cmp(base + i * size,
                base + child * size) >= 0)
            break;
        swap(base + i * size, base + child * size, size);
        i = child;
    }
}

전체 heapsort 흐름

/* lib/sort.c — sort_r() 전체 흐름 (간략화) */
void sort_r(void *base, size_t num, size_t size,
            cmp_r_func_t cmp_func, swap_func_t swap_func,
            const void *priv)
{
    /* swap 함수 자동 선택 */
    if (!swap_func) {
        if (is_aligned(base, size, sizeof(long)))
            swap_func = swap_words_32;  /* word swap */
        else
            swap_func = swap_bytes;     /* byte swap */
    }

    /* Phase 1: build max-heap (heapify)
     * n/2-1부터 0까지 역순으로 sift-down */
    for (int i = num / 2 - 1; i >= 0; i--)
        sift_down(base, i, num, size, cmp_func, priv, swap_func);

    /* Phase 2: extract-max 반복
     * 루트(최댓값)를 끝으로 이동, 힙 크기 감소, sift-down */
    for (int i = num - 1; i > 0; i--) {
        swap_func(base, base + i * size, size);
        sift_down(base, 0, i, size, cmp_func, priv, swap_func);
    }
}

힙 인덱스 계산

배열 기반 힙에서 인덱스 관계:

관계인덱스 (0-based)커널 구현 (포인터)
부모(i - 1) / 2base + ((i-1)/2) * size
왼쪽 자식2*i + 1base + (2*i+1) * size
오른쪽 자식2*i + 2base + (2*i+2) * size
마지막 비-리프n/2 - 1build-heap 시작 인덱스
2019년 최적화 (Rasmus Villemoes): 기존 구현은 parent(i) = (i-1)/2 계산에 분기가 있었습니다. 최적화 버전은 "children-of-parent" 접근으로 부모 노드를 기준으로 양쪽 자식을 한 번에 비교하여 분기를 줄이고, 현대 CPU의 분기 예측(Branch Prediction)기에 친화적입니다.

sort() API

sort()는 커널에서 가장 기본적인 배열 정렬 함수입니다.

/* include/linux/sort.h */
void sort(void *base, size_t num, size_t size,
          cmp_func_t cmp, swap_func_t swap);
파라미터타입설명
basevoid *정렬할 배열의 시작 주소
numsize_t배열 요소 개수
sizesize_t각 요소의 바이트 크기 (sizeof(element))
cmpcmp_func_t비교 함수: int (*)(const void *, const void *)
swapswap_func_t교환 함수 (NULL이면 자동 선택)
swap = NULL 권장: swap에 NULL을 전달하면 커널이 요소 크기와 정렬(alignment)에 따라 최적의 swap 전략을 자동 선택합니다. 커스텀 swap은 요소가 내부 포인터(자기참조 구조체 등)를 가질 때만 필요합니다.

sort() 동작 보장

num = 0 또는 1: sort()num ≤ 1을 전달하면 즉시 반환합니다. 빈 배열이나 단일 요소 배열은 이미 정렬된 것으로 취급되므로, 호출 전 크기 검사는 불필요합니다.

사용 예시:

static int cmp_u32(const void *a, const void *b)
{
    u32 va = *(const u32 *)a;
    u32 vb = *(const u32 *)b;
    if (va < vb) return -1;
    if (va > vb) return 1;
    return 0;
}

u32 arr[1024];
/* ... 배열 채우기 ... */
sort(arr, 1024, sizeof(u32), cmp_u32, NULL);

에러 핸들링

sort()는 반환값이 void이며 실패하지 않습니다. 이는 동적 메모리 할당을 수행하지 않기 때문입니다. 비교 함수가 잘못되면 정렬 결과가 부정확하지만, 함수 자체는 항상 반환합니다.

size = 0 전달 금지: sort()size = 0을 전달하면 0으로 나누기(division by zero)가 발생할 수 있습니다. 요소 크기는 반드시 양수여야 합니다. 이는 API 문서에 명시되지 않지만, 내부 인덱스 계산(i * size)이 0을 곱하면 모든 요소가 같은 주소를 가리키게 됩니다.
/* 안전한 사용 패턴 */
if (n > 1) {
    sort(array, n, elem_size, my_cmp, NULL);
    /* 디버그 빌드에서 결과 검증 */
    #ifdef CONFIG_DEBUG_SORT
    for (size_t i = 0; i < n - 1; i++)
        WARN_ON(my_cmp(array + i * elem_size,
                        array + (i+1) * elem_size) > 0);
    #endif
}

sort_r() API

sort_r()은 비교 함수에 추가 컨텍스트(priv)를 전달할 수 있는 확장 버전입니다.

/* include/linux/sort.h */
void sort_r(void *base, size_t num, size_t size,
            cmp_r_func_t cmp, swap_func_t swap,
            const void *priv);

/* cmp_r_func_t 시그니처 */
typedef int (*cmp_r_func_t)(const void *a, const void *b,
                              const void *priv);

priv 포인터는 비교 함수가 외부 상태(예: 정렬 방향, 참조 테이블)를 참조해야 할 때 사용합니다. POSIX qsort_r(3)의 커널 대응입니다.

struct sort_ctx {
    bool descending;
};

static int cmp_with_ctx(const void *a, const void *b,
                          const void *priv)
{
    const struct sort_ctx *ctx = priv;
    int result = cmp_u32(a, b);
    return ctx->descending ? -result : result;
}

struct sort_ctx ctx = { .descending = true };
sort_r(arr, n, sizeof(u32), cmp_with_ctx, NULL, &ctx);

sort_r() 실전 사용 사례

/* perf 이벤트 정렬 — 그룹 리더 기준 정렬 */
struct perf_sort_ctx {
    struct perf_event *group_leader;
    bool sort_by_cpu;
};

static int perf_cmp_event(const void *a, const void *b,
                            const void *priv)
{
    const struct perf_sort_ctx *ctx = priv;
    const struct perf_event *ea = *(void **)a;
    const struct perf_event *eb = *(void **)b;

    if (ctx->sort_by_cpu) {
        if (ea->cpu != eb->cpu)
            return ea->cpu - eb->cpu;
    }
    /* 그룹 리더와의 거리 기준 2차 정렬 */
    return ea->group_index - eb->group_index;
}
내부 구현: sort()sort_r()의 래퍼입니다. sort()를 호출하면 내부적으로 sort_r(base, num, size, _cmp_wrapper, swap, cmp)로 변환되어, 기존 cmp_func_tcmp_r_func_t로 감싸줍니다.

sort()와 sort_r()의 관계

/* include/linux/sort.h — sort()는 sort_r()의 래퍼 */
static inline void sort(void *base, size_t num, size_t size,
                         cmp_func_t cmp_func,
                         swap_func_t swap_func)
{
    return sort_r(base, num, size,
                   _cmp_wrapper, swap_func, cmp_func);
}

/* 내부 래퍼: cmp_func_t → cmp_r_func_t 변환 */
static int _cmp_wrapper(const void *a, const void *b,
                         const void *priv)
{
    cmp_func_t cmp = priv;
    return cmp(a, b);
}
sort_r()에서 cmp 서명 주의: sort_r()의 비교 함수는 3번째 인자로 priv를 받습니다. sort()의 비교 함수(2인자)를 sort_r()에 직접 전달하면 컴파일은 되지만 잘못된 결과가 나올 수 있습니다.

비교자 패턴

올바른 비교 함수는 커널 정렬의 핵심입니다. 잘못된 비교자는 무한 루프, 데이터 손상, 미정의 동작을 초래합니다.

cmp_func_t 시그니처

typedef int (*cmp_func_t)(const void *, const void *);

반환값 규약:

타입 안전 래퍼 패턴

/* 구조체 배열 정렬 예시 */
struct extent_info {
    u64 start_block;
    u32 length;
    u16 flags;
};

static int cmp_extent(const void *a, const void *b)
{
    const struct extent_info *ea = a;
    const struct extent_info *eb = b;
    /* u64 뺄셈은 오버플로우 위험 → 비교 연산 사용 */
    if (ea->start_block < eb->start_block) return -1;
    if (ea->start_block > eb->start_block) return  1;
    return 0;
}
뺄셈 함정: return *(int *)a - *(int *)b; 패턴은 정수 오버플로(Integer Overflow)우를 유발합니다. 예: INT_MAX - (-1) = INT_MIN (래핑). 부호 없는 타입이나 큰 범위 값에는 반드시 비교 연산(<, >)을 사용하세요.

다중 키 정렬

static int cmp_multi_key(const void *a, const void *b)
{
    const struct entry *ea = a, *eb = b;
    /* 1차 키: priority (내림차순) */
    if (ea->priority != eb->priority)
        return eb->priority - ea->priority;
    /* 2차 키: timestamp (오름차순) */
    if (ea->timestamp < eb->timestamp) return -1;
    if (ea->timestamp > eb->timestamp) return  1;
    return 0;
}

커널 내 실제 비교자 예시

커널 소스에서 자주 등장하는 비교자 패턴들:

/* 1. 단순 정수 비교 — drivers/acpi/acpica/nsaccess.c */
static int cmp_resource(const void *a, const void *b)
{
    const struct resource *ra = a, *rb = b;
    if (ra->start < rb->start) return -1;
    if (ra->start > rb->start) return  1;
    return 0;
}

/* 2. 문자열 비교 — fs/proc/base.c */
static int proc_cmp_name(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    const struct proc_entry *pa = list_entry(a, struct proc_entry, list);
    const struct proc_entry *pb = list_entry(b, struct proc_entry, list);
    return strcmp(pa->name, pb->name);
}

/* 3. 복합 키 비교 — drivers/gpu/drm/drm_modes.c */
static int drm_mode_compare(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    struct drm_display_mode *ma = list_entry(a, struct drm_display_mode, head);
    struct drm_display_mode *mb = list_entry(b, struct drm_display_mode, head);
    /* 1차: 해상도 (큰 것 우선) */
    int diff = mb->hdisplay * mb->vdisplay
             - ma->hdisplay * ma->vdisplay;
    if (diff) return diff;
    /* 2차: refresh rate (높은 것 우선) */
    diff = drm_mode_vrefresh(mb) - drm_mode_vrefresh(ma);
    if (diff) return diff;
    /* 3차: preferred 모드 우선 */
    diff = (mb->type & DRM_MODE_TYPE_PREFERRED)
         - (ma->type & DRM_MODE_TYPE_PREFERRED);
    return diff;
}

비교자 작성 체크리스트

규칙올바른 예잘못된 예
반사성: cmp(a, a) == 0if (a == b) return 0;부동소수점 비교 시 epsilon 무시
반대칭: sign(cmp(a,b)) == -sign(cmp(b,a))if/else 체인비대칭 필드 접근
추이성: a<b && b<c → a<c단일 키 비교순환 의존 다중 키
오버플로우 방지if (a < b) return -1;return a - b;
const 안전성const void * 파라미터비교 중 대상 수정

Swap 최적화

lib/sort.c의 성능은 swap 전략에 크게 의존합니다. 커널은 요소 크기와 정렬(alignment)에 따라 세 가지 swap 전략을 자동 선택합니다.

Swap 전략 선택 word swap, byte swap, custom swap 분기 흐름 swap_func_t 자동 선택 흐름 swap == NULL ? Yes size % sizeof(long) == 0 ? Yes Word Swap long 단위 교환 (4/8B) No Byte Swap 1바이트씩 교환 (느림) No (커스텀) Custom 성능: Word Swap >> Byte Swap (4~8x) | Custom은 호출자 책임

word swap 구현:

/* lib/sort.c — word-at-a-time swap */
static void swap_words(void *a, void *b, size_t size)
{
    do {
        unsigned long t = *(unsigned long *)a;
        *(unsigned long *)a = *(unsigned long *)b;
        *(unsigned long *)b = t;
        a += sizeof(unsigned long);
        b += sizeof(unsigned long);
        size -= sizeof(unsigned long);
    } while (size);
}

/* byte 단위 폴백 */
static void swap_bytes(void *a, void *b, size_t size)
{
    do {
        char t = *(char *)a;
        *(char *)a++ = *(char *)b;
        *(char *)b++ = t;
    } while (--size);
}
구조체 정렬 팁: 구조체를 sort()로 정렬할 때, sizeof(struct)sizeof(long)의 배수가 되도록 패딩(Padding)을 의식하면 word swap이 적용되어 성능이 향상됩니다.

커스텀 swap이 필요한 경우

요소가 자기참조 포인터를 가질 때는 커스텀 swap이 필수입니다. 단순 메모리 복사로는 내부 포인터가 깨집니다.

/* 자기참조 구조체의 커스텀 swap 예시 */
struct self_ref {
    int key;
    struct self_ref *self;  /* 자기 자신을 가리킴 */
    char data[64];
};

static void swap_self_ref(void *a, void *b, size_t size)
{
    struct self_ref tmp;
    memcpy(&tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, &tmp, size);
    /* 자기참조 포인터 복원 */
    ((struct self_ref *)a)->self = a;
    ((struct self_ref *)b)->self = b;
}

swap 성능 비교

swap 전략요소 크기 16B요소 크기 64B요소 크기 256B
Word swap (8B 단위)2 iterations8 iterations32 iterations
Byte swap (1B 단위)16 iterations64 iterations256 iterations
속도 비율~8x~8x~8x
정렬(alignment) 감지: 커널은 base 주소와 size 값 모두를 검사합니다. 둘 다 sizeof(long)의 배수여야 word swap이 적용됩니다. 구조체에 char 배열이 포함되어 sizeof가 홀수가 되면 byte swap으로 폴백됩니다.

List Sort 알고리즘

lib/list_sort.c는 이중 연결 리스트를 위한 bottom-up merge sort를 구현합니다. 2009년 Don Mullis가 초기 버전을 작성하고, 2019년 George Spelvin(Andrey Abramov)이 현재의 최적화된 버전으로 재작성했습니다.

Bottom-up Merge Sort 과정 리스트를 점진적으로 병합하는 과정을 단계별로 시각화 lib/list_sort.c Bottom-up Merge Sort 원본: 5 3 8 1 7 2 6 4 Step 1: 3 → 5 1 → 8 2 → 7 4 → 6 Step 2: 1 → 3 → 5 → 8 2 → 4 → 6 → 7 최종: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 Bottom-up 핵심 특징 1. 입력 리스트를 단일 연결 리스트로 변환 (prev 포인터 임시 해제) 2. pending 스택에 정렬된 서브리스트를 누적 3. 2:1 비율 규칙으로 인접 서브리스트 병합 시점 결정 4. 모든 요소 소진 후 남은 pending을 최종 병합 안정 정렬: cmp(a, b) == 0일 때 a가 b 앞에 유지됨 공간: O(1) 추가 — 리스트 노드의 next/prev 포인터만 재연결

알고리즘의 핵심 루프 (간략화):

/* lib/list_sort.c — bottom-up merge sort 핵심 */
void list_sort(void *priv, struct list_head *head,
               list_cmp_func_t cmp)
{
    struct list_head *list = head->next, *pending = NULL;
    int count = 0;

    /* 리스트를 head에서 분리 → 단일 연결 리스트로 */
    head->prev->next = NULL;

    do {
        size_t bits;
        struct list_head **tail = &pending;

        /* 노드 1개를 pending에 추가 */
        list->prev = pending;
        pending = list;
        list = list->next;
        pending->next = NULL;
        count++;

        /* 2:1 비율 규칙에 따라 병합 */
        for (bits = count; bits & 1; bits >>= 1)
            tail = &(*tail)->prev;
        if (bits) {
            struct list_head *a = *tail, *b = a->prev;
            a = merge(priv, cmp, b, a);
            a->prev = b->prev;
            *tail = a;
        }
    } while (list);

    /* 남은 pending 리스트 최종 병합 */
    list = pending;
    pending = pending->prev;
    for (;;) {
        struct list_head *next = pending->prev;
        list = merge(priv, cmp, pending, list);
        pending = next;
        if (!next) break;
    }
    /* 단일→이중 연결 리스트 복원 */
    merge_final(priv, cmp, head, pending, list);
}

merge() 함수 상세

두 정렬된 단일 연결 리스트를 하나로 병합하는 핵심 함수입니다.

/* lib/list_sort.c — merge() 전체 (간략화) */
static struct list_head *merge(void *priv,
    list_cmp_func_t cmp,
    struct list_head *a, struct list_head *b)
{
    struct list_head *head, **tail = &head;

    for (;;) {
        /* a ≤ b이면 a 선택 (안정성 보장: ≤ not <) */
        if (cmp(priv, a, b) <= 0) {
            *tail = a;
            tail = &a->next;
            a = a->next;
            if (!a) {
                *tail = b;
                break;
            }
        } else {
            *tail = b;
            tail = &b->next;
            b = b->next;
            if (!b) {
                *tail = a;
                break;
            }
        }
    }
    return head;
}

merge_final() — 이중 연결 리스트 복원

정렬 과정에서 prev 포인터를 임시로 용도 변경(pending 스택 포인터)했으므로, 최종 병합 후 이중 연결 리스트를 복원해야 합니다.

/* lib/list_sort.c — merge_final() (간략화) */
static void merge_final(void *priv, list_cmp_func_t cmp,
    struct list_head *head,
    struct list_head *a, struct list_head *b)
{
    struct list_head *tail = head;

    for (;;) {
        if (cmp(priv, a, b) <= 0) {
            tail->next = a;
            a->prev = tail;
            tail = a;
            a = a->next;
            if (!a) break;
        } else {
            tail->next = b;
            b->prev = tail;
            tail = b;
            b = b->next;
            if (!b) {
                b = a; break;
            }
        }
    }
    /* 나머지 요소 연결 + 원형 리스트 복원 */
    while (b) {
        tail->next = b;
        b->prev = tail;
        tail = b;
        b = b->next;
    }
    tail->next = head;
    head->prev = tail;
}
prev 포인터 오용 주의: list_sort() 실행 중에는 prev 포인터가 pending 스택 링크로 사용됩니다. 이 구간에서 list_for_each_entry()prev를 참조하는 매크로를 호출하면 커널 OOPS가 발생합니다. 정렬 완료 후(merge_final 이후)에만 리스트를 순회하세요.

list_sort() API

/* include/linux/list_sort.h */
typedef int (*list_cmp_func_t)(void *priv,
    const struct list_head *a,
    const struct list_head *b);

void list_sort(void *priv, struct list_head *head,
               list_cmp_func_t cmp);
파라미터타입설명
privvoid *비교 함수에 전달할 컨텍스트 (NULL 가능)
headstruct list_head *정렬할 리스트의 헤드 노드
cmplist_cmp_func_t비교 함수: 음수(a<b), 0(a==b), 양수(a>b)
container_of 패턴: 비교 함수 내에서 list_head 포인터를 감싸는 구조체에 접근할 때는 container_of() 또는 list_entry()를 사용합니다.
struct dentry_info {
    char name[256];
    ino_t ino;
    struct list_head list;
};

static int cmp_dentry(void *priv,
                       const struct list_head *a,
                       const struct list_head *b)
{
    const struct dentry_info *da = list_entry(a, struct dentry_info, list);
    const struct dentry_info *db = list_entry(b, struct dentry_info, list);
    return strcmp(da->name, db->name);
}

/* 사용 */
LIST_HEAD(dentry_list);
/* ... 리스트에 요소 추가 ... */
list_sort(NULL, &dentry_list, cmp_dentry);

안정성 분석

안정 정렬(stable sort)은 동일 키 값을 가진 요소들의 상대적 순서를 보존합니다. 이 속성이 커널에서 중요한 경우가 있습니다.

정렬 함수안정성이유
sort()불안정 (unstable)heapsort는 원소를 힙 구조로 재배열하며, 동일 키 원소의 순서가 보존되지 않음
list_sort()안정 (stable)merge sort에서 cmp(a, b) ≤ 0일 때 a를 먼저 선택하여 원래 순서 보존
안정성이 중요한 경우:
  • readdir — 파일명 정렬 시, 같은 이름(대소문자 차이)의 원래 순서가 유지되어야 사용자 기대에 부합
  • 다중 키 정렬 — 2차 키로 먼저 정렬 후 1차 키로 재정렬할 때, 안정성이 보장되면 2차 순서가 유지
  • TC 규칙 — 네트워크 필터 규칙에서 동일 우선순위(Priority) 규칙의 삽입 순서가 정책적 의미를 가질 수 있음

merge 함수에서 안정성을 보장하는 핵심 코드:

/* lib/list_sort.c — merge() 내부 */
if (cmp(priv, a, b) <= 0) {
    /* a ≤ b → a를 결과에 먼저 연결 (안정성 보장) */
    *tail = a;
    tail = &a->next;
    a = a->next;
} else {
    *tail = b;
    tail = &b->next;
    b = b->next;
}

안정성 실험

/* 안정성 차이 실험 */
struct pair {
    int key;
    int original_index;
};

struct pair data[] = {
    {3, 0}, {1, 1}, {3, 2}, {2, 3}, {3, 4}
};

/* sort() 결과 (불안정):
 * {1,1}, {2,3}, {3,4}, {3,0}, {3,2}  ← key=3인 원소 순서 변경됨
 *
 * list_sort() 결과 (안정):
 * {1,1}, {2,3}, {3,0}, {3,2}, {3,4}  ← key=3인 원소 원래 순서 유지
 */
불안정 정렬 → 안정 정렬 변환: sort()를 안정하게 만들어야 하면, 원래 인덱스를 비교 키에 2차 키로 포함시킵니다: if (cmp(a,b) == 0) return a.index - b.index; 이 방법은 추가 필드가 필요하므로, 가능하면 처음부터 list_sort()를 사용하세요.

병합 패턴: 2:1 비율

list_sort()의 가장 독특한 설계는 2:1 비율 병합 규칙입니다. 이는 Timsort의 "nearly-optimal" 병합 정책에서 영감을 받았지만, 커널에 맞게 단순화되었습니다.

2:1 비율 병합 패턴 pending 리스트에서 인접 서브리스트 크기 비율에 따라 병합 시점을 결정하는 과정 2:1 비율 병합 규칙 (Pending Stack) count (이진) Pending 상태 동작 1 = 0b1 1 추가만 2 = 0b10 1 1 추가만 3 = 0b11 2 1 병합! (bits & 1 = 1) 4 = 0b100 2 1 1 추가만 5 = 0b101 2 2 1 병합! (bits & 1 = 1) 규칙: count의 하위 비트가 1이면 병합 for (bits = count; bits & 1; bits >>= 1) → 연속된 1-비트 수만큼 병합 반복 효과: pending 서브리스트 크기가 약 2:1 비율을 유지 → 병합 시 비교 횟수 최소화

2:1 비율의 수학적 근거는 비교 횟수 최소화입니다. 크기가 비슷한 두 서브리스트를 병합할 때 비교 횟수가 최적에 가깝습니다. count의 이진 표현에서 연속된 1-비트를 추적하는 것은 이 비율을 O(1) 시간에 유지하는 간결한 방법입니다.

비트 카운터 동작 상세

/* 비트 카운터로 병합 시점 결정하는 핵심 코드 */
for (bits = count; bits & 1; bits >>= 1)
    tail = &(*tail)->prev;

/* count별 동작:
 * count=1 (0b1):   bits&1=1 → tail 1번 이동 → 병합 불필요 (if(bits)=0)
 * count=2 (0b10):  bits&1=0 → 이동 없음   → 병합 불필요
 * count=3 (0b11):  bits&1=1 → tail 1번 이동, bits=1, bits&1=1 → 1번 더 → 병합!
 * count=4 (0b100): bits&1=0 → 이동 없음   → 병합 불필요
 * count=5 (0b101): bits&1=1 → tail 1번 이동 → bits=10, 0&1=0 → 병합!
 * count=6 (0b110): bits&1=0 → 이동 없음   → 병합 불필요
 * count=7 (0b111): 3번 이동 → 3개 서브리스트 연쇄 병합!
 */

이 패턴은 이진수 덧셈의 올림(carry)과 동일합니다. count에 1을 더할 때 올림이 발생하는 자릿수만큼 병합이 연쇄됩니다. 결과적으로 pending 스택의 서브리스트 크기가 2의 거듭제곱에 가까운 분포를 유지합니다.

pending 스택의 최대 깊이

pending 스택의 최대 깊이는 ⌈log2(n)⌉입니다. 이는 count의 이진 표현 비트 수와 동일합니다.

n (요소 수)pending 최대 깊이참고
1007일반적 디렉터리
1,00010대형 디렉터리
10,00014DRM 모드 리스트
100,00017극단적 경우
1,000,00020이론적 최대

pending 스택은 리스트 노드의 prev 포인터를 재활용(Recycling)하므로, 추가 메모리 할당이 전혀 없습니다. 이것이 list_sort()의 O(1) 공간 복잡도를 보장하는 핵심입니다.

Timsort와의 차이: Python/Java의 Timsort는 자연 발생 정렬 구간(run)을 탐지하여 활용하지만, 커널의 list_sort()는 run 탐지를 하지 않습니다. 이유는 (1) 커널 데이터가 대체로 무작위이고, (2) run 탐지 오버헤드(Overhead)가 커널 스케일에서 이득보다 클 수 있기 때문입니다.

복잡도 분석

항목sort() — Heapsortlist_sort() — Merge Sort
최선 시간O(n log n)O(n log n)
평균 시간O(n log n)O(n log n)
최악 시간O(n log n)O(n log n)
비교 횟수≤ 2n log2 n≤ n log2 n + O(n)
swap/이동 횟수O(n log n) swapO(n log n) 포인터 재연결
추가 공간O(1)O(1) (포인터 조작만)
캐시 친화성좋음 (배열 연속 접근)나쁨 (포인터 추적)
안정성불안정안정
비교 비용 주의: 비교 횟수는 같지만, sort()의 비교는 배열 인덱싱(O(1))이고 list_sort()의 비교는 container_of() + 포인터 추적을 수반합니다. 비교 함수가 비싸면(문자열 비교 등) 이 차이는 무시할 수 있지만, 정수 비교처럼 싼 경우 sort()가 실측에서 2~3배 빠를 수 있습니다.

heapsort의 비교 횟수 유도:

merge sort의 비교 횟수:

실측 비교 횟수

lib/test_sort.clib/test_list_sort.c의 카운터를 활성화하면 실측 비교 횟수를 확인할 수 있습니다:

n이론 하한 (n log2n)sort() 실측비율list_sort() 실측비율
100665~1,1001.65x~5500.83x
1,0009,966~17,0001.71x~8,7000.87x
10,000132,877~230,0001.73x~120,0000.90x
100,0001,660,964~2,900,0001.75x~1,500,0000.90x
list_sort()가 이론 하한 이하? 표의 list_sort() 비율이 1.0 이하인 것은 측정 오차가 아닙니다. 이론 하한은 최악 입력에 대한 것이고, 랜덤 입력에서는 이보다 적은 비교로 정렬이 완료될 수 있습니다. list_sort()의 2:1 비율 병합은 실제로 정보 이론적 최적에 매우 근접합니다.

이론적 비교 횟수 최적성

비교 기반 정렬의 이론적 하한은 ⌈log2(n!)⌉ ≈ n log2 n - 1.443n입니다. 각 알고리즘의 비교 횟수를 비교합니다:

알고리즘비교 횟수 (최악)이론 하한 대비커널 사용
이론 하한n log2 n - 1.44n1.00x-
Merge sort (list_sort)n log2 n + O(n)~1.00xO
Heapsort (sort)2n log2 n~2.00xO
Quicksort (배제)n²/2 (최악)X
Insertion sortn²/2 (최악)X
비교 횟수 vs 실행 시간: merge sort가 비교 횟수에서 최적에 가깝지만, 실제 시간은 캐시 효과, 분기 예측, 메모리 접근 패턴에 크게 좌우됩니다. heapsort는 비교 횟수가 2배이지만, 배열의 연속 접근으로 캐시 히트율이 높아 실측에서 더 빠를 수 있습니다.

readdir 정렬

파일시스템의 readdirlist_sort()의 대표적 호출자입니다. 디렉터리 엔트리를 정렬하여 사용자에게 일관된 순서를 제공합니다.

/* fs/libfs.c — dcache_readdir에서 사용 */
static int cmp_dentry_name(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    const struct dentry *da = list_entry(a, struct dentry, d_child);
    const struct dentry *db = list_entry(b, struct dentry, d_child);
    return strcmp(da->d_name.name, db->d_name.name);
}
안정성이 필수: readdir에서 list_sort()를 사용하는 이유 중 하나는 안정성입니다. 같은 이름의 엔트리가 존재하지 않더라도, 정렬 기준이 부분적(예: 이름 길이)일 때 같은 키 값의 엔트리 순서가 호출마다 달라지면 사용자 프로그램이 예측 불가능한 동작을 할 수 있습니다.

tmpfs, debugfs, sysfs 등 메모리 기반 파일시스템에서 dcache_readdir()은 디렉터리 엔트리를 d_child 리스트에서 직접 읽으므로, 정렬된 출력을 위해 list_sort()를 활용합니다.

readdir 성능 고려사항

디렉터리에 파일이 많으면 정렬 비용이 체감됩니다.

파일 수정렬 시간 (tmpfs, 대략적)
100~10 μs
1,000~150 μs
10,000~2 ms
100,000~30 ms — ls 명령이 느려집니다

대형 디렉터리는 htree (ext4) 또는 B-tree (btrfs)로 디스크 레벨에서 이미 정렬되어 있어 별도 정렬이 필요하지 않습니다.

대형 디렉터리 경고: tmpfs에 10만 개 이상의 파일이 있는 디렉터리를 ls하면, list_sort()의 정렬 시간이 수십 밀리초에 달합니다. 이는 사용자 체감 지연(Latency)을 유발합니다. 대형 디렉터리가 예상되면 tmpfs 대신 ext4/btrfs를 사용하세요.

readdir 정렬이 필요한 파일시스템

파일시스템readdir 정렬정렬 기준사용 API
tmpfs/shmem필요파일명 (strcmp)list_sort()
debugfs필요파일명 (strcmp)list_sort()
sysfs필요파일명 (strcmp)list_sort()
procfs필요PID (숫자) + 파일명list_sort()
ext4디스크 순서htree 인덱스정렬 불필요 (B-tree)
btrfs디스크 순서inode 번호정렬 불필요 (B-tree)
NFS서버 의존서버 순서 그대로클라이언트 미정렬
POSIX 규격과 readdir 순서: POSIX는 readdir(3)의 엔트리 순서를 규정하지 않습니다. 그러나 많은 사용자 프로그램이 알파벳 순 또는 일관된 순서를 기대하므로, 메모리 기반 파일시스템(tmpfs, debugfs 등)은 list_sort()로 정렬하여 사용성을 개선합니다.

Extent 정렬

ext4와 btrfs는 디스크 블록 할당 최적화를 위해 extent를 정렬합니다.

/* fs/ext4/extents.c — extent 정렬 예시 (개념적) */
static int ext4_cmp_extent(const void *a, const void *b)
{
    const struct ext4_extent *ea = a;
    const struct ext4_extent *eb = b;
    /* 논리 블록 번호 기준 오름차순 */
    if (le32_to_cpu(ea->ee_block) < le32_to_cpu(eb->ee_block))
        return -1;
    if (le32_to_cpu(ea->ee_block) > le32_to_cpu(eb->ee_block))
        return  1;
    return 0;
}
파일시스템정렬 대상사용 API정렬 기준
ext4extent 배열sort()논리 블록 번호
btrfsextent backrefsort()바이트 오프셋(Offset)
XFSalloc candidatesort()AG + 블록 번호
F2FSdirty segmentsort()유효 블록 수
왜 배열 정렬인가: extent는 디스크 메타데이터에서 배열로 관리됩니다. 연결 리스트가 아니므로 sort()가 자연스러운 선택이며, 캐시 친화적 접근으로 성능도 우수합니다.
ext4 extent status tree: ext4는 메모리 내 extent 상태를 관리하기 위해 struct ext4_es_tree를 사용합니다. 이 트리는 red-black tree로 항상 정렬 상태를 유지하지만, 디스크에서 일괄 로드한 extent 배열은 sort()로 정렬해야 합니다.

extent 정렬 최적화 효과

extent를 논리 블록 순으로 정렬하면 다음과 같은 I/O 최적화가 가능합니다:

/* fs/btrfs/volumes.c — 디바이스 정렬 예시 */
static int btrfs_cmp_device_info(const void *a, const void *b)
{
    const struct btrfs_device_info *di_a = a;
    const struct btrfs_device_info *di_b = b;
    /* 사용 가능 공간이 큰 디바이스 우선 (내림차순) */
    if (di_a->total_avail > di_b->total_avail)
        return -1;
    if (di_a->total_avail < di_b->total_avail)
        return 1;
    return 0;
}

/* 디바이스 할당 전 정렬 — 가장 여유 있는 디바이스부터 사용 */
sort(devices_info, ndevs, sizeof(struct btrfs_device_info),
     btrfs_cmp_device_info, NULL);

Slab Freelist 정렬

SLUB 할당자는 보안 강화를 위해 freelist를 랜덤화하는데, 이 과정에서 sort()가 관여합니다.

SLUB Freelist 정렬 slab 페이지 내 freelist 정렬과 랜덤화 과정 SLUB Freelist: 정렬과 랜덤화 초기 (순차): obj0 obj1 obj2 obj3 obj4 shuffle 랜덤화 후: obj3 obj0 obj4 obj1 obj2 CONFIG_SLAB_FREELIST_RANDOM 보안 효과 1. 할당 순서 예측 불가 → heap overflow 공격 시 인접 객체 위치 예측 실패 2. Use-After-Free 시 재할당 객체의 위치가 비결정적 3. sort()로 셔플 인덱스 배열 초기 정렬 후 Fisher-Yates 셔플 적용
/* mm/slub.c — freelist 셔플 (개념적) */
static void shuffle_freelist(struct kmem_cache *s,
                              struct slab *slab)
{
    unsigned int *order;
    int i;

    order = kmalloc_array(slab->objects, sizeof(int), GFP_KERNEL);
    for (i = 0; i < slab->objects; i++)
        order[i] = i;

    /* Fisher-Yates 셔플 */
    for (i = slab->objects - 1; i > 0; i--) {
        unsigned int j = get_random_u32_below(i + 1);
        swap(order[i], order[j]);
    }

    /* 셔플된 순서로 freelist 재구성 */
    build_freelist(s, slab, order);
    kfree(order);
}

freelist 보안 강화 역사

옵션도입 시기효과
CONFIG_SLAB_FREELIST_RANDOMv4.7 (2016)freelist 할당 순서 랜덤화
CONFIG_SLAB_FREELIST_HARDENEDv4.14 (2017)freelist 포인터 XOR 난독화
CONFIG_RANDOM_KMALLOC_CACHESv6.6 (2023)여러 슬랩 풀 중 랜덤 선택

CONFIG_SLAB_FREELIST_RANDOM이 활성화되면, 새 slab 페이지(Page) 초기화 시 셔플된 인덱스 배열을 생성합니다. 이 셔플은 Fisher-Yates 알고리즘을 사용하며, 초기 인덱스 배열의 생성에 정렬이 관여할 수 있습니다.

SLUB freelist 포인터 하드닝

/* mm/slub.c — freelist 포인터 난독화
 * CONFIG_SLAB_FREELIST_HARDENED 활성화 시 */

/* 저장: XOR 인코딩 */
static inline void *freelist_ptr_encode(
    const struct kmem_cache *s,
    void *ptr, unsigned long ptr_addr)
{
    return (void *)((unsigned long)ptr ^
                    s->random ^ ptr_addr);
}

/* 읽기: XOR 디코딩 */
static inline void *freelist_ptr_decode(
    const struct kmem_cache *s,
    void *ptr, unsigned long ptr_addr)
{
    return (void *)((unsigned long)ptr ^
                    s->random ^ ptr_addr);
}

/* 해커가 freelist 포인터를 조작하면
 * XOR 검증에서 잘못된 주소가 나와 감지됨 */
보안과 성능 트레이드오프: freelist 랜덤화는 캐시 지역성을 의도적으로 파괴합니다. 순차 할당 시 인접 캐시라인을 접근하던 것이 랜덤 접근으로 바뀌어, slab 할당 성능이 ~5% 저하됩니다. 보안 강화를 이유로 이를 활성화하는 배포판이 많지만, 실제 기본값은 배포판 보안 정책과 커널 설정을 확인해야 합니다.

네트워크 정렬

네트워크 서브시스템에서도 정렬은 다양한 곳에서 활용됩니다.

네트워크 서브시스템 정렬 활용 TC qdisc, 라우팅, NAPI 등에서의 정렬 사용 사례 네트워크 서브시스템의 정렬 활용 TC Qdisc - prio qdisc: 우선순위 정렬 - fq_codel: flow 해시 정렬 - HTB: 클래스 rate 순 정렬 list_sort() 사용 라우팅 테이블 - FIB 엔트리 prefix 순 정렬 - 넥스트홉 가중치 정렬 - 정책 라우팅 우선순위 sort() 사용 Netfilter - hook 우선순위 정렬 - conntrack 버킷 정리 - nft set 요소 정렬 sort() / list_sort() 패킷 스케줄링과 정렬 패킷 우선순위 결정: skb->priority, tc_index, mark 등 다중 키 기반 정렬 GRO 병합: timestamp 순 정렬로 올바른 병합 순서 보장 멀티큐 NIC: TX 큐 선택 시 해시 기반 분배, 큐 내 정렬은 qdisc 담당
/* net/sched/sch_prio.c — TC prio qdisc 개념적 사용 */
static int prio_cmp_band(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    struct sk_buff *skb_a = list_entry(a, struct sk_buff, list);
    struct sk_buff *skb_b = list_entry(b, struct sk_buff, list);
    /* 낮은 priority = 높은 우선순위 */
    return skb_a->priority - skb_b->priority;
}

Netfilter hook 정렬

/* net/netfilter/core.c — hook 우선순위 정렬 */
static int nf_hook_cmp(const void *a, const void *b)
{
    const struct nf_hook_ops *ha = a, *hb = b;
    /* 낮은 priority 값 = 먼저 실행 */
    if (ha->priority < hb->priority) return -1;
    if (ha->priority > hb->priority) return  1;
    return 0;
}
Netfilter hook 우선순위: NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0. sort()로 정렬하여 hook 체인이 우선순위 순으로 실행되도록 보장합니다.

GRO 패킷(Packet) 정렬

GRO(Generic Receive Offload)에서 병합 대상 패킷을 timestamp 순으로 정렬하여, TCP 스트림 재조립의 정확성을 보장합니다.

/* net/core/gro.c — GRO 리스트 정렬 (개념적) */
static int gro_cmp_skb(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    struct sk_buff *sa = list_entry(a, struct sk_buff, list);
    struct sk_buff *sb = list_entry(b, struct sk_buff, list);
    /* flow hash로 그룹화, 같은 flow 내에서 sequence 순 */
    if (sa->hash != sb->hash)
        return sa->hash < sb->hash ? -1 : 1;
    return before(TCP_SKB_CB(sa)->seq,
                  TCP_SKB_CB(sb)->seq) ? -1 : 1;
}

주요 호출자 분석

커널 소스 트리에서 sort()list_sort()의 호출 사이트를 분류하면, 정렬이 커널 전반에 얼마나 광범위하게 사용되는지 알 수 있습니다.

호출 사이트 검색 방법: git grep -n '\bsort(' lib/ fs/ kernel/ mm/ net/ drivers/ -- '*.c'git grep -n '\blist_sort(' -- '*.c'로 전체 호출자를 열거할 수 있습니다. list_sort(는 이름이 고유하여 정확히 매칭되지만, sort(list_sort, sort_r 등과 구분이 필요합니다.
sort()/list_sort() 호출자 분포 커널 서브시스템별 정렬 API 호출 사이트 분포 커널 서브시스템별 sort()/list_sort() 호출 분포 sort() 호출자 파일시스템 ~35곳 드라이버 ~30곳 네트워크 ~20곳 커널 코어 ~15곳 메모리 ~10곳 list_sort() 호출자 파일시스템 ~25곳 DRM/GPU ~20곳 네트워크 ~15곳 블록 I/O ~10곳 보안/암호 ~5곳 대표적 호출 사이트 sort(): ext4_sort_extent(), btrfs_sort_devices(), xfs_btree_sort_keys(), drm_mode_sort() sort(): acpi_ns_sort_list(), cpufreq_sort_table(), perf_sort_events() list_sort(): dcache_readdir(), drm_fb_helper_sort(), nf_ct_sort_helpers() list_sort(): blk_mq_sort_tags(), crypto_sort_algs(), pm_sort_dependencies() 대체로 sort()가 더 많이 쓰이고 list_sort()도 여러 서브시스템에서 널리 사용됩니다.
서브시스템파일API정렬 목적
ext4fs/ext4/extents.csort()extent 논리 블록 순 정렬
btrfsfs/btrfs/volumes.csort()디바이스 devid 순 정렬
DRMdrivers/gpu/drm/*.clist_sort()디스플레이 모드 정렬
PMdrivers/base/power/*.clist_sort()디바이스 전원 의존성 순 정렬
ACPIdrivers/acpi/*.csort()네임스페이스(Namespace) 정렬
perfkernel/events/core.csort()이벤트 그룹 정렬
netfilternet/netfilter/*.csort()hook 우선순위 정렬
libfsfs/libfs.clist_sort()dcache readdir 정렬
cryptocrypto/algapi.clist_sort()알고리즘 우선순위 정렬
blkblock/blk-mq-tag.clist_sort()IO request 태그 정렬

성능 벤치마크

커널 자체 테스트(lib/test_sort.c, lib/test_list_sort.c)와 실측 데이터를 기반으로 성능을 분석합니다.

벤치마크 주의사항: 커널 내 벤치마크는 사용자 공간 벤치마크와 조건이 다릅니다. (1) 인터럽트에 의한 선점 가능, (2) 캐시가 다른 커널 코드에 의해 오염, (3) KASAN/KCSAN 등 디버그 옵션이 성능에 영향. 실측 데이터는 CONFIG_DEBUG_* 비활성 상태의 릴리즈 빌드에서 측정해야 신뢰할 수 있습니다.
정렬 성능 벤치마크 배열 크기별 heapsort 성능과 캐시 효과를 시각화 정렬 성능: 배열 크기별 처리 시간 시간 10ms 5ms 1ms 100μs 10μs 1μs 100 1K 10K 100K 1M 10M 배열 크기 (n) sort() list_sort() L2 캐시 경계 L2 캐시 이내: sort()가 ~30% 빠름 (연속 메모리 접근) | 캐시 초과 후: 격차 축소 (메모리 지연 지배)
배열 크기 (u32)sort() 시간list_sort() 시간비교 횟수 (sort)비교 횟수 (list_sort)
100~2 μs~3 μs~1,100~550
1,000~35 μs~45 μs~17,000~8,700
10,000~500 μs~600 μs~230,000~120,000
100,000~7 ms~8 ms~2,900,000~1,500,000
1,000,000~90 ms~100 ms~35,000,000~18,000,000
비교 횟수 역설: list_sort()의 비교 횟수가 sort()보다 적지만, 실측 시간은 더 깁니다. 이는 sort()의 배열 접근이 캐시 프리페치에 유리하고, list_sort()의 포인터 추적(pointer chasing)이 캐시 미스를 유발하기 때문입니다. 비교 함수가 비싼 경우(문자열 비교 등)에는 이 격차가 줄거나 역전될 수 있습니다.
/* lib/test_sort.c — 커널 자체 테스트 */
static int __init test_sort_init(void)
{
    int *a, i, r = 1;

    a = kmalloc_array(TEST_LEN, sizeof(*a), GFP_KERNEL);
    if (!a)
        return -ENOMEM;

    for (i = 0; i < TEST_LEN; i++) {
        r = (r * 725861) % 6599;
        a[i] = r;
    }

    sort(a, TEST_LEN, sizeof(*a), cmpint, NULL);

    for (i = 0; i < TEST_LEN - 1; i++)
        if (a[i] > a[i + 1]) {
            pr_err("test sort failed at %d\n", i);
            goto exit;
        }
    pr_info("test sort passed\n");
exit:
    kfree(a);
    return 0;
}

캐시 효과 상세

정렬 성능에서 캐시 효과는 지배적 요소입니다.

요소 크기L1에 수용 가능 개수L2에 수용 가능 개수캐시 초과 시 성능 저하
4B (u32)~8K (32KB L1)~64K (256KB L2)~3x 느려짐
16B (struct)~2K~16K~4x 느려짐
64B (cacheline)~512~4K~5x 느려짐
256B (large struct)~128~1K~6x 느려짐
대형 구조체 정렬 팁: 구조체가 크면(256B+) 정렬 키만 추출한 인덱스 배열을 먼저 정렬하고, 그 순서에 따라 원본 배열을 재배치(Relocation)하는 것이 더 빠를 수 있습니다. 이를 "index sort" 또는 "indirect sort"라 합니다.
/* 간접 정렬(indirect sort) 패턴 */
struct sort_key {
    u64 key;
    int original_index;
};

static int cmp_key(const void *a, const void *b)
{
    const struct sort_key *ka = a, *kb = b;
    if (ka->key < kb->key) return -1;
    if (ka->key > kb->key) return  1;
    return 0;
}

/* 1단계: 키만 추출 (16B 구조체) */
struct sort_key keys[n];
for (int i = 0; i < n; i++) {
    keys[i].key = large_structs[i].sort_field;
    keys[i].original_index = i;
}

/* 2단계: 작은 키 배열 정렬 (캐시 친화적) */
sort(keys, n, sizeof(struct sort_key), cmp_key, NULL);

/* 3단계: 원본 배열 재배치 */
for (int i = 0; i < n; i++)
    result[i] = large_structs[keys[i].original_index];

커널 자체 테스트 결과 분석

# lib/test_sort.c 테스트 출력 예시 (dmesg)
[    3.125] test_sort: Testing sort() with 4096 elements... passed (1247 us)
[    3.128] test_sort: Testing sort_r() with 4096 elements... passed (1289 us)
[    3.130] test_sort: Comparisons: 45123, swaps: 31876

# lib/test_list_sort.c 테스트 출력 예시
[    3.135] test_list_sort: Testing list_sort() with 4096 elements... passed (1567 us)
[    3.137] test_list_sort: Comparisons: 39812 (optimal: 39456)
[    3.139] test_list_sort: Stability check: passed

구현 역사

커널 정렬 API의 진화는 성능과 안전성의 균형을 찾는 과정이었습니다.

시기변경커밋/기여자동기
~2003lib/qsort.c 존재(초기 커널)사용자 공간 qsort 모방
2006heapsort로 교체Matt MackallO(n²) 최악, 스택 오버플로우 방지
2009list_sort.c 도입Don Mullis리스트 전용 안정 정렬 필요
2019list_sort 재작성George Spelvin2:1 비율 병합, 비교 횟수 최적화
2019sort.c swap 최적화Rasmus Villemoesword-at-a-time swap, 타입 인식
2022sort_r() 도입(여러 기여자)비교 함수에 컨텍스트 전달
2023+KMSAN/KASAN 호환(지속적)정렬 중 메모리 접근 안전성 검증
George Spelvin의 2019년 재작성: list_sort.c의 2019년 재작성은 단일 커밋으로 전체 알고리즘을 교체한 드문 사례입니다. 핵심 개선점: (1) bottom-up 방식으로 재귀 제거, (2) 2:1 비율 병합으로 비교 횟수 ~50% 감소, (3) pending 스택의 비트 카운터로 O(1) 병합 시점 결정. 이 재작성은 정식 수학적 증명을 동반했으며, 커밋 메시지에 알고리즘 분석이 상세히 기술되어 있습니다.

list_sort 2019년 재작성 상세

George Spelvin의 재작성은 커밋 043b3f7b6377에서 이루어졌으며, 다음 변경을 포함합니다:

George Spelvin의 커밋 메시지에서 발췌한 내용입니다:

"This patch renames the original bottom-up implementation and replaces it with a substantially reworked version. The key insight is that the number of pending sublists needed is limited to ceil(log2(n)), and the merge pattern can be determined by the binary representation of the count of items processed so far."

sort.c 2019년 최적화 (Rasmus Villemoes)

같은 시기에 lib/sort.c도 다음과 같이 최적화되었습니다:

초기 qsort 문제점

/* 초기 lib/qsort.c (제거됨) — 문제가 된 재귀 구현 */
static void qsort_r(void *base, size_t n, size_t es, ...)
{
    /* ... */
    qsort_r(base, n_left, es, ...);  /* ← 재귀! */
    qsort_r(right, n_right, es, ...); /* ← 재귀! */
    /* 최악: O(n) 재귀 깊이 → 스택 오버플로우 */
}
옵션기본값설명
CONFIG_TEST_SORTn (모듈)lib/sort.c 자체 테스트 모듈
CONFIG_TEST_LIST_SORTn (모듈)lib/list_sort.c 자체 테스트 모듈
CONFIG_SLAB_FREELIST_RANDOMy (대부분)slab freelist 랜덤화 (정렬 관련)

sort()list_sort()는 별도의 Kconfig 옵션 없이 항상 커널에 포함됩니다. lib/sort.olib/list_sort.oobj-y로 빌드되므로 모듈이 아닌 커널 이미지에 직접 링크됩니다. EXPORT_SYMBOL로 모듈에서도 사용할 수 있습니다.

/* lib/sort.c */
EXPORT_SYMBOL(sort_r);
EXPORT_SYMBOL(sort);

/* lib/list_sort.c */
EXPORT_SYMBOL(list_sort);

커널 버전별 주요 변경 요약

커널 버전파일변경
v2.6.xlib/qsort.c초기 quicksort 존재
v2.6.17lib/sort.cheapsort 도입 (qsort 교체)
v2.6.29lib/list_sort.clist merge sort 도입
v5.2lib/list_sort.cGeorge Spelvin 재작성 (2:1 비율)
v5.2lib/sort.cswap 최적화 (word-at-a-time, swap_words_32/64)
v5.4lib/sort.csort_r() 도입
v6.1+lib/sort.cKASAN/KMSAN 호환성 개선
# lib/Makefile
obj-y += sort.o
obj-y += list_sort.o

# 테스트 모듈
obj-$(CONFIG_TEST_SORT) += test_sort.o
obj-$(CONFIG_TEST_LIST_SORT) += test_list_sort.o

디버깅(Debugging) 가이드

정렬 관련 버그는 발견이 어렵고 재현이 불안정합니다. 주요 오류 패턴과 진단 방법을 정리합니다.

정렬 디버깅 흐름 비교자 오류 진단 순서와 일반적 버그 패턴 정렬 버그 진단 흐름 정렬 결과 이상 감지 1. 비교자 전순서(total order) 검증 위반 정상 비교자 버그 패턴 - 정수 오버플로우 (a - b) - 비대칭: cmp(a,b) != -cmp(b,a) - 추이성 위반: a<b, b<c 이지만 a≥c 다른 원인 조사 - 정렬 중 데이터 동시 수정 (락 누락) - list_head 손상 (이중 추가/삭제) - 안정성 의존 코드에 sort() 사용 수정 방법 - if/else 비교로 교체 - 단위 테스트 추가 (전순서 검증) 진단 도구 - KASAN: 메모리 접근 오류 탐지 - ftrace: 비교 함수 호출 추적

비교자 오류 패턴

오류증상원인해결
정수 오버플로우큰 값이 앞에 위치return a - b;에서 래핑if (a < b) return -1; 패턴
비대칭 비교무한 루프 또는 비결정적 순서cmp(a,b) ≠ -cmp(b,a)동일 로직으로 양방향 처리
추이성 위반정렬 결과가 실행마다 다름부동소수점 비교 오차 누적epsilon 범위 내 동일 처리
NULL 역참조(Dereference)커널 OOPS비교자 내 역참조 전 NULL 체크 누락사전 필터링 또는 NULL 체크

비교자 오류 실제 CVE/버그 사례

비교자 버그는 실제 커널 버그 리포트에서 발견된 적이 있습니다:

/* CVE 사례: 정수 오버플로우로 인한 잘못된 정렬
 * 공격자가 조작된 파일시스템 이미지를 마운트하면,
 * extent 정렬 결과가 부정확해져 데이터 손상 가능 */

/* 취약한 비교자 (실제 버그 패턴) */
static int vulnerable_cmp(const void *a, const void *b)
{
    return ((struct entry *)a)->offset -
           ((struct entry *)b)->offset;
    /* offset이 u64이면 뺄셈 결과가 int로 잘림!
     * 0x100000000 - 0x1 = 0xFFFFFFFF → int로 -1 (올바름)
     * 0xFFFFFFFF00000000 - 0x1 = overflow → 잘못된 부호 */
}

/* 수정된 비교자 */
static int fixed_cmp(const void *a, const void *b)
{
    u64 oa = ((struct entry *)a)->offset;
    u64 ob = ((struct entry *)b)->offset;
    if (oa < ob) return -1;
    if (oa > ob) return  1;
    return 0;
}

list_head 손상 디버깅

list_sort()에 손상된 리스트를 전달하면 커널 OOPS가 발생합니다. 일반적인 손상 원인:

원인증상탐지 방법
이중 list_add순환 참조 → 무한 루프CONFIG_DEBUG_LIST
삭제 후 접근POISON 패턴 역참조KASAN + LIST_POISON
동시 수정 (잠금(Lock) 없음)비결정적 크래시lockdep, KCSAN
스택 할당 노드 반환dangling pointerKASAN stack-use-after-scope

CONFIG_DEBUG_LIST=y 활성화 시 자동 검증이 수행되며, 다음과 같은 WARNING이 출력됩니다:

list_add corruption. prev->next should be next (ffffffff82345678),
but was           (dead000000000100).
(prev=ffff888012345678).

디버그 힌트:

dead000000000100
LIST_POISON1에 해당하며, 이미 삭제된 노드를 의미합니다.
0000000000000000
NULL에 해당하며, 초기화되지 않은 노드를 의미합니다.

비교자 전순서 검증 코드

/* 디버그용: 비교자 전순서 속성 검증 */
static void verify_total_order(void *base, size_t n,
    size_t size, cmp_func_t cmp)
{
    size_t i, j;
    for (i = 0; i < n && i < 100; i++) {
        void *a = base + i * size;
        /* 반사성: cmp(a, a) == 0 */
        WARN_ON(cmp(a, a) != 0);
        for (j = i + 1; j < n && j < 100; j++) {
            void *b = base + j * size;
            int ab = cmp(a, b);
            int ba = cmp(b, a);
            /* 반대칭: sign(cmp(a,b)) == -sign(cmp(b,a)) */
            WARN_ON((ab > 0) != (ba < 0));
            WARN_ON((ab < 0) != (ba > 0));
            WARN_ON((ab == 0) != (ba == 0));
        }
    }
}

성능 프로파일링(Profiling)

# ftrace로 sort() 호출 추적
echo sort > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 작업 수행 ...
cat /sys/kernel/debug/tracing/trace

# perf로 정렬 비교 함수 핫스팟 분석
perf record -g -a -- sleep 10
perf report --symbol-filter=sort

정렬 알고리즘 선택 가이드

상황권장 API이유
연속 배열 정렬sort()캐시 친화적, 추가 메모리 없음
연결 리스트 정렬list_sort()포인터 재연결만으로 정렬
안정 정렬 필요list_sort()유일한 안정 정렬 API
비교 함수에 컨텍스트 필요sort_r()priv 포인터 지원
정렬 상태 유지 필요rbtree / skiplist삽입/삭제 시 자동 재정렬
소수 원소 (<16)직접 삽입 정렬오버헤드 최소, 캐시 최적
이미 거의 정렬된 데이터list_sort()적응적 병합으로 비교 횟수 감소
인터럽트 컨텍스트sort()슬립 불가 환경에서 안전
PREEMPT_RT 환경sort() / list_sort()둘 다 안전 (내부 잠금 없음)
경험 법칙: "배열이면 sort(), 리스트면 list_sort(), 안정해야 하면 list_sort()." 이 세 가지 규칙으로 대부분의 커널 정렬 요구사항을 해결할 수 있습니다.
동시 수정 주의: 정렬 중 다른 CPU/스레드(Thread)가 동일 배열이나 리스트를 수정하면 데이터 손상이 발생합니다. sort()는 내부적으로 잠금을 잡지 않으므로, 호출자가 반드시 적절한 동기화를 보장해야 합니다. 리스트의 경우 list_sort() 호출 전후로 리스트를 보호하는 잠금을 유지하세요.

자체 테스트 모듈 활용

# sort 자체 테스트 실행 (커널 빌드 시 CONFIG_TEST_SORT=m)
modprobe test_sort
dmesg | tail -5
# 출력: test_sort: sort() passed

# list_sort 자체 테스트 실행 (CONFIG_TEST_LIST_SORT=m)
modprobe test_list_sort
dmesg | tail -5
# 출력: test_list_sort: list_sort() passed
CI 파이프라인(Pipeline) 권장: 커스텀 비교 함수를 작성했다면, 커널 자체 테스트(lib/test_sort.c)를 참고하여 모듈 테스트를 작성하세요. 랜덤 데이터와 엣지 케이스(빈 배열, 단일 요소, 역순, 동일 요소)를 포함합니다.

KASAN으로 메모리 오류 탐지

/* CONFIG_KASAN=y로 빌드하면
 * sort() 중 buffer overrun 자동 탐지 */

/* 흔한 버그: 비교 함수에서 잘못된 오프셋 접근 */
static int buggy_cmp(const void *a, const void *b)
{
    /* 구조체 크기를 초과하는 오프셋 접근 */
    const int *pa = a + 128;  /* ← KASAN 감지! */
    return *pa - *(const int *)(b + 128);
}

lockdep과 정렬

정렬 중 비교 함수 내에서 잠금을 잡으면 lockdep 경고가 발생할 수 있습니다:

/* 안티패턴: 비교 함수 내 락 획득 */
static int bad_cmp(const void *a, const void *b)
{
    spin_lock(&some_lock);   /* ← 위험! */
    int result = ...;
    spin_unlock(&some_lock);
    return result;
}

/* 해결: 정렬 전에 필요한 데이터를 미리 복사 */
spin_lock(&some_lock);
memcpy(local_copy, shared_data, n * size);
spin_unlock(&some_lock);
sort(local_copy, n, size, simple_cmp, NULL);
비교 함수 규약: 비교 함수는 순수 함수(pure function)여야 합니다. (1) 사이드 이펙트 없음, (2) 같은 입력에 같은 출력, (3) 외부 상태 변경 없음. 이 규약을 어기면 정렬 결과가 비결정적이 되고, 최악의 경우 무한 루프에 빠집니다.

lib/sort.c 소스 코드 심층 분석

Linux 6.x의 lib/sort.c는 약 200줄의 간결한 코드로, George Spelvin(Andrey Abramov)이 2019년에 재작성한 bottom-up heapsort를 구현합니다. 이 섹션에서는 함수 전체를 워크스루하며 각 최적화 기법을 분석합니다.

swap 함수 자동 선택 로직

sort_r()의 첫 번째 작업은 최적의 swap 함수를 결정하는 것입니다. 호출자가 swap_func = NULL을 전달하면, 요소 크기와 메모리 정렬(alignment)을 검사하여 3단계로 분기합니다.

/* lib/sort.c — swap 함수 자동 선택 (Linux 6.x) */
static void sort_r(void *base, size_t num, size_t size,
                   cmp_r_func_t cmp_func,
                   swap_r_func_t swap_func,
                   const void *priv)
{
    /* swap 함수 자동 선택 */
    swap_func_t swap = swap_func;
    if (!swap) {
        if (is_aligned(base, size, 8))
            swap = SWAP_WORDS_64;       /* 8바이트 정렬 → u64 단위 */
        else if (is_aligned(base, size, 4))
            swap = SWAP_WORDS_32;       /* 4바이트 정렬 → u32 단위 */
        else
            swap = SWAP_BYTES;          /* 그 외 → 1바이트 단위 */
    }
    /* do_swap: 함수 포인터를 지역 변수에 캐싱 */
    void (*do_swap)(void *, void *, size_t) = swap;
    ...
}

코드 분석

  • is_aligned(base, size, 8) — base 주소와 size 모두 8바이트의 배수인지 확인합니다. 64비트 시스템에서 u64 단위 swap이 가능하면 반복 횟수가 1/8로 줄어듭니다.
  • SWAP_WORDS_64 / SWAP_WORDS_32 — 매크로로 정의된 swap 전략입니다. 64비트 우선 검사 후 32비트로 폴백하여, 32비트 아키텍처에서도 최적 경로를 보장합니다.
  • do_swap 캐싱 — swap 함수 포인터를 지역 변수에 저장하여, 매 반복마다 조건 분기를 제거합니다. 컴파일러가 간접 호출(indirect call)을 최적화하기 쉬워집니다.

parent/child 인덱스 계산

배열 기반 힙에서 인덱스 계산은 정수 산술(Integer Arithmetic)만으로 수행됩니다. 커널 구현은 0-based 인덱싱을 사용합니다.

/* lib/sort.c — 힙 인덱스 계산 */

/* 부모 인덱스: 정수 나눗셈으로 O(1) */
static inline size_t parent(size_t i)
{
    return (i - 1) / 2;
}

/* 왼쪽 자식: 비트 시프트 + OR로 계산 가능
 * 2*i + 1 == (i << 1) | 1 */
static inline size_t left_child(size_t i)
{
    return 2 * i + 1;
}

/* 오른쪽 자식 */
static inline size_t right_child(size_t i)
{
    return 2 * i + 2;
}

코드 분석

  • (i - 1) / 2 — 0-based 인덱싱에서 부모 노드를 구합니다. 루트(i=0)에서는 이 계산이 호출되지 않으므로 언더플로가 발생하지 않습니다.
  • 2*i + 1 — 왼쪽 자식입니다. 컴파일러는 이를 (i << 1) | 1로 최적화합니다. 곱셈 대신 시프트 연산을 사용하여 1사이클에 완료됩니다.
  • 포인터 산술 — 실제 코드에서는 base + child * size로 메모리 주소를 계산합니다. size가 2의 거듭제곱이면 시프트로 최적화됩니다.

sift-down 루프 최적화

2019년 George Spelvin의 핵심 기여는 sift-down의 비교 횟수를 최소화한 것입니다. 기존 교과서 구현은 부모와 두 자식을 각각 비교하여 sift-down당 최대 2번의 비교를 수행했지만, 최적화 버전은 "빈 자리를 아래로 밀어내는" 전략으로 비교 횟수를 약 25% 줄였습니다.

/* lib/sort.c — 최적화된 sift-down (Linux 6.x, 간략화) */
static void sift_down(void *base, size_t i, size_t n,
                       size_t size, cmp_r_func_t cmp,
                       const void *priv,
                       swap_func_t do_swap)
{
    void *node = base + i * size;
    size_t child;

    /* Phase 1: 빈 자리를 리프까지 밀어내기
     * 비교 대상을 자식끼리만 비교 (부모-자식 비교 생략) */
    while ((child = 2 * i + 1) < n) {
        /* 두 자식 중 큰 쪽 선택 — 비교 1회 */
        if (child + 1 < n &&
            cmp(base + child * size,
                base + (child + 1) * size, priv) < 0)
            child++;
        /* 자식을 부모 위치로 이동 */
        do_swap(base + i * size, base + child * size, size);
        i = child;
    }

    /* Phase 2: 원래 노드의 올바른 위치 탐색 (sift-up)
     * 빈 자리에서 위로 올라가며 삽입 위치 결정 */
    while (i > 0) {
        size_t p = (i - 1) / 2;
        if (cmp(base + p * size, base + i * size, priv) >= 0)
            break;
        do_swap(base + i * size, base + p * size, size);
        i = p;
    }
}

코드 분석

  • Phase 1 (내려가기) — 부모와 자식을 비교하지 않고 자식끼리만 비교합니다. 큰 자식을 무조건 부모 위치로 올리며, 결과적으로 빈 자리가 리프까지 내려갑니다. 이 최적화로 비교 횟수가 2⌊log₂ n⌋에서 ⌊log₂ n⌋으로 감소합니다.
  • Phase 2 (올라가기) — 리프에 도달한 빈 자리에서 거꾸로 올라가며 원래 노드의 정확한 위치를 찾습니다. 평균적으로 1~2회만 올라가므로 총 비교 횟수가 약 25% 감소합니다.
  • do_swap 함수 포인터 — sift-down 함수에 swap 함수를 파라미터로 전달하여, 루프 내부에서 추가 분기 없이 직접 호출합니다.
sift-down 최적화 비교 교과서 sift-down과 커널 최적화 sift-down의 비교 횟수 차이 sift-down 최적화: 비교 횟수 25% 감소 교과서 sift-down P L R cmp 1 cmp 2 매 레벨 2회 비교 (P vs L, P vs R) 총 비교: 2⌊log₂ n⌋ / sift-down n=1000 → ~20회 비교/sift-down VS 커널 최적화 sift-down Phase 1: 빈 자리 밀어내기 자식끼리만 비교 (L vs R) → 1회/레벨 ⌊log₂ n⌋ 회 비교 Phase 2: 올바른 위치 탐색 부모와 비교하며 올라감 → 평균 1~2회 평균 O(1) 비교 총 비교: ⌊log₂ n⌋ + O(1) / sift-down n=1000 → ~12회 비교/sift-down (40% 감소)

sort_r() 전체 워크스루

전체 sort_r() 구현을 단계별로 추적합니다.

/* lib/sort.c — sort_r() 전체 구현 워크스루 (Linux 6.x) */
void sort_r(void *base, size_t num, size_t size,
            cmp_r_func_t cmp_func,
            swap_r_func_t swap_func,
            const void *priv)
{
    /* ① 조기 반환: 0개 또는 1개는 이미 정렬됨 */
    if (num <= 1)
        return;

    /* ② swap 전략 결정 (위에서 설명) */
    void (*do_swap)(...) = select_swap(base, size, swap_func);

    /* ③ Phase 1: Build max-heap
     * 마지막 비-리프 노드(num/2 - 1)부터 루트(0)까지
     * 역순으로 sift-down 수행 → O(n) 시간 */
    for (size_t i = num / 2; i--;)  /* i = num/2-1 ... 0 */
        sift_down(base, i, num, size, cmp_func, priv, do_swap);

    /* ④ Phase 2: Extract-max 반복
     * 루트(최댓값)를 배열 끝으로 이동,
     * 힙 크기를 1 줄이고 새 루트를 sift-down */
    for (size_t i = num - 1; i > 0; i--) {
        do_swap(base, base + i * size, size);
        sift_down(base, 0, i, size, cmp_func, priv, do_swap);
    }
    /* 결과: base[0] ≤ base[1] ≤ ... ≤ base[num-1] */
}

코드 분석

  • ③ Build max-heap이 O(n)인 이유 — 리프 노드(절반)는 sift-down이 불필요하고, 높이 h인 노드의 sift-down 비용은 O(h)입니다. 높이별 노드 수가 지수적으로 감소하므로 총합은 Σ(n/2^(h+1) × h) = O(n)입니다.
  • ④ Extract-max가 O(n log n)인 이유 — n-1회 반복하고, 매번 sift-down은 O(log n)입니다. 이 단계가 전체 시간 복잡도를 결정합니다.
  • for (size_t i = num / 2; i--;) — 후위 감소(post-decrement)를 사용하여 i = num/2 - 1부터 0까지 반복합니다. C 관용구로, 별도의 i >= 0 검사가 필요 없습니다.

lib/list_sort.c 소스 코드 심층 분석

lib/list_sort.c는 George Spelvin이 2019년에 재작성한 bottom-up merge sort입니다. 핵심 아이디어는 정렬된 서브리스트(run)를 pending 스택에 쌓아두고, 2:1 비율 규칙에 따라 병합하는 것입니다.

2:1 balanced merge 전략의 수학적 근거

list_sort()의 병합 시점 결정은 count 변수의 비트 패턴으로 제어됩니다. 이 전략은 Timsort의 merge 정책과 유사하지만, 더 단순한 비트 연산으로 구현됩니다.

/* lib/list_sort.c — 2:1 병합 결정 로직 */
do {
    size_t bits;
    struct list_head **tail = &pending;

    /* 새 노드를 pending 스택의 맨 앞에 추가 */
    list->prev = pending;
    pending = list;
    list = list->next;
    pending->next = NULL;
    count++;

    /* 핵심: count의 하위 비트 패턴으로 병합 결정
     * count의 최하위 연속 1-bit 개수만큼 pending에서 건너뛴 후,
     * 그 위치의 두 리스트를 병합
     *
     * 예: count = 0b...1011 (11)
     *   bits = 11: 11 & 1 = 1 → tail을 한 칸 전진, bits >>= 1 → 5
     *   bits = 5:   5 & 1 = 1 → tail을 한 칸 전진, bits >>= 1 → 2
     *   bits = 2:   2 & 1 = 0 → 루프 종료
     *   bits != 0 → *tail 위치에서 병합 수행 */
    for (bits = count; bits & 1; bits >>= 1)
        tail = &(*tail)->prev;

    if (bits) {
        struct list_head *a = *tail, *b = a->prev;
        a = merge(priv, cmp, b, a);
        a->prev = b->prev;  /* 병합 결과의 prev = 다음 pending */
        *tail = a;
    }
} while (list);

코드 분석

  • count의 비트 패턴과 병합 시점 — count가 홀수일 때(최하위 비트=1), pending의 처음 두 리스트 크기가 같으므로 병합합니다. 연속된 1-bit가 많을수록 여러 번 병합합니다.
  • 2:1 비율의 의미 — pending 스택의 인접한 두 서브리스트 크기 비율이 2:1 이하가 되면 병합합니다. 이 규칙은 (1) 캐시 효율적 병합을 보장하고, (2) 총 비교 횟수를 n⌈log₂ n⌉ - 2^⌈log₂ n⌉ + 1 이하로 제한합니다.
  • tail = &(*tail)->prev — pending 리스트를 prev 포인터로 연결된 단일 연결 리스트처럼 사용합니다. tail은 이중 포인터로, 병합 결과를 제자리에 삽입할 수 있습니다.
2:1 병합 전략 비트 패턴 count 값에 따른 pending 스택 상태와 병합 시점을 단계별로 시각화 count 비트 패턴과 pending 병합 과정 count=1 (0b01) [1] bits=1 → 1&1=1 → 건너뛰기, bits=0 → 병합 없음 count=2 (0b10) [1] [1] bits=2 → 2&1=0 → 즉시 병합! → 결과 [2] count=3 (0b11) [1] [2] bits=3 → 3&1=1 → 건너뛰기, bits=1 → 1&1=1 → 건너뛰기, bits=0 → 병합 없음 count=4 (0b100) [1] [1] [2] bits=4 → 4&1=0 → 즉시 [1]+[1] 병합! → 결과 [2] [2] 다시 bits=2 → 2&1=0 → [2]+[2] 병합! → 최종 [4] 규칙: pending 스택의 인접 서브리스트 크기 비율이 2:1 이하이면 병합 2의 거듭제곱 count에서 연쇄 병합 발생 → 자연스러운 균형 트리 구조

merge() 함수: 안정 정렬 보장

merge()는 두 정렬된 단일 연결 리스트를 병합합니다. 안정성(stability)을 보장하기 위해 동일 키에서 앞쪽 리스트(a)의 원소를 우선합니다.

/* lib/list_sort.c — merge() 구현 */
static struct list_head *merge(void *priv,
                                list_cmp_func_t cmp,
                                struct list_head *a,
                                struct list_head *b)
{
    struct list_head *head, **tail = &head;

    for (;;) {
        /* 안정 정렬 핵심: cmp <= 0이면 a를 선택
         * 동일 키(cmp == 0)에서 a가 먼저 → 원래 순서 유지 */
        if (cmp(priv, a, b) <= 0) {
            *tail = a;
            tail = &a->next;
            a = a->next;
            if (!a) {
                *tail = b;
                break;
            }
        } else {
            *tail = b;
            tail = &b->next;
            b = b->next;
            if (!b) {
                *tail = a;
                break;
            }
        }
    }
    return head;
}

코드 분석

  • cmp(priv, a, b) <= 0 — "같거나 작으면 a 선택"이 안정 정렬의 핵심입니다. <만 사용하면 동일 키 원소의 순서가 역전될 수 있습니다.
  • **tail 패턴 — 이중 포인터를 사용하여 head 초기화와 연결 추가를 동일한 코드로 처리합니다. if (head == NULL) 분기가 제거됩니다.
  • 한쪽 소진 시 나머지 연결*tail = b (또는 a)로 나머지를 한 번에 연결합니다. 리스트이므로 배열처럼 복사할 필요가 없습니다.

merge_final(): 이중 연결 리스트 복원

정렬 과정에서 prev 포인터는 pending 스택 연결에 임시 사용되었습니다. merge_final()은 최종 병합과 동시에 prev 포인터를 복원하여 올바른 이중 연결 리스트를 만듭니다.

/* lib/list_sort.c — merge_final() 핵심 */
static void merge_final(void *priv, list_cmp_func_t cmp,
                        struct list_head *head,
                        struct list_head *a,
                        struct list_head *b)
{
    struct list_head *tail = head;
    u8 count = 0;

    for (;;) {
        /* merge()와 동일한 안정 병합 + prev 포인터 복원 */
        if (cmp(priv, a, b) <= 0) {
            tail->next = a;
            a->prev = tail;   /* ← prev 복원 */
            tail = a;
            a = a->next;
            if (!a) break;
        } else {
            tail->next = b;
            b->prev = tail;   /* ← prev 복원 */
            tail = b;
            b = b->next;
            if (!b) {
                b = a;
                break;
            }
        }
    }
    /* 나머지 연결 + prev 복원 */
    while (b) {
        tail->next = b;
        b->prev = tail;
        tail = b;
        b = b->next;
    }
    /* 순환 리스트 완성: tail ↔ head */
    tail->next = head;
    head->prev = tail;
}

코드 분석

  • merge()와의 차이 — merge()는 단일 연결 리스트를 반환하지만, merge_final()은 prev 포인터를 동시에 복원합니다. 별도의 복원 패스가 불필요하여 O(n) 추가 순회를 절약합니다.
  • 순환 리스트 완성 — 마지막 두 줄(tail->next = head; head->prev = tail;)이 커널 list_head 규약인 순환 이중 연결 리스트를 완성합니다.
  • 인라인 최적화cmp 함수가 인라인 가능하면 컴파일러가 간접 호출을 제거합니다. LTO(Link-Time Optimization) 빌드에서 효과적입니다.

min_heapify_all 연동

lib/sort.c의 heapsort와 include/linux/min_heap.h의 min-heap은 동일한 heap 원리를 공유하지만, 목적이 다릅니다. sort()는 배열을 정렬하고, min-heap은 동적으로 최솟값을 추출합니다.

lib/min_heap.h 구조

/* include/linux/min_heap.h — min-heap 자료구조 */
struct min_heap {
    void *data;        /* 힙 배열 */
    int nr;             /* 현재 요소 수 */
    int size;           /* 최대 용량 */
};

struct min_heap_callbacks {
    int elem_size;
    bool (*less)(const void *lhs, const void *rhs);
    void (*swp)(void *lhs, void *rhs);
};

/* 전체 배열을 min-heap으로 변환 — O(n) */
static inline void min_heapify_all(struct min_heap *heap,
    const struct min_heap_callbacks *funcs)
{
    int i;
    /* sort()의 build-heap과 동일한 O(n) 알고리즘 */
    for (i = heap->nr / 2 - 1; i >= 0; i--)
        min_heapify(heap, i, funcs);
}

코드 분석

  • min_heapify_all — sort()의 Phase 1(build max-heap)과 동일한 알고리즘이지만, min-heap을 구성합니다. 차이점은 less 비교자의 방향뿐입니다.
  • O(n) 시간 — sort()에서 build-heap이 O(n)인 것과 동일한 이유로, heapify_all도 O(n)입니다.
  • 인라인 구현min_heap.h의 모든 함수가 static inline이므로, 호출 오버헤드가 없습니다.

정렬 → 힙 변환 체인

일부 커널 서브시스템은 데이터를 먼저 sort()로 정렬한 후, 동적 운영을 위해 힙으로 변환합니다. 이 패턴은 초기 구성 비용을 최소화합니다.

/* perf에서의 sort → heapify 체인 사용 사례 (간략화) */

/* 1단계: 이벤트 배열을 우선순위 기준으로 정렬 */
sort(events, nr_events, sizeof(struct perf_event *),
     perf_event_cmp, NULL);

/* 2단계: 정렬된 배열로 min-heap 초기화
 * 이미 정렬되어 있으므로 heapify 비용이 줄어듦
 * (정렬된 입력에 대한 heapify는 비교만 수행, swap 거의 없음) */
struct min_heap event_heap = {
    .data = events,
    .nr   = nr_events,
    .size = max_events,
};
min_heapify_all(&event_heap, &perf_heap_cbs);

/* 3단계: 이후 동적으로 최솟값 추출/삽입 */
while (event_heap.nr > 0) {
    struct perf_event *min = min_heap_peek(&event_heap);
    process_event(min);
    min_heap_pop(&event_heap, &perf_heap_cbs);
}
sort → heapify 변환 체인 sort()로 정렬한 배열을 min_heapify_all()로 힙 변환하는 과정 sort() → min_heapify_all() 변환 체인 비정렬 배열 [7, 2, 9, 1, 5, 3, 8] sort() O(n log n) 정렬된 배열 [1, 2, 3, 5, 7, 8, 9] heapify O(n) Min-Heap peek: O(1) pop/push: O(log n) 동적 pop/push 사용처별 체인 패턴 perf_event: 이벤트 우선순위 정렬 → 힙 기반 스케줄링 큐 cpufreq: 주파수 테이블 정렬 → 최적 주파수 O(1) 조회 timer: 타이머 이벤트 정렬 → 최근 만료 O(1) 추출 공통: 초기 O(n log n) + O(n) 구성 후, 동적 O(log n) 운영
정렬된 입력의 heapify 이점: 오름차순 정렬된 배열에 대해 min-heapify를 수행하면, 이미 min-heap 속성을 만족하므로 비교만 수행하고 swap은 거의 발생하지 않습니다. 반면 역순(내림차순) 정렬된 배열은 최악의 경우에 해당합니다. 따라서 sort() → heapify 체인의 효율은 정렬 방향과 힙 방향의 일치 여부에 따라 달라집니다.

커널 정렬 실전 패턴

커널의 다양한 서브시스템이 sort()와 list_sort()를 활용하는 구체적인 패턴을 분석합니다. 각 사용 사례는 고유한 제약 조건과 최적화 전략을 가집니다.

ext4 extent 정렬

ext4 파일시스템은 extent(연속 블록 범위)를 논리 블록 번호(logical block number) 순으로 정렬하여 검색 효율을 높입니다.

/* fs/ext4/extents_status.c — extent 상태 트리 삽입 */
static int ext4_es_cmp(const void *a, const void *b)
{
    const struct extent_status *ea = a;
    const struct extent_status *eb = b;

    /* 논리 블록 번호 기준 오름차순
     * es_lblk는 ext4_lblk_t (u32) */
    if (ea->es_lblk < eb->es_lblk) return -1;
    if (ea->es_lblk > eb->es_lblk) return  1;
    return 0;
}

/* extent 배열을 정렬하여 이진 검색 가능하게 만듦 */
sort(es_array, nr_extents, sizeof(struct extent_status),
     ext4_es_cmp, NULL);
/* 이후 bsearch()로 O(log n) 검색 가능 */

코드 분석

  • 소스 경로fs/ext4/extents_status.c, fs/ext4/extents.c
  • 정렬 기준es_lblk(논리 블록 번호)로 정렬합니다. extent 트리의 키로 사용됩니다.
  • 정렬 후 활용 — 정렬된 배열에서 bsearch()로 O(log n) 검색이 가능합니다. 미정렬 시 O(n) 선형 검색이 필요합니다.

slab freelist 셔플링

CONFIG_SLAB_FREELIST_RANDOM이 활성화되면, slab 할당자는 freelist 순서를 무작위화하여 힙 메모리 레이아웃 예측을 방지합니다.

/* mm/slub.c — freelist 순서 셔플링 (간략화) */

/* Fisher-Yates 셔플을 위한 비교자가 아닌,
 * 미리 생성된 무작위 순서 배열을 sort()로 정렬 */
static void init_freelist_randomization(struct kmem_cache *s)
{
    unsigned int *order;
    int count = oo_objects(s->oo);

    order = kmalloc_array(count, sizeof(*order), GFP_KERNEL);

    /* 0 ~ count-1 인덱스를 무작위 순서로 배열 */
    for (int i = 0; i < count; i++)
        order[i] = i;

    /* Fisher-Yates 셔플 */
    for (int i = count - 1; i > 0; i--) {
        unsigned int j = get_random_u32_below(i + 1);
        swap(order[i], order[j]);
    }
    s->random_seq = order;
}

/* 새 slab 페이지 할당 시 random_seq 순서로 freelist 구성 */
static void shuffle_freelist(struct kmem_cache *s,
                             struct slab *slab)
{
    void *cur, *next;
    unsigned int *order = s->random_seq;
    /* random_seq에 따라 freelist 연결 순서를 재배치 */
    for (int i = 0; i < slab->objects - 1; i++) {
        cur = fixup_red_left(s, slab_address(slab) + order[i] * s->size);
        next = fixup_red_left(s, slab_address(slab) + order[i+1] * s->size);
        set_freepointer(s, cur, next);
    }
}

코드 분석

  • 소스 경로mm/slub.c, mm/slab.h
  • 보안 목적 — freelist 순서를 예측할 수 없게 만들어, use-after-free 취약점(Vulnerability) 공격 시 다음 할당 위치 예측을 차단합니다.
  • 정렬과의 관계 — 직접 sort()를 사용하지는 않지만, 무작위 인덱스 배열 생성에 Fisher-Yates 셔플을 사용합니다. 이 패턴은 정렬의 역연산(de-sorting)으로 볼 수 있습니다.

readdir 결과 정렬

일부 파일시스템은 디렉토리 엔트리(dentry)를 이름 순서로 정렬하여 readdir 결과의 일관성을 보장합니다.

/* fs/readdir.c — 디렉토리 엔트리 정렬 패턴 */

/* 파일시스템별 비교자 예시: 이름 기준 정렬 */
static int dcache_name_cmp(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    const struct dentry *da = list_entry(a, struct dentry, d_child);
    const struct dentry *db = list_entry(b, struct dentry, d_child);

    return strcmp(da->d_name.name, db->d_name.name);
}

/* 디렉토리 내 하위 엔트리를 이름순으로 정렬 */
list_sort(NULL, &parent->d_subdirs, dcache_name_cmp);

네트워크: FIB 규칙 우선순위 정렬

FIB(Forwarding Information Base) 규칙은 우선순위 값에 따라 정렬되어, 라우팅(Routing) 결정 시 순서대로 매칭됩니다.

/* net/core/fib_rules.c — FIB 규칙 우선순위 정렬 */
static int fib_rule_cmp(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    struct fib_rule *ra = list_entry(a, struct fib_rule, list);
    struct fib_rule *rb = list_entry(b, struct fib_rule, list);

    /* 낮은 pref 값 = 높은 우선순위 */
    if (ra->pref < rb->pref) return -1;
    if (ra->pref > rb->pref) return  1;
    return 0;
}

/* 규칙 추가 후 전체 리스트 재정렬
 * list_sort()의 안정성이 중요: 같은 우선순위의 규칙은
 * 추가된 순서를 유지해야 함 */
list_sort(NULL, &ops->rules_list, fib_rule_cmp);

보안: LSM 후크(Hook) 정렬

LSM(Linux Security Module) 프레임워크는 등록된 보안 모듈의 후크 함수를 우선순위에 따라 정렬합니다.

/* security/security.c — LSM 후크 정렬 (간략화) */

/* 보안 모듈 후크 우선순위 정렬
 * 우선순위가 높은 모듈의 후크가 먼저 호출됨 */
static int lsm_hook_cmp(const void *a, const void *b)
{
    const struct security_hook_list *ha = a;
    const struct security_hook_list *hb = b;

    /* 커널 내장 LSM이 외부 모듈보다 먼저 */
    if (ha->lsm->order < hb->lsm->order) return -1;
    if (ha->lsm->order > hb->lsm->order) return  1;
    return 0;
}

/* 호출 체인 초기화 시 정렬
 * SELinux → AppArmor → SMACK 순서 등을 보장 */
sort(hook_heads, nr_hooks,
     sizeof(struct security_hook_list),
     lsm_hook_cmp, NULL);

코드 분석

  • 소스 경로security/security.c, include/linux/lsm_hooks.h
  • 정렬 목적 — 보안 모듈의 후크 호출 순서를 결정합니다. 잘못된 순서는 보안 정책 우회로 이어질 수 있습니다.
  • 부팅 시 1회 — LSM 후크 정렬은 커널 부팅 초기화 과정에서 한 번만 수행됩니다. 런타임 오버헤드가 없습니다.
실전 패턴 요약: 커널의 정렬 사용 사례는 크게 세 가지로 분류됩니다. (1) 검색 최적화 — extent, ACPI 리소스 등을 정렬하여 이진 검색 가능하게 만듦, (2) 순서 보장(Ordering) — FIB 규칙, LSM 후크, readdir 등의 실행/출력 순서 결정, (3) 보안 강화 — slab freelist 셔플링으로 메모리 레이아웃 예측 차단.

정렬 안정성과 비교자 설계

정렬 안정성(stability)은 동일 키를 가진 원소들의 상대적 순서가 정렬 전후에 보존되는지를 결정합니다. 커널의 두 정렬 API는 이 특성이 다르며, 비교자 설계에 직접적인 영향을 줍니다.

list_sort()의 안정성 보장 메커니즘

list_sort()의 안정성은 merge() 함수의 단 하나의 비교 연산자에 의해 결정됩니다.

/* 안정성의 핵심: <= 비교 */
if (cmp(priv, a, b) <= 0) {
    /* a를 선택 → 동일 키에서 앞쪽 원소가 먼저 출력 */
    *tail = a;
    ...
}

/* 만약 < 만 사용하면 (불안정):
 * cmp == 0일 때 b가 선택될 수 있어 순서가 뒤집힘 */
안정성 보장의 비용: list_sort()에서 안정성을 보장하는 비용은 제로입니다. <=<의 차이는 CPU 명령어 1개(비교 결과 해석)뿐이며, 추가 메모리나 비교 횟수가 필요하지 않습니다.

sort()의 불안정성과 대응 방법

heapsort 기반의 sort()는 본질적으로 불안정합니다. extract-max 단계에서 동일 키 원소의 위치가 예측 불가능하게 재배치됩니다. 안정 정렬이 필요한 경우 다음 패턴을 사용합니다.

/* 안정 정렬이 필요할 때: 원래 인덱스를 2차 키로 사용 */
struct stable_entry {
    struct original_data data;
    unsigned int orig_index;  /* 원래 위치 기록 */
};

/* 정렬 전: 원래 인덱스 저장 */
for (int i = 0; i < n; i++)
    entries[i].orig_index = i;

/* 비교자: 1차 키 동일 시 원래 인덱스로 결정 */
static int stable_cmp(const void *a, const void *b)
{
    const struct stable_entry *sa = a, *sb = b;
    int r = primary_cmp(&sa->data, &sb->data);
    if (r) return r;
    /* 2차 키: 원래 인덱스 (작은 것이 앞으로) */
    if (sa->orig_index < sb->orig_index) return -1;
    return 1;
}

sort(entries, n, sizeof(struct stable_entry),
     stable_cmp, NULL);

엄격한 약순서(Strict Weak Ordering)

비교 함수는 반드시 엄격한 약순서(strict weak ordering) 관계를 만족해야 합니다. 이를 위반하면 정렬 결과가 정의되지 않거나 무한 루프에 빠질 수 있습니다.

속성정의위반 시 결과
비반사성(Irreflexivity)cmp(a, a) == 0자기 자신과 swap 반복 → 무한 루프
비대칭성(Asymmetry)cmp(a,b) > 0cmp(b,a) < 0순서가 비결정적 → 정렬 결과 불안정
추이성(Transitivity)a < bb < ca < c순환 관계 → 정렬 불가, 무한 루프
동치 추이성a ≡ bb ≡ ca ≡ c동치류(equivalence class)가 깨져 불안정

잘못된 비교자로 인한 커널 버그 사례

/* 버그 사례 1: 부동소수점 유사 비교 (커널에서는 드물지만 교훈적)
 * NaN 처리 없이 비교하면 추이성 위반 */
static int bad_float_cmp(const void *a, const void *b)
{
    /* 잘못됨: NaN과의 비교는 항상 false
     * NaN < x == false, NaN > x == false, NaN == x == false
     * → 추이성 위반! */
    return (*(float *)a > *(float *)b) - (*(float *)a < *(float *)b);
}

/* 버그 사례 2: 오버플로우로 인한 비대칭성 위반 */
static int overflow_cmp(const void *a, const void *b)
{
    /* 잘못됨: INT_MAX - INT_MIN = 오버플로우!
     * cmp(INT_MAX, INT_MIN) = 음수 (올바르지 않음)
     * → a > b인데 음수를 반환 → 비대칭성 위반 */
    return *(int *)a - *(int *)b;
}

/* 올바른 버전 */
static int correct_cmp(const void *a, const void *b)
{
    int va = *(const int *)a;
    int vb = *(const int *)b;
    /* 비교 연산으로 오버플로우 방지 */
    if (va < vb) return -1;
    if (va > vb) return  1;
    return 0;
}

코드 분석

  • 뺄셈 오버플로a - b 패턴은 INT_MAX - (-1) = INT_MIN으로 래핑됩니다. 커널에서 이 버그는 커밋 로그 검색으로 여러 건 확인할 수 있습니다.
  • 실제 영향 — 잘못된 비교자는 정렬 결과를 비결정적으로 만듭니다. 디버깅이 극히 어려운 간헐적 버그의 원인이 됩니다.
  • 커널 도구WARN_ON으로 정렬 후 순서를 검증하는 디버그 코드를 추가하면 비교자 버그를 조기에 발견할 수 있습니다.
비교자 위반 유형과 결과 strict weak ordering의 세 가지 속성 위반과 그로 인한 결과를 시각화 비교자 위반 유형과 결과 비반사성 위반 cmp(a, a) ≠ 0 예: cmp가 주소를 비교 결과: 무한 swap 루프 비대칭성 위반 sign(cmp(a,b)) ≠ -sign(cmp(b,a)) 예: 정수 오버플로 뺄셈 결과: 비결정적 순서 추이성 위반 a<b ∧ b<c ⇏ a<c 예: 순환 다중 키 결과: 정렬 불가, 크래시 올바른 비교자 설계 규칙 1. if/else 체인 사용: if (a < b) return -1; if (a > b) return 1; return 0; 2. 뺄셈 사용 금지: return a - b; → 오버플로 위험 3. const 파라미터 + 사이드 이펙트 없음 + 결정적(deterministic) 출력

벤치마크와 선택 가이드

커널의 두 정렬 API는 각각 최적의 사용 시나리오가 있습니다. 이 섹션에서는 데이터 크기, 캐시 효율, 안정성 요구사항에 따른 선택 기준을 제시합니다.

sort() vs list_sort() 선택 기준

조건추천 API이유
데이터가 배열에 존재sort()변환 비용 없음, 캐시 효율 최고
데이터가 list_head 리스트list_sort()변환 비용 없음, 포인터 재연결만
안정 정렬 필요list_sort()유일한 안정 정렬 API
인터럽트 컨텍스트sort()O(1) 스택, 슬립 없음 보장
원소 < 16개수동 삽입 정렬함수 호출 오버헤드 대비 이득
이미 거의 정렬됨list_sort()merge sort의 적응적 특성 활용
메모리 제약 극심둘 다 O(1)두 API 모두 추가 메모리 O(1)

데이터 크기별 성능 특성

정렬 API 성능 비교입니다 (x86-64, 커널 6.x, 1000회 평균).

nsort() [μs]list_sort() [μs]비율
100.30.50.6x
1005.27.80.7x
1,00072950.8x
10,0009201,2500.7x
100,00012,40018,7000.7x

sort()가 전반적으로 30~40% 빠릅니다 (캐시 효과). 단, 리스트에서 배열로의 변환 비용을 포함하면 list_sort()가 유리합니다.

캐시 효율 분석

sort()와 list_sort()의 성능 차이는 대부분 캐시 효율에서 비롯됩니다.

배열 정렬 vs 리스트 정렬 캐시 효율 배열의 연속 메모리 접근과 리스트의 포인터 체이싱 비교 캐시 효율: 배열 sort() vs 리스트 list_sort() sort() — 연속 메모리 접근 Cache Line 1 Cache Line 2 프리페치 효과적, 캐시 미스 최소 list_sort() — 포인터 체이싱 프리페치 비효과적, 캐시 미스 빈번 메모리 접근 패턴 비교 sort(): 순차 접근 → L1/L2 캐시 히트율 90%↑ → 하드웨어 프리페처 효과적 list_sort(): 랜덤 접근 → L1 캐시 히트율 60~70% → 프리페처 무효, TLB 미스 빈번 n이 L1 캐시(32~48KB)를 초과하면 sort()의 이점이 급격히 커짐 결론: 동일 데이터라면 sort()가 30~40% 빠름 (변환 비용 제외)

인라인 정렬(insertion sort) 임계값

매우 작은 배열(n < 16)에서는 sort() 호출의 함수 호출 오버헤드가 정렬 작업 자체보다 클 수 있습니다. 이 경우 인라인 삽입 정렬이 더 효율적입니다.

/* 소규모 배열 최적화: 인라인 삽입 정렬 */
static inline void small_sort(int *arr, int n)
{
    /* n < 16일 때 sort()보다 효율적
     * 이유: (1) 함수 호출 오버헤드 없음
     *       (2) 적은 원소에서 삽입 정렬의 O(n²)이
     *           heapsort의 O(n log n)보다 빠름
     *       (3) 거의 정렬된 입력에 적응적 */
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

/* 실전 사용 패턴 */
if (count < 16)
    small_sort(array, count);
else
    sort(array, count, sizeof(int), cmp_int, NULL);

코드 분석

  • 임계값 16 — 경험적으로 n < 16에서 삽입 정렬이 heapsort보다 빠릅니다. CPU 파이프라인, 분기 예측, 캐시 라인(Cache Line) 크기에 따라 최적값은 8~32 사이입니다.
  • 적응적 특성 — 이미 거의 정렬된 배열에서 삽입 정렬은 O(n)에 가깝습니다. heapsort는 입력 패턴과 무관하게 O(n log n)입니다.
  • 커널 내 사용 — 일부 드라이버와 파일시스템에서 이 패턴을 사용합니다. 커널 core API에는 포함되지 않으므로 필요 시 각 모듈에서 구현합니다.

선택 의사결정 트리

빠른 결정 규칙: (1) 데이터가 list_head 리스트에 있으면 → list_sort(), (2) 안정 정렬이 필요하면 → list_sort() (배열이어도 리스트 변환 고려), (3) 그 외 배열 데이터 → sort(), (4) 원소 16개 미만 → 인라인 삽입 정렬 고려, (5) 비교에 추가 컨텍스트 필요 → sort_r().

Sift-down 단계별 동작 시각화

커널의 lib/sort.c가 사용하는 최적화된 sift-down은 두 단계로 동작합니다. Phase 1에서 빈 자리를 리프까지 밀어내고, Phase 2에서 정확한 위치를 찾아 올라갑니다. 아래 다이어그램은 루트 노드 값 2를 7개 원소 max-heap에서 sift-down하는 전체 과정을 보여줍니다.

Sift-down 단계별 동작 시각화 max-heap에서 루트 노드를 sift-down하는 Phase 1(밀어내기)과 Phase 2(위치 탐색) 과정 Sift-down 최적화: Phase 1 (밀어내기) + Phase 2 (위치 탐색) 초기 상태: 루트에 작은 값 2가 위치 (extract-max 후) 2 9 7 5 8 3 1 Phase 1: 빈 자리 밀어내기 Step 1: 자식 비교 → 9 vs 7 → 9가 큼 swap(2, 9) → 9가 루트로 올라감 Step 2: 자식 비교 → 5 vs 8 → 8이 큼 swap(2, 8) → 8이 위로 올라감 Step 3: 리프 도달 → Phase 1 완료 비교 2회 (자식끼리만, 부모-자식 비교 없음) 값 2는 리프 위치에 도달 Phase 1 결과: 9 8 7 5 2 3 1 Phase 2: 올바른 위치 탐색 (sift-up) Step 1: parent(2) = 8 → cmp(8, 2) > 0 → 이미 만족 비교 1회만에 올바른 위치 확인! 총 비교: Phase1(2) + Phase2(1) = 3회 교과서 방식: 매 레벨 2회 × 2레벨 = 4회 이상 최적화 핵심: 자식끼리만 비교하며 빈 자리를 리프로 밀어낸 후, 역방향 1~2회 비교로 위치 확정 평균적으로 Phase 2에서 1~2회만 올라감 → 총 비교 횟수 약 25% 감소
sift-down 최적화의 수학적 근거: 교과서 sift-down은 매 레벨에서 부모-자식 비교 2회를 수행합니다 (두 자식 중 큰 것 선택 1회 + 부모와 비교 1회). 커널 최적화 버전은 Phase 1에서 자식끼리만 비교하므로 레벨당 1회이고, Phase 2의 sift-up은 대부분의 경우 1~2레벨만 올라갑니다. Floyd의 bottom-up heapsort 분석에 따르면, 평균 sift-up 거리는 O(1)이므로 총 비교 횟수가 n log n + O(n)에 수렴합니다.

list_sort() 병합 과정 시각화

lib/list_sort.c의 bottom-up merge sort는 입력 리스트의 노드를 하나씩 읽어 pending 스택에 누적하며, 비트 카운터 규칙에 따라 인접 서브리스트를 병합합니다. 아래 다이어그램은 6개 원소 리스트 [4, 2, 6, 1, 5, 3]의 전체 병합 과정을 단계별로 추적합니다.

list_sort() 병합 과정 상세 시각화 6개 원소 리스트가 bottom-up merge sort로 정렬되는 전 과정을 pending 스택 변화와 함께 표현 list_sort(): 6개 원소 병합 과정 전체 추적 단계 count 이진 입력에서 읽음 Pending 스택 (top → bottom) 동작 1 1 0b01 4 [4] 추가만 2 2 0b10 2 [2→4] 병합! [4]+[2]→[2,4] 3 3 0b11 6 [6] [2→4] 추가만 (비율 1:2 유지) 4 4 0b100 1 병합! [6]+[1]→[1,6] [1→6] [2→4] 연쇄 병합! [1,6]+[2,4] [1→2→4→6] 4개 원소 정렬 완료 5 5 0b101 5 [5] [1→2→4→6] 추가만 (비율 1:4) 6 6 0b110 3 병합! [5]+[3]→[3,5] [3→5] [1→2→4→6] 입력 소진, 최종 병합 시작 최종 병합: merge_final([3,5], [1,2,4,6]) → prev 포인터 동시 복원 결과: 1 ↔ 2 ↔ 3 ↔ 4 ↔ 5 ↔ 6 (이중 연결 순환 리스트) 총 비교 횟수: 8회 | 이론 하한 n⌈log₂n⌉ - 2^⌈log₂n⌉ + 1 = 7회 | 비율: 1.14x (거의 최적) 핵심: 2의 거듭제곱 count에서 연쇄 병합 → pending 스택이 자연스럽게 균형 트리 구조 유지 merge_final()은 마지막 두 서브리스트를 병합하면서 prev 포인터를 동시에 복원합니다

swap_func 최적화 결정 트리

lib/sort.c의 성능을 결정하는 핵심 요소 중 하나는 swap 전략입니다. 호출자가 swap_func = NULL을 전달하면, 커널은 요소의 메모리 정렬(alignment)과 크기에 따라 최적의 swap 함수를 자동 선택합니다. 이 과정은 sort_r() 진입 시 한 번만 수행되어 루프 내 분기 오버헤드가 없습니다.

swap_func 최적화 결정 트리 호출자의 swap 함수 지정 여부와 메모리 정렬에 따른 3단계 swap 전략 선택 과정 swap_func 자동 선택 결정 트리 swap_func == NULL? No Custom Swap 호출자 제공 함수 Yes is_aligned(base, size, 8)? Yes SWAP_WORDS_64 u64 단위 swap (8B) 반복: size/8회 No is_aligned(base, size, 4)? Yes SWAP_WORDS_32 u32 단위 swap (4B) 반복: size/4회 No SWAP_BYTES char 단위 swap (1B) 반복: size회 (최느림) swap 전략별 성능 비교 (64B 구조체 기준) SWAP_WORDS_64: 8회 반복 × u64 load/store = 16 메모리 연산 SWAP_WORDS_32: 16회 반복 × u32 load/store = 32 메모리 연산 SWAP_BYTES: 64회 반복 × char load/store = 128 메모리 연산 (8x 느림)
/* lib/sort.c — is_aligned 매크로 (간략화) */
static inline bool is_aligned(const void *base,
                               size_t size, size_t align)
{
    /* base 주소와 size 모두 align의 배수인지 확인
     * 비트 AND로 O(1) 판정 — 나눗셈 없음 */
    return !((unsigned long)base | size) & (align - 1);
}

/* 커스텀 swap이 필요한 대표적 경우 */
struct hlist_node_entry {
    struct hlist_node node;  /* pprev 포인터가 자기참조 */
    u64 key;
    char data[48];
};

/* hlist_node의 pprev는 자기 자신의 주소를 참조하므로
 * 단순 메모리 복사 후 pprev를 수정해야 합니다 */
static void swap_hlist_entry(void *a, void *b, size_t size)
{
    struct hlist_node_entry tmp;
    memcpy(&tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, &tmp, size);
    /* pprev 포인터 복원 */
    if (((struct hlist_node_entry *)a)->node.pprev)
        *((struct hlist_node_entry *)a)->node.pprev =
            &((struct hlist_node_entry *)a)->node;
    if (((struct hlist_node_entry *)b)->node.pprev)
        *((struct hlist_node_entry *)b)->node.pprev =
            &((struct hlist_node_entry *)b)->node;
}
swap 전략 최적화 팁: 구조체를 sort()로 정렬할 때, sizeof(struct)가 8의 배수가 되도록 __attribute__((aligned(8)))이나 명시적 패딩을 추가하면 SWAP_WORDS_64가 적용되어 성능이 향상됩니다. 대부분의 커널 구조체는 자연스럽게 8바이트 정렬되지만, char 배열이 포함된 구조체에서는 주의가 필요합니다.

실전 구현 예제

커널 모듈(Kernel Module)에서 정렬 API를 실제로 활용하는 패턴을 구체적인 예제와 함께 설명합니다.

예제 1: 구조체 배열 정렬 — 디바이스 우선순위

디바이스 드라이버에서 여러 디바이스를 우선순위 기준으로 정렬하는 실전 패턴입니다. sort()를 사용하고, 커스텀 비교자로 다중 키 정렬을 수행합니다.

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

/* 디바이스 초기화 순서 정렬 예제 */
struct device_init_entry {
    const char *name;
    int priority;          /* 낮을수록 먼저 초기화 */
    int dependency_depth;  /* 의존성 깊이 (2차 키) */
    void (*init_fn)(void);
};

/* 다중 키 비교자: priority 오름차순 → dependency_depth 오름차순 */
static int cmp_device_init(const void *a, const void *b)
{
    const struct device_init_entry *da = a;
    const struct device_init_entry *db = b;

    /* 1차 키: priority (오름차순) */
    if (da->priority < db->priority)
        return -1;
    if (da->priority > db->priority)
        return 1;

    /* 2차 키: dependency_depth (오름차순)
     * 같은 priority이면 의존성이 적은 것부터 */
    if (da->dependency_depth < db->dependency_depth)
        return -1;
    if (da->dependency_depth > db->dependency_depth)
        return 1;

    return 0;
}

/* 사용 예시 */
static void init_devices_sorted(struct device_init_entry *devs,
                                 int nr_devs)
{
    /* 우선순위 기준 정렬 — swap=NULL로 자동 최적화 */
    sort(devs, nr_devs,
         sizeof(struct device_init_entry),
         cmp_device_init, NULL);

    /* 정렬된 순서대로 디바이스 초기화 */
    for (int i = 0; i < nr_devs; i++) {
        pr_info("init: %s (prio=%d, depth=%d)\n",
                devs[i].name,
                devs[i].priority,
                devs[i].dependency_depth);
        devs[i].init_fn();
    }
}

예제 2: list_sort()를 사용한 우선순위 재정렬

작업 큐(Workqueue)(Work Queue)에서 대기 중인 작업들을 우선순위와 마감 시간에 따라 재정렬하는 패턴입니다. list_sort()의 안정성을 활용하여 동일 우선순위 작업의 삽입 순서를 보존합니다.

#include <linux/list_sort.h>
#include <linux/list.h>

struct work_item {
    struct list_head list;
    int priority;        /* 0=최고, 255=최저 */
    ktime_t deadline;    /* 마감 시간 */
    void (*work_fn)(struct work_item *);
    char desc[32];
};

/* 비교자: priority 오름차순 → deadline 오름차순
 * list_sort()가 안정 정렬이므로, 같은 priority+deadline의
 * 작업은 리스트에 추가된 순서를 유지합니다 */
static int cmp_work_item(void *priv,
                          const struct list_head *a,
                          const struct list_head *b)
{
    const struct work_item *wa =
        list_entry(a, struct work_item, list);
    const struct work_item *wb =
        list_entry(b, struct work_item, list);

    /* 1차 키: priority (낮은 값 = 높은 우선순위) */
    if (wa->priority != wb->priority)
        return wa->priority - wb->priority;

    /* 2차 키: deadline (이른 마감이 먼저) */
    if (ktime_before(wa->deadline, wb->deadline))
        return -1;
    if (ktime_after(wa->deadline, wb->deadline))
        return 1;

    /* 동일 priority+deadline → 안정 정렬로 삽입 순서 유지 */
    return 0;
}

/* 큐 재정렬 함수 */
static void reorder_work_queue(struct list_head *queue,
                                spinlock_t *lock)
{
    spin_lock(lock);
    /* list_sort()는 내부적으로 락을 잡지 않으므로
     * 호출자가 동기화를 보장해야 합니다 */
    list_sort(NULL, queue, cmp_work_item);
    spin_unlock(lock);
}

/* 사용 예시: 높은 우선순위 작업 삽입 후 재정렬 */
static void submit_urgent_work(struct list_head *queue,
                                struct work_item *item,
                                spinlock_t *lock)
{
    spin_lock(lock);
    list_add_tail(&item->list, queue);
    spin_unlock(lock);

    /* 긴급 작업이 추가되었으므로 큐 재정렬
     * 안정 정렬이므로 기존 작업의 상대적 순서는 유지됨 */
    reorder_work_queue(queue, lock);
}
재정렬 빈도 주의: 매번 작업 추가 시 list_sort()를 호출하면 O(n log n) 비용이 반복됩니다. 작업 추가가 빈번하면, 삽입 정렬(적절한 위치에 삽입)이 O(n)으로 더 효율적입니다. list_sort()는 일괄(batch) 추가 후 한 번 정렬하거나, 대규모 재정렬이 필요할 때 적합합니다.

예제 3: sort_r()로 컨텍스트 기반 정렬

sort_r()priv 파라미터를 활용하여, 동일한 배열을 다른 기준으로 정렬하는 패턴입니다. 전역 변수 없이 정렬 기준을 동적으로 변경할 수 있습니다.

#include <linux/sort.h>

struct event_record {
    u64 timestamp;
    u32 cpu;
    u32 event_type;
    u32 data;
};

/* 정렬 컨텍스트: 어떤 필드를 기준으로 정렬할지 결정 */
enum sort_key {
    SORT_BY_TIME,
    SORT_BY_CPU,
    SORT_BY_TYPE,
};

struct event_sort_ctx {
    enum sort_key key;
    bool descending;
};

/* 범용 비교자: priv로 전달된 컨텍스트에 따라 정렬 기준 변경 */
static int cmp_event(const void *a, const void *b,
                      const void *priv)
{
    const struct event_record *ea = a;
    const struct event_record *eb = b;
    const struct event_sort_ctx *ctx = priv;
    int result;

    switch (ctx->key) {
    case SORT_BY_TIME:
        if (ea->timestamp < eb->timestamp)
            result = -1;
        else if (ea->timestamp > eb->timestamp)
            result = 1;
        else
            result = 0;
        break;
    case SORT_BY_CPU:
        result = (int)ea->cpu - (int)eb->cpu;
        break;
    case SORT_BY_TYPE:
        result = (int)ea->event_type - (int)eb->event_type;
        break;
    default:
        result = 0;
    }

    return ctx->descending ? -result : result;
}

/* 사용: 같은 배열을 다른 기준으로 정렬 */
static void analyze_events(struct event_record *events,
                           int nr_events)
{
    struct event_sort_ctx ctx;

    /* 시간순 정렬 → 시계열 분석 */
    ctx = (struct event_sort_ctx){
        .key = SORT_BY_TIME, .descending = false
    };
    sort_r(events, nr_events,
           sizeof(struct event_record),
           cmp_event, NULL, &ctx);
    print_timeline(events, nr_events);

    /* CPU별 정렬 → per-CPU 분석 */
    ctx = (struct event_sort_ctx){
        .key = SORT_BY_CPU, .descending = false
    };
    sort_r(events, nr_events,
           sizeof(struct event_record),
           cmp_event, NULL, &ctx);
    print_per_cpu_summary(events, nr_events);
}

예제 4: 성능 비교 시나리오

동일한 데이터를 sort()(배열)와 list_sort()(리스트)로 정렬할 때의 성능 차이를 측정하는 벤치마크 패턴입니다.

#include <linux/sort.h>
#include <linux/list_sort.h>
#include <linux/ktime.h>

#define BENCH_SIZE  10000

struct bench_elem {
    u64 key;
    char payload[56];       /* 총 64B — 캐시라인 크기 */
    struct list_head list;  /* list_sort용 */
};

static int cmp_bench_array(const void *a, const void *b)
{
    const struct bench_elem *ea = a;
    const struct bench_elem *eb = b;
    if (ea->key < eb->key) return -1;
    if (ea->key > eb->key) return  1;
    return 0;
}

static int cmp_bench_list(void *priv,
    const struct list_head *a,
    const struct list_head *b)
{
    const struct bench_elem *ea =
        list_entry(a, struct bench_elem, list);
    const struct bench_elem *eb =
        list_entry(b, struct bench_elem, list);
    if (ea->key < eb->key) return -1;
    if (ea->key > eb->key) return  1;
    return 0;
}

static void run_sort_benchmark(void)
{
    struct bench_elem *arr;
    LIST_HEAD(bench_list);
    ktime_t t1, t2;
    s64 arr_ns, list_ns;

    arr = kvmalloc_array(BENCH_SIZE,
                          sizeof(struct bench_elem),
                          GFP_KERNEL);

    /* 랜덤 데이터 생성 */
    for (int i = 0; i < BENCH_SIZE; i++) {
        arr[i].key = get_random_u64();
        list_add_tail(&arr[i].list, &bench_list);
    }

    /* 배열 정렬 벤치마크 */
    t1 = ktime_get();
    sort(arr, BENCH_SIZE,
         sizeof(struct bench_elem),
         cmp_bench_array, NULL);
    t2 = ktime_get();
    arr_ns = ktime_to_ns(ktime_sub(t2, t1));

    /* 리스트 재구축 후 리스트 정렬 벤치마크 */
    INIT_LIST_HEAD(&bench_list);
    for (int i = 0; i < BENCH_SIZE; i++)
        list_add_tail(&arr[i].list, &bench_list);

    t1 = ktime_get();
    list_sort(NULL, &bench_list, cmp_bench_list);
    t2 = ktime_get();
    list_ns = ktime_to_ns(ktime_sub(t2, t1));

    pr_info("sort benchmark: n=%d\n", BENCH_SIZE);
    pr_info("  sort():      %lld ns\n", arr_ns);
    pr_info("  list_sort(): %lld ns\n", list_ns);
    pr_info("  ratio: sort()/list_sort() = %lld%%\n",
            arr_ns * 100 / list_ns);

    kvfree(arr);
}
벤치마크 결과 해석: 64바이트 구조체 10,000개 기준으로, sort()는 약 900μs, list_sort()는 약 1,250μs가 소요됩니다 (x86-64, CONFIG_DEBUG 비활성). sort()가 약 30% 빠른 이유는 배열의 연속 메모리 접근이 캐시 프리페치에 유리하기 때문입니다. 그러나 데이터가 이미 리스트에 있다면, 배열로 복사하는 O(n) 비용을 포함하면 list_sort()가 더 효율적입니다.

예제 5: 정렬된 리스트에 단일 원소 삽입

이미 정렬된 리스트에 새 원소를 하나 추가할 때는 list_sort() 대신 O(n) 삽입 정렬이 더 효율적입니다.

/* 정렬된 리스트에 단일 원소 삽입 — O(n) */
static void sorted_list_insert(struct list_head *head,
                                struct work_item *new_item,
                                list_cmp_func_t cmp)
{
    struct list_head *pos;

    /* 삽입 위치 탐색: cmp(new, pos) <= 0인 첫 위치 */
    list_for_each(pos, head) {
        if (cmp(NULL, &new_item->list, pos) <= 0)
            break;
    }
    /* pos 앞에 삽입 → 정렬 순서 유지 */
    list_add_tail(&new_item->list, pos);
}

/* 선택 기준:
 * - 단일 삽입 → sorted_list_insert()   O(n)
 * - 다수 삽입 후 정렬 → list_sort()     O(n log n)
 * - 임계점: ~log₂(n)개 이상 삽입 시 list_sort()가 유리
 *   (n=1000이면 ~10개 이상 동시 삽입 시 list_sort 선택) */
삽입 정렬 vs 일괄 정렬: m개의 새 원소를 n개 리스트에 추가할 때: (1) 삽입 정렬: O(m × n) — m이 작으면 유리, (2) 일괄 추가 후 list_sort(): O((n+m) log(n+m)) — m이 클수록 유리. 교차점은 대략 m > log₂(n)입니다. n=1000이면 m > 10일 때 list_sort()가 더 빠릅니다.

lib/sort.c는 2024~2026년 구간에 6.12의 *_nonatomic 변종 도입을 제외하면 거의 변경이 없습니다. __sort_rmay_schedule 플래그가 추가되어 장시간 정렬 도중 cond_resched()가 허용되며, 인접한 min_heap 유틸리티 쪽이 더 활발하게 확장되었습니다. 6.18, 6.19에서도 lib/sort.c 알고리즘 본체는 안정 상태를 유지하고 있습니다.

커널릴리스변경 사항실무 시사점
6.12 (LTS)2024-11sort_nonatomic()/sort_r_nonatomic() 도입 — __sort_rmay_schedule 플래그를 추가해 장시간 정렬 중 cond_resched() 허용대용량 배열 정렬 시 CONFIG_PREEMPT 비활성 커널에서 스케줄 지연 완화. atomic 컨텍스트에서는 기존 sort()/sort_r() 사용
6.132025-01Min heap API 확장(비인라인 함수·최적화 6커밋) — sort.c와는 별개이지만 정렬 유틸리티 계열<linux/min_heap.h> 소비자(BPF, perf, RCU)가 더 효율적으로 동작
6.142025-03메인 API 안정 — 주변 확장만
6.152025-05메인 API 안정 — 주변 확장만
6.162025-07메인 API 안정 — 주변 확장만
6.172025-09메인 API 안정 — 주변 확장만
6.18 (LTS)2025-11메인 API 안정 — 주변 확장만. Rust 커널 인프라 확장(비트맵 API, scatter-gather 추상화)이 진행되었으나 lib/sort.c/lib/list_sort.c 본체 변경 없음LTS 커널이며 현재 kernel.org 공개 기준 projected EOL은 2028-12입니다. sort_nonatomic() 계열 도입은 6.12에서 완료되어 현재는 안정 운용 단계입니다.
6.192026-02메인 API 안정 — 주변 확장만. free_pcppages_bulk 배치 처리 개선이 메모리 할당 경로에 영향을 주나 정렬 라이브러리 자체는 변경 없음
핵심 요약: (1) 수천~수만 원소 규모 배열을 잠금 없이 정렬할 수 있다면 6.12+ sort_nonatomic()/sort_r_nonatomic()을 사용해 스케줄 지연을 줄이세요. (2) 반드시 atomic 컨텍스트(스핀락(Spinlock) 보유, IRQ 비활성)에서 정렬해야 한다면 기존 sort()/sort_r()을 유지해야 합니다 — *_nonatomic 변종은 sleeping 경로를 포함합니다. (3) 리스트(list_head)를 정렬한다면 여전히 list_sort()가 기본 선택이며, 우선순위 큐 형태가 필요하면 min_heap이 더 적합합니다. (4) 6.18, 6.19에서도 API 변경은 없으므로 기존 sort()/list_sort() 호출 코드는 수정 없이 사용 가능합니다.

요약 비교 테이블

특성sort()sort_r()list_sort()
헤더linux/sort.hlinux/sort.hlinux/list_sort.h
대상배열배열리스트
알고리즘HeapsortHeapsortMerge sort
비교자 타입cmp_func_tcmp_r_func_tlist_cmp_func_t
컨텍스트없음priv 포인터priv 포인터
안정성불안정불안정안정
추가 메모리O(1)O(1)O(1)
swap 커스텀가능가능해당 없음
최악 시간O(n log n)O(n log n)O(n log n)
반환값voidvoidvoid

Heapsort 단계별 시각화

커널의 lib/sort.c가 사용하는 bottom-up heapsort는 두 단계로 구성됩니다. 먼저 입력 배열을 max-heap으로 변환(heapify)한 뒤, 루트(최대값)를 반복 추출하여 정렬된 영역을 확장합니다. 아래 다이어그램은 8개 원소 [5, 3, 8, 1, 7, 2, 6, 4]에 대한 전체 과정을 단계별로 보여줍니다.

Heapsort 8원소 단계별 시각화 8개 원소 배열의 heapify 과정과 extract-max 반복을 트리와 배열로 동시에 표현 Phase 1: Build Max-Heap (heapify) 초기: 5 3 8 1 7 2 6 4 sift(3): i=3 → 원소 1과 자식 4 비교 → swap(1,4) sift(2): i=2 → 원소 8 ≥ 자식(2,6) → 유지 sift(1): i=1 → 원소 3 < 자식 7 → swap(3,7) sift(0): i=0 → 원소 5 < 자식 8 → swap(5,8), 계속 sift → swap(5,6) Max-Heap 완성 8 7 6 4 5 2 3 1 배열: 8 7 6 4 5 2 3 1 Phase 2: Extract-Max 반복 단계 swap sift-down 후 배열 1 swap(8,1) [7,5,6,4,1,2,3] | 8 2 swap(7,3) [6,5,3,4,1,2] | 7,8 3 swap(6,2) [5,4,3,2,1] | 6,7,8 4 swap(5,1) [4,1,3,2] | 5,6,7,8 5 swap(4,2) [3,1,2] | 4,5,6,7,8 6 swap(3,2) [2,1] | 3,4,5,6,7,8 7 swap(2,1) [1] | 2,3,4,5,6,7,8 정렬 완료 1 2 3 4 5 6 7 8 Bottom-up vs Top-down 비교 횟수 Top-down heapsort: ~2n log₂n 비교 Bottom-up (커널): ~1.5n log₂n 비교 → 25% 비교 횟수 감소 (George Spelvin 패치, 2019) Bottom-up Sift-down 상세 1. 리프까지 최대 자식 경로 추적 (비교 log n회) 2. 경로 끝에서 삽입 위치를 역추적 (비교 ~log log n회) 3. 요소를 올바른 위치에 삽입 대부분의 요소가 리프 근처에 위치 → 역추적 거리가 짧음 평균 비교: n log₂n + O(n) ≈ 이론적 하한에 근접 Heap 불변식 (Invariant) ∀ i ∈ [0, n): A[parent(i)] ≥ A[i] → heapify 후, 모든 extract-max 직전에 성립 총 swap 횟수: O(n log n) 총 메모리: O(1) — in-place 정렬

heapify 과정의 커널 구현

커널의 heapify는 배열의 마지막 비-리프(non-leaf) 노드부터 인덱스 0까지 역순으로 sift-down을 수행합니다. n/2 - 1부터 시작하므로 리프 노드(전체의 약 절반)는 건너뜁니다.

/* lib/sort.c — heapify: O(n) 시간에 max-heap 구축 */
for (size_t i = num / 2 - 1; i >= 0; i--) {
    /* bottom-up sift: 리프까지 최대 자식 추적 후 역추적 */
    size_t child;
    for (child = 2 * i + 1; child + 1 < num; child = 2 * child + 1) {
        if (cmp_func(base + child * size, base + (child + 1) * size, priv) < 0)
            child++;  /* 오른쪽 자식이 더 크면 선택 */
    }
    /* child가 리프 → 역추적하며 삽입 위치 탐색 */
    while (child > i && cmp_func(base + i * size, base + child * size, priv) > 0)
        child = (child - 1) / 2;
    /* 실제 이동: child 위치에 원소 배치 후 경로상 shift */
}

코드 분석

  • 루프 범위 n/2-1 → 0 — 리프가 아닌 노드만 순회합니다. 리프 노드는 자식이 없으므로 sift-down이 불필요합니다.
  • Bottom-up 경로 추적 — 먼저 리프까지 최대 자식 방향으로 내려가면서 비교만 수행합니다. 이 단계에서 swap은 발생하지 않습니다.
  • 역추적(Backtrace)(backtrack) — 리프에서 올라오며 삽입 위치를 찾습니다. 대부분의 원소가 리프 근처에 배치되므로 역추적 거리가 매우 짧습니다.
  • O(n) 복잡도 — 높이 h인 노드의 sift-down 비용은 O(h)이고, 높이별 노드 수가 기하급수적으로 감소하므로 총 비용은 O(n)입니다.

extract-max 루프의 커널 구현

heapify 완료 후, 루트(최대값)와 마지막 원소를 swap하고 힙 크기를 1 줄인 뒤 sift-down을 반복합니다. 이 과정이 n-1번 반복되어 오름차순 정렬이 완성됩니다.

/* lib/sort.c — extract-max 루프: O(n log n) */
for (size_t i = num - 1; i > 0; i--) {
    /* 루트(최대)와 마지막 미정렬 원소 교환 */
    do_swap(base, base + i * size, size);

    /* 힙 크기를 i로 줄이고 루트에서 sift-down */
    size_t child, root = 0;
    for (child = 1; child < i; ) {
        if (child + 1 < i &&
            cmp_func(base + child * size, base + (child + 1) * size, priv) < 0)
            child++;
        child = 2 * child + 1;  /* bottom-up: 리프까지 추적 */
    }
    /* 역추적 후 원소 이동 */
}

코드 분석

  • do_swap(base, base + i * size, size) — 루트(인덱스 0)와 인덱스 i를 교환합니다. swap 함수는 heapify 전에 한 번 결정되어 캐싱된 함수 포인터입니다.
  • 힙 크기 축소 — 루프 변수 i가 현재 힙 크기 역할을 합니다. 교환된 최대값은 인덱스 i 이후의 정렬 영역에 고정됩니다.
  • 정렬 방향 — max-heap + extract를 조합하면 자연스럽게 오름차순이 됩니다. 내림차순이 필요하면 비교자를 반전합니다.

list_sort 병합 시각화

lib/list_sort.c의 bottom-up 병합 정렬은 timsort와 유사한 pending 리스트 방식을 사용합니다. 입력 리스트를 하나씩 떼어내며 pending 스택에 push하고, 인접 서브리스트의 크기 비율이 2:1을 넘으면 병합합니다. 이 섹션에서는 6개 노드 리스트 [D→B→F→A→E→C]의 병합 과정을 추적합니다.

list_sort bottom-up 병합 과정 pending 리스트에 서브리스트가 누적되고 2:1 비율 규칙에 따라 병합되는 과정 list_sort() Bottom-up 병합: pending 리스트 변화 입력: D B F A E C Step 1 D를 pending에 push pending: [D(1)] count=1 (이진: 1) → 병합 없음 Step 2 B를 push → 크기 1+1 = 2:1 초과 → merge pending: [B→D(2)] merge(D, B) → B→D (정렬됨, 크기 2) count=2 (이진: 10) → bit0=0 병합 Step 3 F를 push pending: [F(1), B→D(2)] count=3 (이진: 11) → 병합 없음 Step 4 A를 push → 크기 1+1 병합, 2+2 병합 pending: [A→B→D→F(4)] merge(F, A) → A→F (크기 2) merge(B→D, A→F) → A→B→D→F (크기 4) count=4 (이진: 100) Step 5 E를 push pending: [E(1), A→B→D→F(4)] count=5 (이진: 101) → 병합 없음 Step 6 C를 push → 크기 1+1 병합 pending: [C→E(2), A→B→D→F(4)] merge(E, C) → C→E (크기 2) count=6 (이진: 110) 최종 병합 입력 소진 → pending 스택 전체를 순서대로 병합 merge(A→B→D→F(4), C→E(2)) → A→B→C→D→E→F 결과: A B C D E F 병합 트리거 규칙: count의 이진 표현 count의 하위 비트가 1이면 → 병합하지 않음 count의 하위 비트가 0이면 → 인접 서브리스트 병합 연속된 0 비트 수 = 연쇄 병합 횟수 count=4(100₂): 하위 0이 2개 → 2회 연쇄 병합 count=6(110₂): 하위 0이 1개 → 1회 병합 2:1 비율이 보장하는 것 • pending 리스트 길이 ≤ O(log n) • 총 비교 횟수: n log₂n - n + 1 (최적) • 추가 메모리: O(1) — 리스트 포인터 재활용 • 안정(stable) 정렬 보장

pending 리스트의 내부 표현

커널 구현에서 pending 리스트는 struct list_headprev 포인터만 사용하여 단방향 연결 리스트를 구성합니다. 이 기법은 정렬 중 next 포인터가 불필요하므로 메모리 쓰기를 절반으로 줄입니다.

/* lib/list_sort.c — pending 리스트 관리 */
struct list_head *pending = NULL;
int count = 0;

list_for_each_safe(cur, tmp, head) {
    /* 현재 노드를 분리하여 크기 1인 서브리스트로 만듦 */
    cur->prev = pending;      /* pending 스택에 push */
    pending = cur;
    count++;

    /* count의 하위 비트를 검사하여 병합 결정 */
    for (int bits = count; !(bits & 1); bits >>= 1) {
        /* pending 스택의 상위 두 서브리스트를 병합 */
        pending = merge(priv, cmp, pending->prev, pending);
    }
}
/* 입력 소진 후 남은 pending을 모두 병합 */
merge_final(priv, cmp, head, pending);

코드 분석

  • cur->prev = pendingprev 포인터를 pending 스택의 next 포인터로 재활용합니다. 정렬 완료 후 merge_final에서 prevnext를 모두 복원합니다.
  • !(bits & 1) — count의 최하위 비트가 0인 동안 반복합니다. 이것이 2:1 비율 병합의 핵심 트리거입니다. count=4(100₂)이면 2번 연쇄 병합합니다.
  • merge() — 두 정렬된 서브리스트를 하나로 합칩니다. 동일 키에 대해 왼쪽 원소를 우선하여 안정성을 보장합니다.
  • merge_final() — 마지막 병합에서 prev/next 양방향 포인터를 모두 복원하고 원형 리스트로 연결합니다.

merge_final의 리스트 복원

merge_final()은 마지막 병합과 동시에 단방향 pending 체인을 완전한 이중 원형 리스트(list_head)로 복원합니다. 이 과정에서 별도 메모리 할당 없이 기존 노드의 포인터만 재연결합니다.

/* lib/list_sort.c — merge_final: 양방향 원형 리스트 복원 */
static void merge_final(void *priv, list_cmp_func_t cmp,
                         struct list_head *head,
                         struct list_head *a,
                         struct list_head *b)
{
    struct list_head *tail = head;

    while (a && b) {
        if (cmp(priv, a, b) <= 0) {  /* <= 로 안정성 보장 */
            tail->next = a;
            a->prev = tail;
            a = a->next;
        } else {
            tail->next = b;
            b->prev = tail;
            b = b->next;
        }
        tail = tail->next;
    }
    /* 나머지 연결 및 원형 리스트 완성 */
    tail->next = a ? a : b;
    /* ... prev 포인터 복원 + head->prev = tail 설정 ... */
}

코드 분석

  • cmp(priv, a, b) <= 0 — 같은 키를 가진 원소에서 왼쪽(먼저 나온) 원소를 우선합니다. 이 <=가 안정 정렬의 핵심입니다.
  • 양방향 복원 — 병합하면서 동시에 prev 포인터를 설정하므로, 별도의 복원 패스가 필요 없습니다.

swap 최적화 심층

lib/sort.c의 swap 서브시스템은 원소 크기와 메모리 정렬에 따라 3가지 전략을 자동 선택합니다. 이 최적화는 캐시 라인 활용률을 극대화하고 불필요한 바이트 복사를 최소화합니다. 특히 64비트 시스템에서 SWAP_WORDS_64는 8바이트 단위로 교환하여 32바이트 구조체도 4회 반복으로 완료합니다.

SWAP_WORDS_64/32/BYTES 동작 비교 swap 전략별 메모리 접근 패턴과 반복 횟수를 시각적으로 비교 Swap 전략별 메모리 접근 패턴 (32바이트 원소 기준) SWAP_WORDS_64 조건: base & size 모두 8B 정렬 A: u64 [0] u64 [1] u64 [2] u64 [3] B: u64 [0] u64 [1] u64 [2] u64 [3] 4회 반복 (32B / 8B) 레지스터 1개로 교환 SWAP_WORDS_32 조건: base & size 모두 4B 정렬 (8B 미달) A: u32 u32 u32 u32 u32 u32 u32 u32 8회 반복 (32B / 4B) 32비트 아키텍처 최적 SWAP_BYTES 조건: 정렬 요건 미충족 (fallback) A: ... (32 bytes) ... 32회 반복 (32B / 1B) 임의 정렬 구조체 대응 swap 전략별 상대 성능 (32바이트 원소, 1000개 배열) SWAP_WORDS_64: 1.0x (기준) SWAP_WORDS_32: 1.7x 느림 SWAP_BYTES: 3.8x 느림 커스텀 swap: 0.85x (인라인 가능 시) is_aligned(base, size, align) 매크로 ((uintptr_t)base | size) & (align - 1) == 0 → base 주소와 size가 모두 align의 배수인지 단일 OR+AND로 검사

SWAP_WORDS_64 내부 구현

SWAP_WORDS_64는 8바이트 단위로 메모리를 교환합니다. 임시 변수 하나(u64 t)만 사용하므로 레지스터(Register)에 완전히 들어갑니다. 컴파일러는 이 패턴을 인식하여 xchg 명령어나 SIMD 교환으로 최적화할 수 있습니다.

/* lib/sort.c — SWAP_WORDS_64 구현 */
static void swap_words_64(void *a, void *b, size_t size)
{
    u64 *ia = (u64 *)a;
    u64 *ib = (u64 *)b;

    do {
        u64 t = *ia;
        *ia++ = *ib;
        *ib++ = t;
    } while ((size -= 8) > 0);
}

코드 분석

  • 8바이트 단위 루프u64 포인터 캐스팅 후 증가 연산자로 순회합니다. 32바이트 구조체는 단 4회 반복으로 교환이 완료됩니다.
  • 레지스터 활용 — 임시 변수 t는 8바이트이므로 64비트 범용 레지스터 하나에 들어갑니다. 메모리 접근 최소화로 캐시 미스 영향이 적습니다.
  • 정렬 전제is_aligned()에서 이미 검증했으므로, 비정렬 접근(unaligned access) 페널티가 발생하지 않습니다.

SWAP_BYTES의 fallback 전략

정렬 요건을 충족하지 못하는 구조체에 대해 SWAP_BYTES가 사용됩니다. 1바이트 단위 교환은 느리지만, 패킹된(__packed) 구조체나 가변 크기 원소를 안전하게 처리합니다.

/* lib/sort.c — SWAP_BYTES 구현 (fallback) */
static void swap_bytes(void *a, void *b, size_t size)
{
    u8 *ia = (u8 *)a;
    u8 *ib = (u8 *)b;

    do {
        u8 t = *ia;
        *ia++ = *ib;
        *ib++ = t;
    } while (--size > 0);
}

코드 분석

  • 바이트 단위 — 정렬 제약 없이 모든 메모리 레이아웃에서 동작합니다. 32바이트 원소에 32회 반복이 필요합니다.
  • 사용 사례__packed 구조체, 홀수 크기 원소, 또는 비정렬 메모리 풀에서 할당된 배열에서 자동 선택됩니다.

커널 정렬 호출자 맵

Linux 커널에서 sort(), sort_r(), list_sort()를 호출하는 서브시스템은 광범위합니다. 아래 다이어그램은 주요 호출자를 기능 영역별로 분류하고, 각 호출의 목적과 정렬 대상을 요약합니다.

커널 정렬 호출자 서브시스템 맵 sort, sort_r, list_sort를 호출하는 주요 커널 서브시스템을 기능 영역별로 분류한 맵 커널 정렬 API 호출자 맵 sort() sort_r() list_sort() 파일시스템 (Filesystem) ext4: extent 트리 정렬 (sort) xfs: 디렉터리 엔트리 정렬 (sort) btrfs: delayed ref 병합 (list_sort) nfs: readdir 정렬 (list_sort) f2fs: dirty segment 정렬 (sort) overlayfs: whiteout 정렬 (list_sort) fat: 디렉터리 슬롯 정렬 (sort) 메모리 관리 (MM) SLUB: freelist 셔플 정렬 (sort_r) compaction: 페이지 정렬 (list_sort) hugetlb: 페이지 풀 정렬 (list_sort) vmscan: LRU 배치 정렬 (sort) CMA: 영역 주소 정렬 (sort) memblock: 메모리 블록 정렬 (sort) 네트워크 (Network) netfilter: 규칙 체인 정렬 (list_sort) tc: 필터 우선순위 정렬 (list_sort) bridge: FDB 정렬 (sort) xfrm: policy 정렬 (list_sort) nf_conntrack: 버킷 정렬 (sort) 드라이버/디바이스 PCI: BAR 리소스 정렬 (sort) ACPI: 리소스 디스크립터 정렬 (sort) DRM/GPU: GEM 객체 정렬 (list_sort) USB: 엔드포인트 정렬 (sort) block: I/O 요청 병합 정렬 (list_sort) thermal: 쿨링 디바이스 정렬 (sort) 커널 코어 (Core) module: 심볼 테이블 정렬 (sort) kallsyms: 커널 심볼 정렬 (sort) ftrace: 함수 트레이스 정렬 (sort) perf: 이벤트 정렬 (list_sort) extable: 예외 테이블 정렬 (sort) kprobes: 브레이크포인트 정렬 (sort) 보안 (Security) SELinux: 접근 벡터 캐시 정렬 (sort) AppArmor: 규칙 정렬 (list_sort) audit: 감사 규칙 정렬 (list_sort) IMA: 해시 테이블 정렬 (sort) 호출 통계 (Linux 6.x 기준) sort(): ~120개 호출 sort_r(): ~15개 호출 list_sort(): ~80개 호출 sort() 선택 기준 • 배열 정렬, 추가 컨텍스트 불필요 • 원소 크기 고정, 안정성 불필요 • 가장 일반적인 선택 sort_r() 선택 기준 • 비교자에 priv 컨텍스트 필요 • SLUB freelist (random seed 전달) • 상태 의존적 비교 로직 list_sort() 선택 기준 • 연결 리스트 정렬 • 안정(stable) 정렬 필요 • 동적 크기 컬렉션

서브시스템별 정렬 특성

각 서브시스템이 정렬을 사용하는 목적은 크게 세 가지로 나뉩니다.

검색 최적화

kallsyms
이진 탐색을 위한 심볼 주소 정렬
extable
예외 테이블을 이진 탐색 O(log n)으로 검색합니다
module
모듈 심볼을 이름순 이진 탐색으로 검색합니다
ftrace
함수 주소를 이진 탐색으로 식별합니다

병합/중복 제거

ext4 extent
인접 extent 병합으로 디스크 단편화(Fragmentation)를 감소시킵니다
block I/O
인접 섹터 요청 병합으로 디스크 시크를 최소화합니다
compaction
연속 빈 페이지 병합으로 고차 할당 성공률을 향상시킵니다
btrfs
delayed ref 병합으로 트랜잭션(Transaction) 커밋을 최적화합니다

순서/우선순위 결정

netfilter
규칙 우선순위 순서를 보장합니다
PCI BAR
리소스 주소 순서로 충돌 없는 할당을 수행합니다
thermal
온도 임계값 순서로 올바른 쿨링 단계를 결정합니다

코드 분석

  • 검색 최적화 — 부팅 시 한 번 정렬하여 런타임에 O(log n) 이진 탐색을 활용합니다. extable은 페이지 폴트(Page Fault) 핸들러(Handler)에서 매우 빈번하게 검색되므로 정렬이 필수적입니다.
  • 병합/중복 제거 — 정렬 후 인접 원소를 선형 스캔하여 O(n) 시간에 병합합니다. 파일시스템 extent, 블록 I/O 요청, 메모리 compaction에서 공통적으로 사용되는 패턴입니다.
  • 순서/우선순위 — 정책이나 리소스의 올바른 처리 순서를 보장합니다. netfilter 규칙은 우선순위가 낮은 것부터 평가되어야 합니다.

정렬 진화사

Linux 커널의 정렬 구현은 2002년 첫 도입 이후 여러 차례의 최적화와 재설계를 거쳤습니다. 아래 타임라인은 주요 변경점과 그 배경을 정리합니다.

Linux 커널 정렬 구현 진화 타임라인 2002년부터 현재까지 sort(), list_sort()의 주요 변경 이력을 시간순으로 표현 Linux 커널 정렬 구현 진화 타임라인 2002 — Linux 2.5 lib/sort.c 최초 도입 (Matt Mackall) Classic top-down heapsort. Quicksort 대체: 최악 O(n²) 방지, 커널 스택 O(1) 보장. qsort()→sort() 네이밍. 2006 — Linux 2.6.17 남은 Quicksort 사용처 완전 제거 XFS 등에서 사용하던 자체 qsort 구현도 sort()로 통합. 2009 — Linux 2.6.32 lib/list_sort.c 도입 (Don Mullis) Bottom-up merge sort for list_head. 안정 정렬, O(1) 추가 메모리. NFS readdir, ext3 extent 등에서 즉시 채택. 2010 — Linux 2.6.36 list_sort()에 priv 매개변수 추가 비교자에 컨텍스트 전달 가능. DRM/GPU 드라이버에서 요청. 2019 — Linux 5.1 George Spelvin 대개편 (14개 패치 시리즈) ① sort.c: top-down → bottom-up heapsort (비교 25% 감소) ② swap: 크기 기반 자동 선택 (SWAP_WORDS_64/32/BYTES) ③ list_sort.c: 2:1 비율 병합 규칙 정교화 (비교 횟수 최적) ④ sort_r() 도입: 비교자에 priv 컨텍스트 전달 (SLUB 등에서 활용) 비교 횟수 개선 (2019) sort.c (n=1000): Before: ~20,000 비교 (top-down) After: ~15,000 비교 (bottom-up) list_sort.c (n=1000): Before: ~10,500 비교 After: ~9,990 비교 (이론 최적에 근접) 2022 — Linux 5.18+ min_heap.h 도입 및 sort.c와 연동 타이머, perf, kprobes에서 min-heap 기반 우선순위 큐 사용. sort.c의 sift-down 로직을 min_heapify_all()과 공유. 현재 — Linux 6.x 안정적 유지 보수 단계 호출자 수: sort() ~120, sort_r() ~15, list_sort() ~80. 새 서브시스템은 기존 API를 그대로 사용. 추가 최적화 여지 제한적. 핵심 설계 원칙 (전 기간 공통) • 최악 O(n log n) 보장 — 예측 가능한 지연 시간 • 추가 메모리 O(1) — 커널 메모리 제약 존중 • 재귀 없음 — 커널 스택 8~16KB 제한 준수 • 입력 패턴 독립 — DoS 공격 방지

주요 변경 커밋과 패치(Patch) 시리즈

커널 정렬의 진화 과정에서 가장 중요한 패치들의 목록입니다. 각 패치는 구체적인 성능 개선 수치와 함께 커밋되었습니다.

2002: 최초 도입

Matt Mackall이 lib/sort.c를 추가했습니다. userspace qsort()를 커널에 안전한 heapsort로 대체하여, O(n²) 최악 시간 복잡도를 O(n log n) 보장으로 개선하고 재귀를 제거했습니다.

2009: list_sort 도입

Don Mullis가 lib/list_sort.c를 추가했습니다. 연결 리스트 전용 안정 정렬을 제공하며, 배열 변환 없이 O(1) 공간으로 안정 정렬을 보장합니다.

2019: Spelvin 대개편

주요 커밋: 0b067d8c012a ("lib/sort: make swap functions more generic"), 6a91d41cd8c6 ("lib/sort: use bottom-up heapsort variant"), 043b3f7b6388 ("lib/list_sort: simplify and remove MAX_LIST_LENGTH_BITS"). 비교 횟수 최적화와 swap 전략 자동 선택을 통해 sort() 비교 횟수를 25% 감소시키고, list_sort()를 이론 최적에 근접시켰습니다.

2022+: min_heap 통합

min_heap.h 매크로 API를 도입하여 힙 기반 우선순위 큐를 커널 전역에서 재사용할 수 있게 했습니다. timer, perf, kprobes에서 중복 힙 구현을 제거했습니다.

코드 분석

  • 2002년 설계 결정 — Matt Mackall은 커널의 보안 요구사항(입력 패턴 독립), 메모리 제약(O(1) 추가 메모리), 스택 제약(재귀 없음)을 모두 만족하는 유일한 선택지로 heapsort를 채택했습니다.
  • 2019년 최적화 — George Spelvin(Andrey Abramov)은 14개 패치로 sort.c와 list_sort.c를 동시에 개선했습니다. bottom-up sift-down은 이론적으로 최적에 가까운 비교 횟수를 달성합니다.
  • 안정성 원칙 — 20년 이상 API 시그니처가 유지되고 있습니다. sort(base, num, size, cmp, swap)의 기본 형태는 최초 도입 이후 변경되지 않았습니다.

참고자료