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_head) 문서를 먼저 읽으세요. list_sort()는 커널 이중 연결 리스트(struct list_head)를 직접 조작하므로, list_head의 구조와 순회 매크로(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)이중 연결 리스트 (struct list_head)
알고리즘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) 등 모든 타입에 대응합니다.

사용 빈도와 선택 기준

커널 6.x 기준으로 두 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⌊log⊂2; n⌋ 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는 이중 연결 리스트(struct list_head)를 위한 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() ~110곳, list_sort() ~75곳 (커널 6.x 기준, 드라이버 포함)
서브시스템파일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) 재귀 깊이 → 스택 오버플로우 */
}

Kconfig 관련 옵션

옵션기본값설명
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.1lib/list_sort.cGeorge Spelvin 재작성 (2:1 비율)
v5.4lib/sort.cswap 최적화 (word-at-a-time)
v5.14lib/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
동시 수정 (락 없음)비결정적 크래시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) 외부 상태 변경 없음. 이 규약을 어기면 정렬 결과가 비결정적이 되고, 최악의 경우 무한 루프에 빠집니다.

요약 비교 테이블

특성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

참고 자료