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()를 사용합니다.
단계별 이해
- 정렬 개념 복습
비교 기반 정렬의 하한(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) | 이중 연결 리스트 |
| 알고리즘 | 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) 등 모든 타입에 대응합니다.
사용 빈도와 선택 기준
최근 메인라인 계열에서 두 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는 이중 연결 리스트를 위한 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.2 | lib/list_sort.c | George Spelvin 재작성 (2:1 비율) |
| v5.2 | lib/sort.c | swap 최적화 (word-at-a-time, swap_words_32/64) |
| v5.4 | 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 |
| 동시 수정 (잠금(Lock) 없음) | 비결정적 크래시 | 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).
디버그 힌트:
dead000000000100LIST_POISON1에 해당하며, 이미 삭제된 노드를 의미합니다.0000000000000000NULL에 해당하며, 초기화되지 않은 노드를 의미합니다.
비교자 전순서 검증 코드
/* 디버그용: 비교자 전순서 속성 검증 */
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);
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 함수를 파라미터로 전달하여, 루프 내부에서 추가 분기 없이 직접 호출합니다.
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은 이중 포인터로, 병합 결과를 제자리에 삽입할 수 있습니다.
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()와 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 후크 정렬은 커널 부팅 초기화 과정에서 한 번만 수행됩니다. 런타임 오버헤드가 없습니다.
정렬 안정성과 비교자 설계
정렬 안정성(stability)은 동일 키를 가진 원소들의 상대적 순서가 정렬 전후에 보존되는지를 결정합니다. 커널의 두 정렬 API는 이 특성이 다르며, 비교자 설계에 직접적인 영향을 줍니다.
list_sort()의 안정성 보장 메커니즘
list_sort()의 안정성은 merge() 함수의 단 하나의 비교 연산자에 의해 결정됩니다.
/* 안정성의 핵심: <= 비교 */
if (cmp(priv, a, b) <= 0) {
/* a를 선택 → 동일 키에서 앞쪽 원소가 먼저 출력 */
*tail = a;
...
}
/* 만약 < 만 사용하면 (불안정):
* cmp == 0일 때 b가 선택될 수 있어 순서가 뒤집힘 */
<=와 <의 차이는 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) > 0 ⇒ cmp(b,a) < 0 | 순서가 비결정적 → 정렬 결과 불안정 |
| 추이성(Transitivity) | a < b ∧ b < c ⇒ a < c | 순환 관계 → 정렬 불가, 무한 루프 |
| 동치 추이성 | a ≡ b ∧ b ≡ c ⇒ a ≡ 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으로 정렬 후 순서를 검증하는 디버그 코드를 추가하면 비교자 버그를 조기에 발견할 수 있습니다.
벤치마크와 선택 가이드
커널의 두 정렬 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회 평균).
| n | sort() [μs] | list_sort() [μs] | 비율 |
|---|---|---|---|
| 10 | 0.3 | 0.5 | 0.6x |
| 100 | 5.2 | 7.8 | 0.7x |
| 1,000 | 72 | 95 | 0.8x |
| 10,000 | 920 | 1,250 | 0.7x |
| 100,000 | 12,400 | 18,700 | 0.7x |
sort()가 전반적으로 30~40% 빠릅니다 (캐시 효과). 단, 리스트에서 배열로의 변환 비용을 포함하면 list_sort()가 유리합니다.
캐시 효율 분석
sort()와 list_sort()의 성능 차이는 대부분 캐시 효율에서 비롯됩니다.
인라인 정렬(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에는 포함되지 않으므로 필요 시 각 모듈에서 구현합니다.
선택 의사결정 트리
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하는 전체 과정을 보여줍니다.
list_sort() 병합 과정 시각화
lib/list_sort.c의 bottom-up merge sort는 입력 리스트의 노드를 하나씩 읽어 pending 스택에 누적하며, 비트 카운터 규칙에 따라 인접 서브리스트를 병합합니다. 아래 다이어그램은 6개 원소 리스트 [4, 2, 6, 1, 5, 3]의 전체 병합 과정을 단계별로 추적합니다.
swap_func 최적화 결정 트리
lib/sort.c의 성능을 결정하는 핵심 요소 중 하나는 swap 전략입니다. 호출자가 swap_func = NULL을 전달하면, 커널은 요소의 메모리 정렬(alignment)과 크기에 따라 최적의 swap 함수를 자동 선택합니다. 이 과정은 sort_r() 진입 시 한 번만 수행되어 루프 내 분기 오버헤드가 없습니다.
/* 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;
}
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);
}
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 선택) */
list_sort(): O((n+m) log(n+m)) — m이 클수록 유리.
교차점은 대략 m > log₂(n)입니다. n=1000이면 m > 10일 때 list_sort()가 더 빠릅니다.
Linux 6.12 ~ 6.19 Sorting 동향
lib/sort.c는 2024~2026년 구간에 6.12의 *_nonatomic 변종 도입을 제외하면 거의 변경이 없습니다. __sort_r에 may_schedule 플래그가 추가되어 장시간 정렬 도중 cond_resched()가 허용되며, 인접한 min_heap 유틸리티 쪽이 더 활발하게 확장되었습니다. 6.18, 6.19에서도 lib/sort.c 알고리즘 본체는 안정 상태를 유지하고 있습니다.
| 커널 | 릴리스 | 변경 사항 | 실무 시사점 |
|---|---|---|---|
| 6.12 (LTS) | 2024-11 | sort_nonatomic()/sort_r_nonatomic() 도입 — __sort_r에 may_schedule 플래그를 추가해 장시간 정렬 중 cond_resched() 허용 | 대용량 배열 정렬 시 CONFIG_PREEMPT 비활성 커널에서 스케줄 지연 완화. atomic 컨텍스트에서는 기존 sort()/sort_r() 사용 |
| 6.13 | 2025-01 | Min heap API 확장(비인라인 함수·최적화 6커밋) — sort.c와는 별개이지만 정렬 유틸리티 계열 | <linux/min_heap.h> 소비자(BPF, perf, RCU)가 더 효율적으로 동작 |
| 6.14 | 2025-03 | 메인 API 안정 — 주변 확장만 | — |
| 6.15 | 2025-05 | 메인 API 안정 — 주변 확장만 | — |
| 6.16 | 2025-07 | 메인 API 안정 — 주변 확장만 | — |
| 6.17 | 2025-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.19 | 2026-02 | 메인 API 안정 — 주변 확장만. free_pcppages_bulk 배치 처리 개선이 메모리 할당 경로에 영향을 주나 정렬 라이브러리 자체는 변경 없음 | — |
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() 호출 코드는 수정 없이 사용 가능합니다.
관련 문서
- 연결 리스트 — 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 |
Heapsort 단계별 시각화
커널의 lib/sort.c가 사용하는 bottom-up heapsort는 두 단계로 구성됩니다. 먼저 입력 배열을 max-heap으로 변환(heapify)한 뒤, 루트(최대값)를 반복 추출하여 정렬된 영역을 확장합니다. 아래 다이어그램은 8개 원소 [5, 3, 8, 1, 7, 2, 6, 4]에 대한 전체 과정을 단계별로 보여줍니다.
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]의 병합 과정을 추적합니다.
pending 리스트의 내부 표현
커널 구현에서 pending 리스트는 struct list_head의 prev 포인터만 사용하여 단방향 연결 리스트를 구성합니다. 이 기법은 정렬 중 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 = pending—prev포인터를 pending 스택의 next 포인터로 재활용합니다. 정렬 완료 후merge_final에서prev와next를 모두 복원합니다.!(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 내부 구현
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()를 호출하는 서브시스템은 광범위합니다. 아래 다이어그램은 주요 호출자를 기능 영역별로 분류하고, 각 호출의 목적과 정렬 대상을 요약합니다.
서브시스템별 정렬 특성
각 서브시스템이 정렬을 사용하는 목적은 크게 세 가지로 나뉩니다.
검색 최적화
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년 첫 도입 이후 여러 차례의 최적화와 재설계를 거쳤습니다. 아래 타임라인은 주요 변경점과 그 배경을 정리합니다.
주요 변경 커밋과 패치(Patch) 시리즈
커널 정렬의 진화 과정에서 가장 중요한 패치들의 목록입니다. 각 패치는 구체적인 성능 개선 수치와 함께 커밋되었습니다.
- 2002: 최초 도입
-
Matt Mackall이
lib/sort.c를 추가했습니다. userspaceqsort()를 커널에 안전한 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)의 기본 형태는 최초 도입 이후 변경되지 않았습니다.
참고자료
- 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) 추가 메모리 병합 정렬의 학술적 배경입니다