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()는 커널 이중 연결 리스트(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()를 사용합니다.
단계별 이해
- 정렬 개념 복습
비교 기반 정렬의 하한(O(n log n))과 안정성 개념을 확인합니다. - heapsort 원리 이해
max-heap 구성 후 루트 추출 반복으로 정렬하는 과정을 시각화합니다. - merge sort 원리 이해
분할-병합 패턴과 리스트 특화 bottom-up 방식의 차이를 파악합니다. - API 사용법 학습
sort(),sort_r(),list_sort()의 시그니처와 비교자 작성 패턴을 익힙니다. - 실전 호출 사례 분석
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에 있습니다.
커널 정렬 개요
qsort(3)는 재귀 깊이, 스택 크기, 최악 시간에 대해 느슨한 제약을 가집니다.
커널은 (1) 스택이 8~16KB로 극도로 제한되고, (2) 동적 메모리 할당이 실패할 수 있으며, (3) 인터럽트(Interrupt) 컨텍스트에서도 호출될 수 있어, 이 세 가지 제약이 알고리즘 선택을 결정합니다.
리눅스 커널은 사용자 공간의 qsort(3)와 달리, 두 가지 범용 정렬 함수를 제공합니다. 각각은 대상 자료구조에 최적화되어 있습니다.
| 항목 | sort() — lib/sort.c | list_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-place | O(1) 추가 (포인터 재연결) |
| 안정성 | 불안정 (unstable) | 안정 (stable) |
| 비교자 | cmp_func_t / cmp_r_func_t | list_cmp_func_t |
| 도입 시점 | 2.6.x (qsort 대체, 2006) | 2.6.x (2009, George Spelvin 재작성 2019) |
두 함수는 모두 비교 함수(comparator)를 콜백(Callback)으로 받아 정렬 기준을 결정합니다. 정렬 로직 자체는 비교 독립적이므로, 하나의 구현으로 정수, 포인터, 구조체(Struct) 등 모든 타입에 대응합니다.
사용 빈도와 선택 기준
커널 6.x 기준으로 두 API의 사용 빈도를 분석하면:
| 기준 | sort() | list_sort() |
|---|---|---|
| 호출 사이트 수 | ~110곳 | ~75곳 |
| 주요 사용처 | 파일시스템(Filesystem), ACPI, perf | DRM, 네트워크, PM |
| 선택 이유 | 데이터가 배열에 이미 존재 | 데이터가 리스트로 관리됨 |
| 전형적 규모 | 수십~수천 개 | 수십~수백 개 |
sort() → 리스트 재구성 패턴이 더 빠를 수 있습니다. 반대로 데이터가 배열에 있지만 안정 정렬이 필요하면, 임시 리스트로 변환 → list_sort() → 배열에 복사할 수 있습니다. 두 경우 모두 추가 O(n) 시간이 소요됩니다.
왜 Quicksort가 아닌가
사용자 공간에서 가장 널리 쓰이는 quicksort가 커널에서 배제된 이유는 세 가지입니다.
| 문제 | Quicksort | Heapsort (커널 선택) |
|---|---|---|
| 최악 시간 | O(n²) — 정렬된/역순 입력 | O(n log n) — 항상 보장 |
| 스택 사용 | O(n) 최악 재귀 깊이 | O(1) — 반복(iterative) |
| 공격 표면 | pivot 선택 조작 가능 | 입력 패턴 무관 |
introsort 대안 검토
introsort(quicksort + heapsort 하이브리드, C++ STL의 std::sort)도 검토 대상이었습니다:
| 항목 | Introsort | Heapsort (커널 선택) |
|---|---|---|
| 평균 성능 | 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 알고리즘
lib/sort.c의 heapsort는 두 단계로 동작합니다:
- Build-heap (heapify) — 배열을 max-heap으로 구성합니다. 하위 절반의 노드부터 역순으로 sift-down을 수행합니다.
- Extract-max — 루트(최댓값)와 마지막 요소를 교환한 뒤, 힙 크기를 줄이고 루트에서 sift-down합니다. n-1회 반복하면 정렬 완료입니다.
커널 구현의 핵심 최적화는 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) / 2 | base + ((i-1)/2) * size |
| 왼쪽 자식 | 2*i + 1 | base + (2*i+1) * size |
| 오른쪽 자식 | 2*i + 2 | base + (2*i+2) * size |
| 마지막 비-리프 | n/2 - 1 | build-heap 시작 인덱스 |
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);
| 파라미터 | 타입 | 설명 |
|---|---|---|
base | void * | 정렬할 배열의 시작 주소 |
num | size_t | 배열 요소 개수 |
size | size_t | 각 요소의 바이트 크기 (sizeof(element)) |
cmp | cmp_func_t | 비교 함수: int (*)(const void *, const void *) |
swap | swap_func_t | 교환 함수 (NULL이면 자동 선택) |
swap에 NULL을 전달하면 커널이 요소 크기와 정렬(alignment)에 따라 최적의 swap 전략을 자동 선택합니다.
커스텀 swap은 요소가 내부 포인터(자기참조 구조체 등)를 가질 때만 필요합니다.
sort() 동작 보장
- 시간 — O(n log n) 최악/평균/최선, 어떤 입력이든 동일
- 공간 — O(1) 추가 메모리 (in-place)
- 안정성 — 불안정 (동일 키 원소의 순서 비보장)
- 중단 안전 — 정렬 중 인터럽트/선점(Preemption)은 안전하지만, 비교 함수 내에서 슬립(Sleep)하면 안됨 (atomic 컨텍스트 가능)
- 재진입 — 같은 배열에 대해 재진입 호출 불가 (동일 데이터 동시 정렬 금지)
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이며 실패하지 않습니다. 이는 동적 메모리 할당을 수행하지 않기 때문입니다. 비교 함수가 잘못되면 정렬 결과가 부정확하지만, 함수 자체는 항상 반환합니다.
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_t를 cmp_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()의 비교 함수는 3번째 인자로 priv를 받습니다. sort()의 비교 함수(2인자)를 sort_r()에 직접 전달하면 컴파일은 되지만 잘못된 결과가 나올 수 있습니다.
비교자 패턴
올바른 비교 함수는 커널 정렬의 핵심입니다. 잘못된 비교자는 무한 루프, 데이터 손상, 미정의 동작을 초래합니다.
cmp_func_t 시그니처
typedef int (*cmp_func_t)(const void *, const void *);
반환값 규약:
- 음수 — a < b
- 0 — a == b
- 양수 — a > b
타입 안전 래퍼 패턴
/* 구조체 배열 정렬 예시 */
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) == 0 | if (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 전략을 자동 선택합니다.
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 iterations | 8 iterations | 32 iterations |
| Byte swap (1B 단위) | 16 iterations | 64 iterations | 256 iterations |
| 속도 비율 | ~8x | ~8x | ~8x |
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)이 현재의 최적화된 버전으로 재작성했습니다.
알고리즘의 핵심 루프 (간략화):
/* 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;
}
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);
| 파라미터 | 타입 | 설명 |
|---|---|---|
priv | void * | 비교 함수에 전달할 컨텍스트 (NULL 가능) |
head | struct list_head * | 정렬할 리스트의 헤드 노드 |
cmp | list_cmp_func_t | 비교 함수: 음수(a<b), 0(a==b), 양수(a>b) |
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 비율의 수학적 근거는 비교 횟수 최소화입니다. 크기가 비슷한 두 서브리스트를 병합할 때 비교 횟수가 최적에 가깝습니다. 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 최대 깊이 | 참고 |
|---|---|---|
| 100 | 7 | 일반적 디렉터리 |
| 1,000 | 10 | 대형 디렉터리 |
| 10,000 | 14 | DRM 모드 리스트 |
| 100,000 | 17 | 극단적 경우 |
| 1,000,000 | 20 | 이론적 최대 |
pending 스택은 리스트 노드의 prev 포인터를 재활용(Recycling)하므로, 추가 메모리 할당이 전혀 없습니다. 이것이 list_sort()의 O(1) 공간 복잡도를 보장하는 핵심입니다.
list_sort()는 run 탐지를 하지 않습니다. 이유는 (1) 커널 데이터가 대체로 무작위이고, (2) run 탐지 오버헤드(Overhead)가 커널 스케일에서 이득보다 클 수 있기 때문입니다.
복잡도 분석
| 항목 | sort() — Heapsort | list_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) swap | O(n log n) 포인터 재연결 |
| 추가 공간 | O(1) | O(1) (포인터 조작만) |
| 캐시 친화성 | 좋음 (배열 연속 접근) | 나쁨 (포인터 추적) |
| 안정성 | 불안정 | 안정 |
sort()의 비교는 배열 인덱싱(O(1))이고 list_sort()의 비교는 container_of() + 포인터 추적을 수반합니다. 비교 함수가 비싸면(문자열 비교 등) 이 차이는 무시할 수 있지만, 정수 비교처럼 싼 경우 sort()가 실측에서 2~3배 빠를 수 있습니다.
heapsort의 비교 횟수 유도:
- Build-heap: 최대 2n 비교 (sift-down × n/2 노드)
- Extract 단계: 최대 2⌊log2 n⌋ 비교 × (n-1)회
- 총 비교: ≤ 2n log2 n + O(n)
merge sort의 비교 횟수:
- 각 병합 단계에서 n개 원소를 이동하며 최대 n-1회 비교
- log2 n 단계 → 총 ≤ n log2 n
- 실제로는 2:1 비율 병합으로 이론적 최솟값(n log2 n - n + 1)에 근접
실측 비교 횟수
lib/test_sort.c와 lib/test_list_sort.c의 카운터를 활성화하면 실측 비교 횟수를 확인할 수 있습니다:
| n | 이론 하한 (n log2n) | sort() 실측 | 비율 | list_sort() 실측 | 비율 |
|---|---|---|---|---|---|
| 100 | 665 | ~1,100 | 1.65x | ~550 | 0.83x |
| 1,000 | 9,966 | ~17,000 | 1.71x | ~8,700 | 0.87x |
| 10,000 | 132,877 | ~230,000 | 1.73x | ~120,000 | 0.90x |
| 100,000 | 1,660,964 | ~2,900,000 | 1.75x | ~1,500,000 | 0.90x |
이론적 비교 횟수 최적성
비교 기반 정렬의 이론적 하한은 ⌈log2(n!)⌉ ≈ n log2 n - 1.443n입니다. 각 알고리즘의 비교 횟수를 비교합니다:
| 알고리즘 | 비교 횟수 (최악) | 이론 하한 대비 | 커널 사용 |
|---|---|---|---|
| 이론 하한 | n log2 n - 1.44n | 1.00x | - |
| Merge sort (list_sort) | n log2 n + O(n) | ~1.00x | O |
| Heapsort (sort) | 2n log2 n | ~2.00x | O |
| Quicksort (배제) | n²/2 (최악) | ∞ | X |
| Insertion sort | n²/2 (최악) | ∞ | X |
readdir 정렬
파일시스템의 readdir은 list_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);
}
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)로
* 디스크 레벨에서 이미 정렬되어 있어 별도 정렬 불필요 */
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 | 서버 의존 | 서버 순서 그대로 | 클라이언트 미정렬 |
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 | 정렬 기준 |
|---|---|---|---|
| ext4 | extent 배열 | sort() | 논리 블록 번호 |
| btrfs | extent backref | sort() | 바이트 오프셋(Offset) |
| XFS | alloc candidate | sort() | AG + 블록 번호 |
| F2FS | dirty segment | sort() | 유효 블록 수 |
sort()가 자연스러운 선택이며, 캐시 친화적 접근으로 성능도 우수합니다.
struct ext4_es_tree를 사용합니다. 이 트리는 red-black tree로 항상 정렬 상태를 유지하지만, 디스크에서 일괄 로드한 extent 배열은 sort()로 정렬해야 합니다.
extent 정렬 최적화 효과
extent를 논리 블록 순으로 정렬하면 다음과 같은 I/O 최적화가 가능합니다:
- 연속 읽기 병합 — 인접 extent를 하나의 큰 I/O 요청으로 합침 (bio merging)
- 엘리베이터 최적화 — 블록 번호 순 정렬로 디스크 시크 최소화
- 프리페치 효율 — 순차 접근 패턴을 생성하여 HW 프리페치 활성화
- delalloc 할당 — 정렬된 extent 목록에서 연속 블록 그룹을 효율적으로 할당
/* 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()가 관여합니다.
/* 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_RANDOM | v4.7 (2016) | freelist 할당 순서 랜덤화 |
CONFIG_SLAB_FREELIST_HARDENED | v4.14 (2017) | freelist 포인터 XOR 난독화 |
CONFIG_RANDOM_KMALLOC_CACHES | v6.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 검증에서 잘못된 주소가 나와 감지됨 */
네트워크 정렬
네트워크 서브시스템에서도 정렬은 다양한 곳에서 활용됩니다.
/* 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;
}
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 등과 구분이 필요합니다.
| 서브시스템 | 파일 | API | 정렬 목적 |
|---|---|---|---|
| ext4 | fs/ext4/extents.c | sort() | extent 논리 블록 순 정렬 |
| btrfs | fs/btrfs/volumes.c | sort() | 디바이스 devid 순 정렬 |
| DRM | drivers/gpu/drm/*.c | list_sort() | 디스플레이 모드 정렬 |
| PM | drivers/base/power/*.c | list_sort() | 디바이스 전원 의존성 순 정렬 |
| ACPI | drivers/acpi/*.c | sort() | 네임스페이스(Namespace) 정렬 |
| perf | kernel/events/core.c | sort() | 이벤트 그룹 정렬 |
| netfilter | net/netfilter/*.c | sort() | hook 우선순위 정렬 |
| libfs | fs/libfs.c | list_sort() | dcache readdir 정렬 |
| crypto | crypto/algapi.c | list_sort() | 알고리즘 우선순위 정렬 |
| blk | block/blk-mq-tag.c | list_sort() | IO request 태그 정렬 |
성능 벤치마크
커널 자체 테스트(lib/test_sort.c, lib/test_list_sort.c)와 실측 데이터를 기반으로 성능을 분석합니다.
CONFIG_DEBUG_* 비활성 상태의 릴리즈 빌드에서 측정해야 신뢰할 수 있습니다.
| 배열 크기 (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 느려짐 |
/* 간접 정렬(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의 진화는 성능과 안전성의 균형을 찾는 과정이었습니다.
| 시기 | 변경 | 커밋/기여자 | 동기 |
|---|---|---|---|
| ~2003 | lib/qsort.c 존재 | (초기 커널) | 사용자 공간 qsort 모방 |
| 2006 | heapsort로 교체 | Matt Mackall | O(n²) 최악, 스택 오버플로우 방지 |
| 2009 | list_sort.c 도입 | Don Mullis | 리스트 전용 안정 정렬 필요 |
| 2019 | list_sort 재작성 | George Spelvin | 2:1 비율 병합, 비교 횟수 최적화 |
| 2019 | sort.c swap 최적화 | Rasmus Villemoes | word-at-a-time swap, 타입 인식 |
| 2022 | sort_r() 도입 | (여러 기여자) | 비교 함수에 컨텍스트 전달 |
| 2023+ | KMSAN/KASAN 호환 | (지속적) | 정렬 중 메모리 접근 안전성 검증 |
list_sort.c의 2019년 재작성은 단일 커밋으로 전체 알고리즘을 교체한 드문 사례입니다.
핵심 개선점: (1) bottom-up 방식으로 재귀 제거, (2) 2:1 비율 병합으로 비교 횟수 ~50% 감소, (3) pending 스택의 비트 카운터로 O(1) 병합 시점 결정.
이 재작성은 정식 수학적 증명을 동반했으며, 커밋 메시지에 알고리즘 분석이 상세히 기술되어 있습니다.
list_sort 2019년 재작성 상세
George Spelvin의 재작성은 커밋 043b3f7b6377에서 이루어졌으며, 다음 변경을 포함합니다:
- top-down → bottom-up — 재귀 분할 대신, 요소를 하나씩 읽어 pending 스택에 쌓는 방식
- 자연 병합 제거 — 이전 버전은 정렬된 구간(run)을 탐지했으나, 오버헤드가 이득보다 커서 제거
- 비트 카운터 도입 — count의 이진 표현으로 병합 시점을 O(1)에 결정
- merge_final 최적화 — 마지막 병합에서
prev포인터 복원을 동시에 수행
/* 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도 다음과 같이 최적화되었습니다:
- swap 타입 감지 — 요소 크기와 정렬을 보고 word/byte swap 자동 선택
- parent_to_children 접근 — 부모에서 자식으로의 sift-down을 최적화하여 비교 횟수 감소
- sort_r() 도입 — 비교 함수에 컨텍스트를 전달하는
qsort_r(3)대응
초기 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_SORT | n (모듈) | lib/sort.c 자체 테스트 모듈 |
CONFIG_TEST_LIST_SORT | n (모듈) | lib/list_sort.c 자체 테스트 모듈 |
CONFIG_SLAB_FREELIST_RANDOM | y (대부분) | slab freelist 랜덤화 (정렬 관련) |
sort()와 list_sort()는 별도의 Kconfig 옵션 없이 항상 커널에 포함됩니다. lib/sort.o와 lib/list_sort.o는 obj-y로 빌드되므로 모듈이 아닌 커널 이미지에 직접 링크됩니다. EXPORT_SYMBOL로 모듈에서도 사용할 수 있습니다.
/* lib/sort.c */
EXPORT_SYMBOL(sort_r);
EXPORT_SYMBOL(sort);
/* lib/list_sort.c */
EXPORT_SYMBOL(list_sort);
커널 버전별 주요 변경 요약
| 커널 버전 | 파일 | 변경 |
|---|---|---|
| v2.6.x | lib/qsort.c | 초기 quicksort 존재 |
| v2.6.17 | lib/sort.c | heapsort 도입 (qsort 교체) |
| v2.6.29 | lib/list_sort.c | list merge sort 도입 |
| v5.1 | lib/list_sort.c | George Spelvin 재작성 (2:1 비율) |
| v5.4 | lib/sort.c | swap 최적화 (word-at-a-time) |
| v5.14 | lib/sort.c | sort_r() 도입 |
| v6.1+ | lib/sort.c | KASAN/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) 가이드
정렬 관련 버그는 발견이 어렵고 재현이 불안정합니다. 주요 오류 패턴과 진단 방법을 정리합니다.
비교자 오류 패턴
| 오류 | 증상 | 원인 | 해결 |
|---|---|---|---|
| 정수 오버플로우 | 큰 값이 앞에 위치 | 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 pointer | KASAN 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()."
이 세 가지 규칙으로 대부분의 커널 정렬 요구사항을 해결할 수 있습니다.
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
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);
관련 문서
- 연결 리스트 (list_head) — list_sort()의 기반 자료구조, 순회/조작 매크로
- Min-Heap / Red-Black Tree — lib/min_heap.h 힙 기반 우선순위 큐, 자기 균형 이진 탐색 트리
- XArray — 인덱스 기반 정렬 컨테이너(Container)
- 메모리 관리(Memory Management) — slab 할당자, freelist 구조
- VFS — readdir, dcache 동작 원리
- ext4 — extent 정렬, 블록 할당
- 동기화 기법 — 정렬 중 동시 접근 보호
- 메모리 관리 (Slab) — SLUB freelist 랜덤화, 보안 강화
- NAPI — 네트워크 패킷 수신 처리, GRO 정렬
- XArray — 인덱스 기반 정렬 컨테이너
요약 비교 테이블
| 특성 | sort() | sort_r() | list_sort() |
|---|---|---|---|
| 헤더 | linux/sort.h | linux/sort.h | linux/list_sort.h |
| 대상 | 배열 | 배열 | 리스트 |
| 알고리즘 | Heapsort | Heapsort | Merge sort |
| 비교자 타입 | cmp_func_t | cmp_r_func_t | list_cmp_func_t |
| 컨텍스트 | 없음 | priv 포인터 | priv 포인터 |
| 안정성 | 불안정 | 불안정 | 안정 |
| 추가 메모리 | O(1) | O(1) | O(1) |
| swap 커스텀 | 가능 | 가능 | 해당 없음 |
| 최악 시간 | O(n log n) | O(n log n) | O(n log n) |
| 반환값 | void | void | void |
참고 자료
- lib/sort.c — Bootlin Elixir — 배열 정렬(Heapsort) 구현 소스 코드입니다
- include/linux/sort.h — Bootlin Elixir —
sort(),sort_r()함수 선언 및 비교자 타입 정의입니다 - lib/list_sort.c — Bootlin Elixir — 연결 리스트 병합 정렬(Merge Sort) 구현 소스 코드입니다
- include/linux/list_sort.h — Bootlin Elixir —
list_sort()함수 선언 및 비교자 타입 정의입니다 - lib/test_sort.c — Bootlin Elixir —
sort()자체 테스트 모듈 소스 코드입니다 - lib/test_list_sort.c — Bootlin Elixir —
list_sort()자체 테스트 모듈 소스 코드입니다 - LWN: A new sort implementation for the kernel (2020) — George Spelvin의 bottom-up heapsort 최적화 패치 분석 기사입니다
- LWN: Reworking sort() and list_sort() (2019) —
sort()와list_sort()의 비교 횟수 최적화 리워크 과정을 다룹니다 - George Spelvin의 sort/list_sort 최적화 패치 시리즈 — 비교 횟수 25% 감소, bottom-up heapsort 도입 등 핵심 패치입니다
- Kernel API — Sorting (kernel.org) — 커널 공식 문서의 정렬 API 레퍼런스입니다
- Wikipedia: Heapsort —
sort()가 채택한 힙 정렬 알고리즘의 이론적 배경입니다 - Wikipedia: Merge sort — Bottom-up list implementation —
list_sort()가 채택한 연결 리스트 병합 정렬의 학술적 기반입니다 - Knuth, D. — Sorting and Searching (TAOCP Vol. 3) — 정렬 알고리즘 전반의 학술적 기초 참고 문헌입니다
- Huang & Langston (1988) — Practical In-Place Merging — O(1) 추가 메모리 병합 정렬의 학술적 배경입니다