Linux 7.0 메모리 관리 분석 시리즈
Compaction는 리눅스 커널의 외부 단편화(external fragmentation)를 완화하기 위한 메커니즘입니다. Buddy Allocator는 2의 거듭제곱(order) 크기 블록을 할당하는데, 프리 페이지가 충분히 있지만 연속된 high-order 블록이 없는 경우 할당에 실패합니다. Compaction은 이동 가능한(movable) 페이지를 zone의 한쪽 끝으로 옮기고, 반대쪽 끝의 프리 페이지들을 병합하여 high-order 블록을 생성합니다.
Compaction의 핵심은 page migration에 의존합니다. 두 개의 스캐너가 zone의 양쪽 끝에서 서로를 향해 이동합니다: migrate scanner는 이동 가능한 페이지를 탐색하고, free scanner는 프리 페이지를 탐색합니다. 두 스캐너가 만나면 compaction이 완료됩니다. 커널은 Direct compaction (할당 실패 시 즉시 실행), Background compaction (kcompactd 데몬), Proactive compaction (백그라운드 자동 단편화 해소, 커널 5.9+) 세 가지 모드를 지원합니다. 커널 6.15부터는 defrag_mode sysctl이 추가되어 페이지 할당자가 단편화 회피를 적극 수행할 수 있습니다.
일상 비유: Compaction는 주차장 정리와 비슷합니다. 주차장에 빈 자리가 충분하지만 차들이 흩어져 있어大型 버스(연속 4MB 블록)를 주차할 수 없는 상황입니다. 정비사(migrate scanner)가 한쪽 끝의 차(movable 페이지)를 모두 옮기고, 반대쪽 끝의 빈 자리(free scanner)를 모으면 큰 버스를 주차할 수 있는 연속 공간이 생깁니다.
소스 파일 경로:
```
mm/compaction.c ← Compaction 핵심 로직 (3334줄)
include/linux/compaction.h ← enum compact_priority, enum compact_result 정의
mm/internal.h ← struct compact_control, struct capture_control 정의
include/linux/mmzone.h ← zone 구조체 내 compaction 캐시 필드
```
# Compaction 트리거 (전체 시스템)
echo 1 > /proc/sys/vm/compact_memory
# NUMA 노드별 compaction 트리거
echo 1 > /sys/devices/system/node/node0/compact
# 현재 compaction 통계
cat /proc/vmstat | grep -E "compact_|kcompactd"
# Compaction deferred 상태 확인
cat /proc/vmstat | grep "compact_defer_shift"
# 외부 단편화 지수 확인
cat /proc/extfraginfo
# Proactive compaction 활성화/비활성화
cat /proc/sys/vm/compaction_proactiveness # 0~100 (기본 20)
# 단편화 점수 확인 (노드별)
cat /sys/kernel/mm/compaction/proactiveness
# defrag_mode — 단편화 회피 모드 (커널 6.15+)
cat /proc/sys/vm/defrag_mode # 0: 비활성 (기본), 1: 적극적 단편화 회피
# kcompactd 스레드 상태 확인
ps -eo pid,comm | grep kcompactd
# zone별 compaction 캐시 PFN 확인
cat /proc/zoneinfo | grep -E "compact_cached"
Compaction의 모든 상태와 파라미터를 담당하는 핵심 구조체입니다. direct compaction, kcompactd, /proc 트리거 등 모든 진입점에서 사용됩니다.
// mm/internal.h:950-988
struct compact_control {
struct list_head freepages[NR_PAGE_ORDERS]; // 프리 페이지 목록 (order별)
struct list_head migratepages; // 이동 대상 페이지 목록
unsigned int nr_freepages; // 격리된 프리 페이지 수
unsigned int nr_migratepages; // 이동 대상 페이지 수
unsigned long free_pfn; // free scanner 시작 위치
unsigned long migrate_pfn; // migrate scanner 위치 (in/out)
unsigned long fast_start_pfn; // 선형 스캔 시작 위치
struct zone *zone; // 대상 zone
unsigned long total_migrate_scanned; // 총 이동 스캔 수
unsigned long total_free_scanned; // 총 프리 스캔 수
unsigned short fast_search_fail; // fast search 실패 수
short search_order; // fast search 시작 order
const gfp_t gfp_mask; // GFP 마스크
int order; // 할당 대상 order
int migratetype; // 할당 대상 migratetype
const unsigned int alloc_flags; // 할당 플래그
const int highest_zoneidx; // 최대 zone 인덱스
enum migrate_mode mode; // Async/Sync/Sync_light
bool ignore_skip_hint; // skip 힌트 무시 여부
bool no_set_skip_hint; // skip 힌트 설정 안 함
bool ignore_block_suitable; // 적합하지 않은 블록 무시
bool direct_compaction; // true = direct, false = kcompactd
bool proactive_compaction; // true = proactive
bool whole_zone; // 전체 zone 스캔 여부
bool contended; // lock 경쟁 발생 여부
bool finish_pageblock; // 현재 pageblock 스캔 완료 여부
bool alloc_contig; // alloc_contig_range 할당 여부
};
주요 필드 설명:
freepages[NR_PAGE_ORDERS]: free scanner가 격리한 프리 페이지를 order별로 분리하여 보관migratepages: migrate scanner가 격리한 이동 대상 페이지 목록migrate_pfn / free_pfn: 두 스캐너의 현재 위치. compaction이 진행되면서 서로를 향해 이동mode: MIGRATE_ASYNC (논블로킹), MIGRATE_SYNC_LIGHT (기본), MIGRATE_SYNC (완전 동기)direct_compaction: direct compaction인지 kcompactd/background인지 구분Direct compaction 중 IRQ 핸들러가 프리 페이지를 반환하면 즉시 캡처하여 할당에 활용합니다.
// mm/internal.h:994-997
struct capture_control {
struct compact_control *cc; // compaction 제어 구조체 포인터
struct page *page; // 캡처된 프리 페이지 (할당 성공 시)
};
compact_zone_order()에서 current->capture_control에 설정하고, IRQ에서 페이지 해제 시 __free_pages_core()가 이 구조체를 통해 페이지를 캡처합니다.
// include/linux/compaction.h:9-17
enum compact_priority {
COMPACT_PRIO_SYNC_FULL, // 0: 완전 동기 (최고 우선순위)
MIN_COMPACT_PRIORITY = COMPACT_PRIO_SYNC_FULL,
COMPACT_PRIO_SYNC_LIGHT, // 1: 가벼운 동기 (기본)
MIN_COMPACT_COSTLY_PRIORITY = COMPACT_PRIO_SYNC_LIGHT,
DEF_COMPACT_PRIORITY = COMPACT_PRIO_SYNC_LIGHT,
COMPACT_PRIO_ASYNC, // 2: 비동기 (최저 우선순위)
INIT_COMPACT_PRIORITY = COMPACT_PRIO_ASYNC
};
// include/linux/compaction.h:21-56
enum compact_result {
COMPACT_NOT_SUITABLE_ZONE, // 내부: zone 적합하지 않음
COMPACT_SKIPPED, // compaction 시작 안 함
COMPACT_DEFERRED, // 이전 실패로 인해 연기됨
COMPACT_NO_SUITABLE_PAGE, // 내부: 적합 프리 페이지 없음
COMPACT_CONTINUE, // 계속 진행
COMPACT_COMPLETE, // 전체 zone 스캔 완료
COMPACT_PARTIAL_SKIPPED, // 일부 영역만 스캔
COMPACT_CONTENDED, // lock 경쟁으로 중단
COMPACT_SUCCESS, // 할당 성공
};
할당 실패 시 page allocator에서 직접 호출하는 메인 진입 함수입니다. zonelist의 각 zone에 대해 compact_zone_order()를 순차적으로 호출합니다.
// mm/compaction.c:2814-2880
enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
unsigned int alloc_flags, const struct alloc_context *ac,
enum compact_priority prio, struct page **capture)
{
// 1. gfp_compaction_allowed() 검사
// 2. zonelist 순회
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ...) {
// cpuset 검사
// compaction_deferred() 검사 → 연기된 zone 건너뜀
status = compact_zone_order(zone, order, ...);
if (status == COMPACT_SUCCESS) break;
// ASYNC에서 need_resched() 또는 fatal signal → 중단
}
}
분기 로직:
gfp_compaction_allowed() 실패 → 즉시 COMPACT_SKIPPEDcompaction_deferred() 참 → COMPACT_DEFERRED (이전 실패로 연기)COMPACT_SUCCESS → 반복 즉시 중단 (할당 성공 예상)COMPACT_COMPLETE / COMPACT_PARTIAL_SKIPPED → defer_compaction() 호출하나의 zone에 대한 compaction을 수행하는 핵심 함수입니다. migrate/free 스캐너를 교대로 실행하고, 이동 가능한 페이지를 격리하여 migration합니다.
// mm/compaction.c:2510-2747
static enum compact_result compact_zone(struct compact_control *cc,
struct capture_control *capc)
{
// 1. 스캐너 초기 위치 설정 (캐시된 PFN 또는 zone 끝)
// 2. while (compact_finished() == COMPACT_CONTINUE) 반복:
// a. isolate_migratepages(cc) → 이동 대상 페이지 격리
// b. migrate_pages() → compaction_alloc/free 콜백으로 실제 이동
// c. check_drain: order > 0이면 lru_add_drain_cpu_zone()
// 3. 프리 페이지 해제 및 캐시 업데이트
}
핵심 분기:
compaction_suit_allocation_order() 실패 → 즉시 반환 (watermark 미충족)compaction_restarting() → __reset_isolation_suitable() (스킵 힌트 초기화)isolate_migratepages() 결과에 따라 ISOLATE_ABORT / ISOLATE_NONE / ISOLATE_SUCCESS 분기migrate_pages() 실패 → -ENOMEM 시 COMPACT_CONTENDED, ASYNC 실패 시 finish_pageblock 설정compaction이 끝났는지判断하는 함수입니다. 두 스캐너가 만났는지, 프리 페이지가 충분한지, watermark를 만족하는지 확인합니다.
// mm/compaction.c:2234-2355
static enum compact_result __compact_finished(struct compact_control *cc)
{
// 1. compact_scanners_met(): 두 스캐너가 만나면 COMPACT_COMPLETE/PARTIAL_SKIPPED
// 2. proactive_compaction: fragmentation 점수로 판단
// - 점수 > wmark_low → COMPACT_CONTINUE
// - 점수 ≤ wmark_low → COMPACT_SUCCESS
// 3. defrag_mode && kcompactd: NR_FREE_PAGES_BLOCKS 기준 watermark 검사
// 4. direct compactor: free_area에서 대상 migratetype의 프리 페이지 확인
// - 해당 migratetype 프리 → COMPACT_SUCCESS
// - MIGRATE_MOVABLE → CMA fallback 허용
// - steal 가능하면 COMPACT_SUCCESS
// 5. contended || fatal_signal → COMPACT_CONTENDED
}
핵심 분기:
compact_scanners_met() → reset_cached_positions() 후 COMPACT_COMPLETEproactive_compaction → fragmentation_score_zone() vs fragmentation_score_wmark() 비교defrag_mode 활성화 시 NR_FREE_PAGES_BLOCKS 기반 watermark 검사 (high watermark 요구)free_area_empty() 실패 → find_suitable_fallback()으로 steal 가능성 검사하나의 pageblock 내에서 이동 가능한 페이지를 격리하는 함수입니다. LRU 목록에서 페이지를 제거하고 cc->migratepages 목록에 추가합니다.
// mm/compaction.c:836-1308
static int isolate_migratepages_block(struct compact_control *cc,
unsigned long low_pfn, unsigned long end_pfn, isolate_mode_t mode)
{
for (; low_pfn < end_pfn; low_pfn++) {
// PageHuge → isolate_or_dissolve_huge_folio()
// PageBuddy → 건너뜀
// PageCompound && !alloc_contig → skip_isolation_on_order() 검사
// PageLRU → folio 격리
// - too_many_isolated() 검사 (LRU 격리 제한)
// - ISOLATE_ASYNC_MIGRATE → writeback/dirty 페이지 제외
// - folio_test_clear_lru() → LRU에서 제거
// - COMPACT_CLUSTER_MAX 도달 시 중단
}
}
migrate_pages()가 호출하는 콜백 함수입니다. compaction_alloc()은 프리 페이지 목록에서 대상 페이지를 제공하고, compaction_free()는 실패 시 프리 페이지로 반환합니다.
// mm/compaction.c:1797-1848
static struct folio *compaction_alloc_noprof(struct folio *src, unsigned long data)
{
struct compact_control *cc = (struct compact_control *)data;
// 1. freepages[]에서 알맞은 order의 프리 페이지 탐색
// 2. 없으면 isolate_freepages() 호출하여 추가 격리
// 3. 큰 order 블록을 split하여 요청 order로 제공
// 4. nr_freepages, nr_migratepages 갱신
}
static void compaction_free(struct folio *dst, unsigned long data)
{
// 실패한 페이지를 freepages[]로 반환
}
각 NUMA 노드마다 하나씩 실행되는 커널 스레드입니다. proactive compaction과 요청 기반 compaction을 처리합니다.
// mm/compaction.c:3165-3231
static int kcompactd(void *p)
{
pg_data_t *pgdat = (pg_data_t *)p;
long default_timeout = msecs_to_jiffies(HPAGE_FRAG_CHECK_INTERVAL_MSEC);
long timeout = default_timeout;
current->flags |= PF_KCOMPACTD;
set_freezable();
while (!kthread_should_stop()) {
// 1. proactiveness 비활성화 시 timeout = MAX_SCHEDULE_TIMEOUT
// 2. wait_event_freezable_timeout()으로 대기
// 3. kcompactd_work_requested() → kcompactd_do_work() 실행
// 4. should_proactive_compact_node() → compact_node() 실행
// - fragmentation_score_node() > wmark_high → proactive 시작
// - 점수 개선 없으면 timeout 증가 (지연)
timeout = default_timeout;
}
}
proactive compaction 분기:
sysctl_compaction_proactiveness == 0 → timeout = MAX_SCHEDULE_TIMEOUT (비활성)fragmentation_score_node() > wmark_high → compact_node() 실행timeout << COMPACT_MAX_DEFER_SHIFT (최대 64배 지연)Direct compaction에서 compact_control 구조체를 초기화하는 함수입니다. 우선순위(prio)에 따라 mode, whole_zone, ignore_skip_hint 등이 달라집니다.
// mm/compaction.c:2749-2768
static enum compact_result compact_zone_order(struct zone *zone, int order,
gfp_t gfp_mask, enum compact_priority prio,
unsigned int alloc_flags, int highest_zoneidx,
struct page **capture)
{
struct compact_control cc = {
.order = order,
.search_order = order,
.gfp_mask = gfp_mask,
.zone = zone,
.mode = (prio == COMPACT_PRIO_ASYNC) ?
MIGRATE_ASYNC : MIGRATE_SYNC_LIGHT,
.alloc_flags = alloc_flags,
.highest_zoneidx = highest_zoneidx,
.direct_compaction = true,
.whole_zone = (prio == MIN_COMPACT_PRIORITY), // 최고 우선순위에서 전체 zone 스캔
.ignore_skip_hint = (prio == MIN_COMPACT_PRIORITY), // skip 힌트 무시
.ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY) // 적합 블록 무시
};
struct capture_control capc = {
.cc = &cc,
.page = NULL,
};
// capture_control을 current에 설정 — IRQ에서 프리 페이지 캡처 가능
WRITE_ONCE(current->capture_control, &capc);
ret = compact_zone(&cc, &capc);
...
}
compact_control 초기화 분기:
COMPACT_PRIO_ASYNC → MIGRATE_ASYNC (논블로킹, 부분 스캔)COMPACT_PRIO_SYNC_LIGHT (기본) → MIGRATE_SYNC_LIGHT (조건부 동기)MIN_COMPACT_PRIORITY → whole_zone=true, ignore_skip_hint=true (전체 zone, skip 무시)__alloc_pages() 실패
└→ __alloc_pages_slowpath()
└→ __alloc_pages_direct_reclaim() 실패
└→ __alloc_pages_direct_compact()
└→ try_to_compact_pages() ← Direct compaction 진입
└→ compact_zone_order() ← compact_control 초기화
└→ compact_zone() ← 코어 루프
├→ compaction_suit_allocation_order() ← watermark 검사
├→ isolate_migratepages() ← 이동 대상 격리
│ └→ isolate_migratepages_block() ← pageblock 단위
├→ migrate_pages() ← 실제 이동
│ ├→ compaction_alloc() ← 대상 페이지 할당
│ └→ compaction_free() ← 실패 시 반환
└→ compact_finished() ← 완료 검사
wakeup_kcompactd() ← page allocator에서 호출
└→ wake_up_interruptible()
kcompactd() 스레드 기상
├→ kcompactd_do_work() ← 일반 compaction
│ └→ compact_zone()
└→ should_proactive_compact_node() ← proactive compaction
└→ compact_node() ← 전체 zone 순회
└→ compact_zone()
Zone 시작 (low_pfn)
│
│ ← migrate scanner →
│ (migrate_pfn: 앞에서 뒤로)
│
│ 이동 가능한 페이지 격리 → migratepages
│
│ ← free scanner
│ (free_pfn: 뒤에서 앞으로)
│
│ 프리 페이지 격리 → freepages[]
│
└─ 두 스캐너가 만나면 compaction 종료
| 항목 | MIGRATE_ASYNC | MIGRATE_SYNC_LIGHT | MIGRATE_SYNC |
|---|---|---|---|
| **블로킹** | 비동기 (논블로킹) | 조건부 동기 | 완전 동기 |
| **우선순위** | COMPACT_PRIO_ASYNC | DEF_COMPACT_PRIORITY | MIN_COMPACT_PRIORITY |
| **스캐너 범위** | 부분 스캔 | 부분 스캔 | 전체 zone (whole_zone) |
| **skip_hint** | 사용 | 사용 | 무시 (ignore_skip_hint) |
| **적합 블록 무시** | 아님 | 아님 | 무시 (ignore_block_suitable) |
| **isolation stride** | COMPACT_CLUSTER_MAX | 1 | 1 |
| **lock 경쟁 시** | trylock → 중단 | lock 대기 | lock 대기 |
| 경로 | 진입 함수 | compact_control 설정 | 주요 특징 |
|---|---|---|---|
| **Direct compaction** | `try_to_compact_pages()` | direct_compaction=true, whole_zone=false (prio별) | 할당 실패 시 즉시 실행, capture_control로 프리 페이지 캡처 |
| **kcompactd** | `kcompactd_do_work()` | direct_compaction=false, whole_zone=false | 백그라운드 실행, watermark_high 기준 |
| **Proactive** | `compact_node()` | proactive_compaction=true, whole_zone=true | fragmentation 점수 기반, kcompactd 내부 |
| **compact_memory** | `compact_nodes()` | order=-1, whole_zone=true, ignore_skip_hint=true | /proc/sys/vm/compact_memory 트리거 |
| **NUMA node compact** | `compact_node()` | order=-1, whole_zone=true | /sys/devices/system/node/nodeX/compact 트리거 |
| 결과 | 의미 | 후속 동작 |
|---|---|---|
| `COMPACT_SUCCESS` | 할당 가능한 프리 블록 확보 | 할당 시도 |
| `COMPACT_CONTINUE` | 계속 진행 가능 | 반복 루프 계속 |
| `COMPACT_COMPLETE` | 전체 zone 스캔 완료 | `defer_compaction()` (non-MIN) |
| `COMPACT_PARTIAL_SKIPPED` | 부분 스캔만 완료 | `defer_compaction()` |
| `COMPACT_DEFERRED` | 이전 실패로 연기 | 건너뜀 |
| `COMPACT_SKIPPED` | compaction 불가 (watermark 미충족 등) | 다음 zone으로 |
| `COMPACT_CONTENDED` | lock 경쟁으로 중단 | 즉시 반환 |
| 조건 | skip 설정 | skip 검사 | 설명 |
|---|---|---|---|
| 격리된 페이지 없음 | `set_pageblock_skip()` | `isolation_suitable()` | 빈 블록은 향후 스캔에서 건너뜀 |
| isolated == 0 && finish_pageblock | `set_pageblock_skip()` | `isolation_suitable()` | 완료 시에도 설정 |
| ignore_skip_hint=true | 설정 안 함 | 항상 통과 | MIN_COMPACT_PRIORITY에서 사용 |
| no_set_skip_hint=true | 설정 안 함 | 설정 | isolated 포지션 업데이트 시 |
| pageblock_skip_persistent | 설정 안 함 | 항상 건너뜀 | compound >= pageblock_order |
| defrag_mode | kcompactd 동작 | compact_finished 기준 | 일반 할당자 영향 |
|---|---|---|---|
| `0` (기본) | 일반 compact_zone | free_area에서 프리 페이지 확인 | 기존대로 동작 |
| `1` (적극적) | NR_FREE_PAGES_BLOCKS 기준 | high watermark 요구 | 할당 시 compaction 회피 강화 |
minzkn.com의 "메모리 운영 플레이북"과 유사하게, compaction 관련 증상별 진단 포인트를 정리합니다.
| 증상 | 우선 점검 | 권장 조치 | |
|---|---|---|---|
| **고차 페이지 할당 실패** (THP 2MB 등) | `/proc/buddyinfo`, `cat /proc/vmstat \ | grep compact` | compaction 튜닝, hugepage 정책 점검, `defrag_mode=1` 시도 |
| **compact_stall 급증** | `cat /proc/vmstat \ | grep compact_stall` | THP `defrag` 모드를 `madvise`로 변경, proactive_compactiveness 조정 |
| **compact_fail > compact_success** | `cat /proc/vmstat \ | grep compact_fail` | 단편화 심화 상태 — `echo 1 > /proc/sys/vm/compact_memory` 수동 트리거 |
| **kcompactd 과도한 CPU 사용** | `top -H -p $(pgrep kcompactd)` | `compaction_proactiveness` 감소 (0으로 비활성화 가능) | |
| **khugepaged 과도한 CPU** | `top -H -p $(pgrep khugepaged)` | THP defrag를 `madvise`로 변경, khugepaged 스캔 주기 조정 | |
| **defer_compaction 반복** | `cat /proc/vmstat \ | grep defer` | 메모리 부족 상태 — 회수 경로 점검, 불필요한 메모리 사용 해소 |
| **zone 내 fragmentation index 높음** | `cat /proc/extfraginfo` | memcg 제한 재조정, CMA 영역 확보, zonelist 순서 변경 |
# compaction 관련 통계 전체 확인 스크립트
echo "=== Compaction 통계 ==="
cat /proc/vmstat | grep -E "compact_|kcompactd|defer"
echo ""
echo "=== buddyinfo (단편화 상태) ==="
cat /proc/buddyinfo
echo ""
echo "=== fragmentation index ==="
cat /proc/extfraginfo
echo ""
echo "=== sysctl 파라미터 ==="
sysctl vm.compaction_proactiveness vm.compact_memory vm.extfrag_threshold 2>/dev/null
echo ""
echo "=== defrag_mode (커널 6.15+) ==="
cat /proc/sys/vm/defrag_mode 2>/dev/null || echo "해당 없음"