메모리 컴팩션 (Memory Compaction)
리눅스 커널의 Memory Compaction 메커니즘을 심층 분석합니다. Buddy Allocator에서 발생하는 외부 단편화(external fragmentation) 문제를 해결하기 위한 Two-Scanner 알고리즘(migrate scanner + free scanner), migrate type별 페이지(Page) 분류(UNMOVABLE/MOVABLE/RECLAIMABLE/CMA), kcompactd 데몬 동작, Direct Compaction과 Proactive Compaction 경로, fragmentation index 계산, CMA 및 THP와의 상호작용, ftrace 이벤트 기반 디버깅(Debugging), vmstat 카운터 분석, 운영 튜닝 플레이북까지 커널 소스(mm/compaction.c, mm/migrate.c) 기반으로 분석합니다.
핵심 요약
- 외부 단편화 해소 -- 물리 메모리(Physical Memory)에 빈 페이지가 충분하지만 연속되지 않아 고차(order) 할당이 실패하는 문제를 해결합니다.
- Two-Scanner -- migrate scanner(존 시작부터 이동 가능 페이지 탐색)와 free scanner(존 끝부터 빈 페이지 탐색)가 양쪽에서 만날 때까지 동작합니다.
- 3가지 트리거 -- Direct Compaction(할당 실패 시 즉시), kcompactd(워터마크(Watermark) 기반 백그라운드), Proactive Compaction(단편화 점수 기반 사전 예방).
- migrate type 구분 -- UNMOVABLE(커널 slab 등), MOVABLE(사용자 페이지), RECLAIMABLE(파일 캐시(Cache)), CMA(연속 메모리 예약)로 분류하여 compaction 대상을 결정합니다.
- 성능 영향 -- Direct Compaction은 할당 지연(Latency)을 수백 ms까지 증가시킬 수 있으므로 Proactive Compaction으로 사전 방지가 중요합니다.
단계별 이해
- 외부 단편화 개념 파악
Buddy Allocator에서 order-0 페이지는 충분하지만 order-3 이상 연속 블록을 만들 수 없는 상황을 이해합니다. - migrate type 분류 이해
각 pageblock(보통 2MB)이 어떤 migrate type으로 분류되는지, 왜 이 분류가 compaction에 중요한지 파악합니다. - Two-Scanner 알고리즘 추적
migrate scanner와 free scanner가 존의 양 끝에서 출발하여 이동 가능한 페이지를 빈 페이지 위치로 옮기는 과정을 따라갑니다. - 트리거 경로 구분
Direct, kcompactd, Proactive 각 트리거의 진입 조건과 동작 차이를 구분합니다. - 모니터링과 튜닝
vmstat 카운터, ftrace 이벤트, /proc 인터페이스로 compaction 상태를 진단하고 최적화합니다.
mm/compaction.c (핵심 compaction 로직), mm/migrate.c (페이지 마이그레이션), mm/page_alloc.c (Buddy Allocator와 direct compaction 트리거), include/linux/compaction.h (API 선언).
Mel Gorman의 커밋 시리즈(v2.6.35, "Memory compaction")가 최초 구현이며, Vlastimil Babka의 proactive compaction(v5.9)이 최근 주요 개선입니다.
왜 필요한가: 외부 단편화 문제
Buddy Allocator는 물리 메모리를 2의 거듭제곱 크기 블록(order 0 = 4KB, order 1 = 8KB, ..., order 10 = 4MB)으로 관리합니다. 시스템이 오래 동작하면 할당과 해제가 반복되면서 외부 단편화(external fragmentation)가 발생합니다. 전체 빈 페이지 수는 충분하지만, 연속된 물리 페이지가 부족하여 고차 할당이 실패하는 상황입니다.
외부 단편화의 실제 영향
| 할당 요구 | 필요 order | 연속 페이지 | 단편화 시 영향 |
|---|---|---|---|
| 일반 페이지 할당 | order-0 | 1 페이지 (4KB) | 영향 없음 |
| 네트워크 버퍼 (jumbo) | order-1~3 | 2~8 페이지 | 중간 영향 |
| THP (Transparent Huge Page) | order-9 | 512 페이지 (2MB) | 심각한 영향 |
| CMA 연속 할당 | 가변 | 수백~수천 페이지 | 심각한 영향 |
| hugetlbfs (1GB) | order-18 | 262144 페이지 | 부팅 시 예약 필수 |
/proc/meminfo의 MemFree가 충분해도 /proc/buddyinfo에서 고차 order의 빈 블록이 0이면 고차 할당은 실패합니다.
이때 OOM Killer가 호출되지 않고 compaction이 먼저 시도됩니다.
Buddy Allocator의 /proc/buddyinfo를 보면 단편화 상태를 직접 확인할 수 있습니다:
# 단편화 확인
cat /proc/buddyinfo
# Node 0, zone Normal 312 156 78 39 12 3 1 0 0 0 0
# ^order-0 ^order-4 ^order-7 이상 없음
# 페이지 타입별 분포 확인
cat /proc/pagetypeinfo
# Free pages count per migrate type at order
# Node Zone Type 0 1 2 3 4 5
# Node 0 Normal Unmovable 45 12 3 0 0 0
# Node 0 Normal Movable 200 80 30 10 2 1
# Node 0 Normal Reclaimable 67 20 5 1 0 0
설명
/proc/buddyinfo는 각 order별 빈 블록 수를 보여줍니다. 고차 order(7 이상)의 값이 0이면 2MB THP 할당이 불가능합니다.
/proc/pagetypeinfo는 migrate type별로 분류하여 어떤 종류의 페이지가 단편화를 유발하는지 진단할 수 있습니다.
Two-Scanner 알고리즘
Memory Compaction의 핵심은 Two-Scanner 알고리즘입니다. 하나의 존(zone) 내에서 두 개의 스캐너가 양쪽 끝에서 출발하여 서로를 향해 진행합니다:
- Migrate Scanner -- 존의 시작(낮은 PFN)부터 위쪽으로 스캔하며 이동 가능한(movable) 페이지를 격리(Isolation)합니다.
- Free Scanner -- 존의 끝(높은 PFN)부터 아래쪽으로 스캔하며 빈 페이지를 격리합니다.
두 스캐너가 만나면 compaction 라운드가 종료됩니다. 격리된 이동 가능 페이지를 빈 페이지 위치로 마이그레이션하면, 존의 아래쪽에 사용 중인 페이지가 모이고 위쪽에 빈 페이지가 연속으로 생깁니다.
compact_zone() 핵심 구현
/* mm/compaction.c - compact_zone() 핵심 루프 (단순화) */
static enum compact_result compact_zone(struct compact_control *cc)
{
/* migrate scanner: 존 시작부터 위쪽으로 */
cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[...];
/* free scanner: 존 끝부터 아래쪽으로 */
cc->free_pfn = cc->zone->compact_cached_free_pfn;
while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
/* 1. migrate scanner: 이동 가능 페이지 격리 */
isolate_migratepages(cc);
/* 2. free scanner: 빈 페이지 격리 */
if (list_empty(&cc->migratepages))
continue;
/* 3. 실제 마이그레이션 수행 */
err = migrate_pages(&cc->migratepages, compaction_alloc,
compaction_free, (unsigned long)cc,
cc->mode, MR_COMPACTION, NULL);
}
return ret;
}
설명
compact_zone()은 struct compact_control을 통해 스캐너 위치를 추적합니다.
compact_finished()는 두 스캐너가 만났거나 요청된 order의 블록이 충분히 확보되었는지 확인합니다.
compaction_alloc()은 free scanner가 격리한 빈 페이지를 마이그레이션 대상으로 제공하는 콜백(Callback)입니다.
스캐너 위치 캐싱
매번 존의 시작/끝부터 스캔하면 비효율적이므로, 각 존은 마지막 스캔 위치를 캐싱합니다:
struct zone {
/* ... */
/* Compaction scanner 캐시 */
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsigned long compact_cached_free_pfn;
/* ... */
};
ASYNC_AND_SYNC 배열로 비동기/동기 모드별 별도 캐시를 유지합니다. Compaction이 완료되거나 존 전체를 스캔한 경우 캐시가 리셋됩니다.
Migrate Type: 페이지 이동(Page Migration) 가능성 분류
커널은 물리 메모리를 pageblock(일반적으로 2MB = pageblock_order 크기) 단위로 migrate type을 지정합니다. 이 분류는 compaction이 어떤 페이지를 이동시킬 수 있는지를 결정합니다.
| Migrate Type | 설명 | Compaction 이동 가능 | 예시 |
|---|---|---|---|
MIGRATE_UNMOVABLE | 이동 불가능한 페이지 | 불가 | 커널 slab, 페이지 테이블(Page Table), kmalloc |
MIGRATE_MOVABLE | 이동 가능한 페이지 | 가능 | 사용자 프로세스(Process) anon/file 페이지 |
MIGRATE_RECLAIMABLE | 회수 가능한 페이지 | 회수 후 가능 | 페이지 캐시(Page Cache), dentry/inode 캐시 |
MIGRATE_PCPTYPES | per-CPU 페이지 캐시 경계 | - | (분류 기준값, 실제 타입 아님) |
MIGRATE_HIGHATOMIC | 고우선순위 원자적(Atomic) 할당용 | 불가 | GFP_ATOMIC 예약 블록 |
MIGRATE_CMA | CMA 예약 영역 | 조건부 가능 | DMA 연속 할당 예약 |
MIGRATE_ISOLATE | 격리 중 (hot-remove 등) | - | 메모리 핫플러그(Hotplug) 진행 중 |
pageblock_flags 관리
/* include/linux/pageblock-flags.h */
enum pageblock_bits {
PB_migrate,
PB_migrate_end = PB_migrate + 3 - 1, /* 3비트: 8가지 타입 */
PB_migrate_skip, /* compaction 스캐너 skip 비트 */
NR_PAGEBLOCK_BITS
};
/* pageblock의 migrate type 설정 */
void set_pageblock_migratetype(struct page *page, int migratetype)
{
/* pageblock의 첫 번째 page의 flags에 3비트로 인코딩 */
set_pfnblock_flags_mask(page, (unsigned long)migratetype,
page_to_pfn(page), MIGRATETYPE_MASK);
}
/* pageblock의 migrate type 조회 */
int get_pageblock_migratetype(struct page *page)
{
return get_pfnblock_flags_mask(page, page_to_pfn(page),
MIGRATETYPE_MASK);
}
설명
각 pageblock(2MB)의 첫 번째struct page에 3비트로 migrate type을 저장합니다.
PB_migrate_skip 비트는 compaction 스캐너가 이미 방문한 pageblock을 건너뛰도록 합니다.
할당 시 fallback 순서(MOVABLE -> RECLAIMABLE -> UNMOVABLE)에 따라 다른 migrate type의 블록을 빌려올 수 있으며, 이때 pageblock의 type이 변경될 수 있습니다.
페이지 격리와 마이그레이션 경로
Compaction은 두 단계로 진행됩니다: (1) 페이지 격리(isolation) -- LRU 리스트에서 분리, (2) 페이지 마이그레이션(migration) -- 새 위치로 복사 후 매핑(Mapping) 갱신.
isolate_migratepages_block() 상세
/* mm/compaction.c - 이동 가능 페이지 격리 (단순화) */
static bool 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++) {
struct page *page = pfn_to_page(low_pfn);
/* skip 비트가 설정된 pageblock 건너뛰기 */
if (get_pageblock_skip(page))
continue;
/* 격리 불가 조건 확인 */
if (!PageLRU(page)) /* LRU에 없으면 skip */
continue;
if (PageWriteback(page)) /* 쓰기 중이면 skip */
continue;
/* pageblock의 migrate type 확인 */
mt = get_pageblock_migratetype(page);
if (mt == MIGRATE_UNMOVABLE)
continue; /* 이동 불가 */
/* LRU에서 격리 */
if (__isolate_lru_page_prepare(page, mode) == 0) {
list_move(&page->lru, &cc->migratepages);
cc->nr_migratepages++;
}
}
return true;
}
설명
migrate scanner는 pageblock 단위로 순회하면서PageLRU()인 이동 가능 페이지를 cc->migratepages 리스트에 모읍니다.
MIGRATE_UNMOVABLE 타입의 pageblock은 건너뛰고, PB_migrate_skip 비트로 이미 처리된 블록도 건너뜁니다.
PageWriteback(디스크 쓰기 중)인 페이지는 안전하게 이동할 수 없으므로 제외합니다.
kcompactd 데몬 동작
kcompactd는 NUMA 노드마다 하나씩 생성되는 커널 스레드(Kernel Thread)로, 백그라운드에서 compaction을 수행합니다. kswapd와 유사하게 워터마크 기반으로 깨어나며, 시스템 부하가 적을 때 단편화를 사전에 해소합니다.
깨어남 조건
| 조건 | 트리거 위치 | 설명 |
|---|---|---|
| Watermark boost | boost_watermark() | 페이지 해제 시 워터마크 부스트가 활성화되면 kcompactd를 깨움 |
| Order-based 요청 | wakeup_kcompactd() | 특정 order 이상의 블록이 부족할 때 요청 |
| Proactive 트리거 | kcompactd_do_work() | 단편화 점수가 임계치 초과 시 자체 루프 |
/* mm/compaction.c - kcompactd 메인 루프 (단순화) */
static int kcompactd(void *p)
{
pg_data_t *pgdat = (pg_data_t *)p;
while (!kthread_should_stop()) {
/* 요청 대기 */
wait_event_freezable(pgdat->kcompactd_wait,
kcompactd_work_requested(pgdat));
/* 깨어나면 compaction 수행 */
kcompactd_do_work(pgdat);
/* proactive compaction 조건 확인 */
if (should_proactive_compact_node(pgdat))
proactive_compact_node(pgdat);
}
return 0;
}
static void kcompactd_do_work(pg_data_t *pgdat)
{
for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
struct zone *zone = &pgdat->node_zones[zoneid];
if (!populated_zone(zone))
continue;
/* compaction 필요 여부 확인 */
if (compaction_suitable(zone, pgdat->kcompactd_max_order,
0, zoneid) != COMPACT_CONTINUE)
continue;
/* 실제 compaction 수행 */
compact_zone(&cc);
}
}
설명
kcompactd는 wait_event_freezable()로 대기하다가 wakeup_kcompactd()에 의해 깨어납니다.
깨어나면 노드의 모든 존을 순회하며 compaction_suitable()로 compaction이 필요한 존에서 compact_zone()을 호출합니다.
should_proactive_compact_node()는 v5.9+에서 추가된 proactive compaction 조건을 확인합니다.
Direct Compaction 경로
페이지 할당 요청이 고차 order이고, Buddy Allocator에서 즉시 충족되지 않으며, kswapd 깨우기(Wakeup)와 직접 회수(direct reclaim)로도 해결되지 않으면, 할당 요청 컨텍스트에서 직접(direct) compaction을 수행합니다.
/* mm/page_alloc.c - 할당 slow path에서 direct compaction (단순화) */
static struct page *__alloc_pages_direct_compact(gfp_t gfp_mask,
unsigned int order, unsigned int alloc_flags,
const struct alloc_context *ac,
enum compact_priority prio,
enum compact_result *compact_result)
{
/* compaction 가능 여부 사전 확인 */
if (!order)
return NULL; /* order-0은 compaction 불필요 */
/* compaction 수행 */
*compact_result = try_to_compact_pages(gfp_mask, order,
alloc_flags, ac, prio, &page);
if (page)
return page; /* compaction으로 확보 성공 */
/* compact_result에 따라 재시도/포기 결정 */
if (*compact_result == COMPACT_SKIPPED ||
*compact_result == COMPACT_DEFERRED)
return NULL;
/* compaction 후 다시 할당 시도 */
return get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
}
Compaction Priority
Direct compaction은 단계적으로 강도를 높여가며 시도합니다:
| Priority | 설명 | 동작 |
|---|---|---|
COMPACT_PRIO_ASYNC | 비동기 (최약) | 일부 pageblock만 스캔, 잠금(Lock) 경쟁 시 즉시 포기 |
COMPACT_PRIO_SYNC_LIGHT | 가벼운 동기 | 더 넓은 범위 스캔, 일부 대기 허용 |
COMPACT_PRIO_SYNC_FULL | 완전 동기 (최강) | 전체 존 스캔, 쓰기 중 페이지 대기, 최대 노력 |
COMPACT_PRIO_SYNC_FULL은 수백 ms의 지연을 유발할 수 있습니다. 실시간성이 중요한 워크로드에서는 /proc/sys/vm/compaction_proactiveness를 높여 사전 compaction을 강화하거나, THP를 비활성화하는 것을 고려하세요.
Compaction 결과 코드
| 결과 | 의미 | 후속 동작 |
|---|---|---|
COMPACT_SUCCESS | 충분한 연속 블록 확보 | 할당 재시도 |
COMPACT_PARTIAL_SKIPPED | 일부 존만 처리 | 다음 priority로 재시도 |
COMPACT_COMPLETE | 전체 스캔 완료, 불충분 | OOM 또는 포기 |
COMPACT_SKIPPED | compaction 불필요/불가능 | 다른 경로 시도 |
COMPACT_DEFERRED | 최근 실패로 연기 | deferred 카운터 감소 대기 |
COMPACT_CONTENDED | 잠금 경쟁으로 중단 | 나중에 재시도 |
COMPACT_NOT_SUITABLE_ZONE | 존 조건 불충족 | 다른 존에서 시도 |
Proactive Compaction (v5.9+)
커널 v5.9에서 도입된 Proactive Compaction은 할당 실패를 기다리지 않고, 단편화 점수가 특정 임계치를 넘으면 kcompactd가 사전에 compaction을 수행합니다.
/* mm/compaction.c - proactive compaction 핵심 로직 (단순화) */
static bool should_proactive_compact_node(pg_data_t *pgdat)
{
int wmark_high;
if (!sysctl_compaction_proactiveness)
return false; /* 비활성화 */
wmark_high = 100 - sysctl_compaction_proactiveness + 10;
/* fragmentation_score가 wmark_high 이상이면 compaction 필요 */
return fragmentation_score_node(pgdat) > wmark_high;
}
static unsigned int fragmentation_score_zone(struct zone *zone)
{
unsigned int score = 0;
/* 각 order에 대해 외부 단편화 점수 누적 */
for (order = 1; order < MAX_ORDER; order++) {
int index = __fragmentation_index(order, ...);
score += (index * (1 << order)) / zone->managed_pages;
}
return score;
}
설명
sysctl_compaction_proactiveness가 20(기본값)이면 wmark_high = 90, wmark_low = 80으로 설정됩니다.
노드의 fragmentation score가 90을 초과하면 proactive compaction이 시작되고, 80 아래로 내려가면 중지됩니다.
값을 높일수록(예: 50) wmark가 낮아져 더 적극적으로 compaction하며, 0이면 완전 비활성화됩니다.
트리거 조건: 워터마크, Order, Fragmentation Index
Compaction이 시작되려면 여러 조건이 충족되어야 합니다. compaction_suitable() 함수가 이 판단을 담당합니다:
/* mm/compaction.c - compaction 적합성 판단 (단순화) */
enum compact_result compaction_suitable(struct zone *zone, int order,
unsigned int alloc_flags,
int highest_zoneidx)
{
/* 1. order-0이면 compaction 불필요 */
if (order == -1)
return COMPACT_SKIPPED;
/* 2. 빈 페이지가 워터마크 이하이면 회수가 먼저 필요 */
watermark = low_wmark_pages(zone) + compact_gap(order);
if (!__zone_watermark_ok(zone, 0, watermark, highest_zoneidx,
alloc_flags, zone->free_area))
return COMPACT_SKIPPED;
/* 3. fragmentation index로 단편화 문제인지 판단 */
fragindex = fragmentation_index(zone, order);
if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold)
return COMPACT_NOT_SUITABLE_ZONE;
/* 단편화가 아니라 실제 메모리 부족 */
return COMPACT_CONTINUE;
}
설명
compaction은 다음 세 조건을 모두 만족해야 시작됩니다: (1) 요청 order가 1 이상, (2) 존의 빈 페이지 수가 최소 워터마크 이상 (회수가 먼저가 아니라는 의미), (3) fragmentation index가extfrag_threshold(기본 500)보다 큼 (단편화가 실제 원인).
compact_gap() 함수
compact_gap()은 compaction이 의미 있으려면 존에 최소 몇 페이지의 빈 공간이 있어야 하는지를 계산합니다:
static inline unsigned long compact_gap(unsigned int order)
{
/* 최소 2 * (1 << order) 페이지의 여유 필요 */
return 2UL << order;
}
단편화 인덱스 계산
Fragmentation Index는 특정 order의 할당 실패가 실제 메모리 부족 때문인지, 외부 단편화 때문인지를 0~1000 범위로 정량화합니다.
/* mm/compaction.c - fragmentation_index 계산 (단순화) */
static int __fragmentation_index(unsigned int order, struct contig_page_info *info)
{
unsigned long requested = 1UL << order;
if (info->free_blocks_total == 0)
return 0; /* 빈 블록이 아예 없음 */
if (info->free_blocks_suitable)
return -1000; /* 이미 충분한 블록 존재 */
/*
* fragindex = 1000 - (free_pages 대비 적합 블록 비율)
* 0에 가까울수록: 빈 페이지 자체가 부족 (allocation problem)
* 1000에 가까울수록: 빈 페이지는 충분하나 단편화 (fragmentation problem)
*/
return 1000 - div_u64(
(1000 + (div_u64(info->free_pages * 1000ULL, requested))),
info->free_blocks_total);
}
설명
free_blocks_suitable: 요청 order 이상의 빈 블록 수. 이미 충분하면 -1000을 반환합니다.
free_pages: 모든 order의 총 빈 페이지 수. free_blocks_total: 모든 order의 빈 블록 수.
빈 페이지가 많지만 작은 블록으로 쪼개져 있으면(free_blocks_total이 크면) fragindex가 1000에 가까워집니다.
debugfs로 단편화 인덱스 확인
# 각 존, 각 order별 단편화 인덱스 확인
cat /sys/kernel/debug/extfrag/extfrag_index
# Node 0, zone Normal -1.000 -1.000 -1.000 -1.000 0.861 0.912 0.953 0.976 0.988 0.994 0.997
# order-0 order-3 ^order-4: 높은 단편화
# unusable index: 특정 order를 할당할 수 없는 비율
cat /sys/kernel/debug/extfrag/unusable_index
# Node 0, zone Normal 0.000 0.012 0.045 0.134 0.387 0.621 0.812 0.923 0.967 0.989 0.997
CMA와의 상호작용
CMA(Contiguous Memory Allocator)는 부팅 시 대규모 연속 메모리를 예약하고, 사용하지 않을 때는 MOVABLE 할당에 빌려줍니다. CMA 할당 요청이 오면 빌려간 페이지를 마이그레이션하여 연속 블록을 복구해야 하는데, 이때 compaction 인프라를 활용합니다.
| 속성 | MIGRATE_CMA | MIGRATE_MOVABLE |
|---|---|---|
| 평소 사용 | MOVABLE 할당에 빌려줌 | 직접 할당 |
| CMA 할당 시 | 빌려간 페이지를 마이그레이션하여 반환 | 영향 없음 |
| UNMOVABLE 할당 | 사용 불가 (빌려줄 수 없음) | fallback으로 사용 가능 |
| Compaction 대상 | CMA 영역 내 MOVABLE 페이지 | 일반 MOVABLE 페이지 |
/* mm/cma.c - CMA 할당 시 compaction/migration (단순화) */
struct page *cma_alloc(struct cma *cma, unsigned long count,
unsigned int align, bool no_warn)
{
for (;;) {
/* CMA 예약 영역에서 연속 PFN 찾기 */
pfn = cma_alloc_range(cma, count, align);
if (!pfn)
break;
/* 해당 범위의 페이지를 격리 */
ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
GFP_KERNEL | __GFP_NORETRY);
if (ret == 0)
return pfn_to_page(pfn); /* 성공 */
/* 실패하면 다른 범위 시도 */
cma_clear_bitmap(cma, pfn, count);
}
return NULL;
}
/* mm/page_alloc.c - alloc_contig_range는 내부적으로 __alloc_contig_migrate_range() 호출 */
/* __alloc_contig_migrate_range()는 migrate_pages()를 사용하여 범위 내 페이지를 이동 */
설명
CMA 할당은alloc_contig_range()를 통해 지정 범위의 모든 사용 중 페이지를 마이그레이션합니다.
이 과정은 compaction과 동일한 migrate_pages() 인프라를 사용하지만, 특정 PFN 범위를 대상으로 한다는 점이 다릅니다.
UNMOVABLE 페이지가 CMA 영역에 할당되면 마이그레이션이 불가능하므로, CMA 영역은 UNMOVABLE 할당 요청을 거부합니다.
CONFIG_CMA_DEBUG를 활성화하면 이러한 오염을 감지할 수 있습니다.
THP 트리거 Compaction
Transparent Huge Pages(THP)는 order-9(2MB) 페이지를 투명하게 사용하므로, 할당 시 compaction이 빈번하게 트리거됩니다. THP의 defrag 모드에 따라 compaction 동작이 달라집니다:
| defrag 모드 | 설정 위치 | compaction 동작 |
|---|---|---|
always | /sys/kernel/mm/transparent_hugepage/defrag | 모든 THP 할당에서 direct compaction 수행 |
defer | 동일 | kcompactd에게 위임, 직접 compaction 안 함 |
defer+madvise | 동일 | MADV_HUGEPAGE 영역만 direct, 나머지 defer |
madvise | 동일 | MADV_HUGEPAGE 영역에서만 direct compaction |
never | 동일 | compaction 트리거 안 함, fallback to order-0 |
# 현재 THP defrag 모드 확인
cat /sys/kernel/mm/transparent_hugepage/defrag
# always defer defer+madvise [madvise] never
# 서버 권장: defer+madvise (대부분 defer, 명시적 요청만 direct)
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# 데이터베이스/지연 민감 워크로드: never 또는 madvise
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
always 모드는 THP 할당마다 최대 수백 ms의 direct compaction 지연을 유발할 수 있습니다. Redis, MongoDB 등 지연 민감 워크로드에서는 madvise 또는 never가 권장됩니다. compact_stall vmstat 카운터가 급증한다면 THP defrag 모드를 낮추는 것을 고려하세요.
THP 할당 경로
/* mm/huge_memory.c - THP 할당 시 compaction 트리거 (단순화) */
static struct page *__alloc_hugepage(gfp_t gfp, int order, int node)
{
/* GFP 플래그에 따라 direct compaction 허용 여부 결정 */
gfp_t alloc_gfp = gfp | __GFP_COMP | __GFP_NOWARN;
/* defrag 모드에 따라 __GFP_DIRECT_RECLAIM 설정 */
if (defrag == ALWAYS || (defrag == MADVISE && vma_is_madvised))
alloc_gfp |= __GFP_DIRECT_RECLAIM;
else if (defrag == DEFER || defrag == DEFER_MADVISE)
alloc_gfp &= ~__GFP_DIRECT_RECLAIM;
/* kcompactd에게만 위임 */
return alloc_pages_node(node, alloc_gfp, order);
/* -> __alloc_pages() -> slow path -> __alloc_pages_direct_compact() */
}
ftrace 이벤트로 디버깅
커널은 compaction 동작을 추적할 수 있는 ftrace 이벤트를 제공합니다. 이를 통해 compaction이 언제, 왜, 얼마나 효과적으로 수행되는지 분석할 수 있습니다.
주요 ftrace 이벤트 상세
| 이벤트 | 필드 | 의미 |
|---|---|---|
mm_compaction_begin | zone_start, migrate_pfn, free_pfn, zone_end, sync | compaction 시작, 스캐너 초기 위치 |
mm_compaction_end | zone_start, migrate_pfn, free_pfn, zone_end, sync, status | compaction 종료, 결과 상태 |
mm_compaction_isolate_migratepages | start_pfn, end_pfn, nr_scanned, nr_taken | migrate scanner 격리 결과 |
mm_compaction_isolate_freepages | start_pfn, end_pfn, nr_scanned, nr_taken | free scanner 격리 결과 |
mm_compaction_migratepages | nr_migrated, nr_failed | 마이그레이션 성공/실패 수 |
mm_compaction_try_to_compact_pages | order, gfp_mask, mode | direct compaction 시작 |
mm_compaction_finished | zone, order, result | 존별 compaction 완료 판정 |
mm_compaction_suitable | zone, order, result | compaction 적합성 판단 |
mm_compaction_deferred | zone, order | compaction 연기 |
mm_compaction_kcompactd_wake | nid, order, highest_zoneidx | kcompactd 깨어남 |
# ftrace 출력 예시
# kcompactd0-34 [002] 1234.567: mm_compaction_begin:
# zone_start=0x100000 migrate_pfn=0x100000 free_pfn=0x1ff000 zone_end=0x200000 sync=0
# kcompactd0-34 [002] 1234.568: mm_compaction_isolate_migratepages:
# range=(0x100000 ~ 0x100200) nr_scanned=512 nr_taken=45
# kcompactd0-34 [002] 1234.569: mm_compaction_migratepages:
# nr_migrated=42 nr_failed=3
# kcompactd0-34 [002] 1234.570: mm_compaction_end:
# zone_start=0x100000 migrate_pfn=0x100200 free_pfn=0x1fe800 zone_end=0x200000
# sync=0 status=success
설명
이 예시에서 kcompactd가 비동기(sync=0) 모드로 동작하여, 512 페이지를 스캔해 45개를 격리하고 42개 마이그레이션에 성공했습니다(3개 실패).nr_failed가 높으면 이동 불가능한 페이지(UNMOVABLE이 아니지만 잠금 중이거나 writeback 중인 페이지)가 많다는 의미입니다.
vmstat compact_* 카운터 분석
/proc/vmstat에서 compaction 관련 카운터를 모니터링하면 시스템의 compaction 부하를 정량적으로 파악할 수 있습니다.
| 카운터 | 의미 | 높을 때 의미 |
|---|---|---|
compact_stall | Direct compaction으로 인한 프로세스 정지 횟수 | 할당 지연 증가, 단편화 심각 |
compact_fail | Compaction 시도 후 실패 횟수 | UNMOVABLE 오염 심각, 구조적 문제 |
compact_success | Compaction 성공 횟수 | compaction이 효과적으로 동작 중 |
compact_migrate_scanned | Migrate scanner가 스캔한 페이지 수 | 스캔 범위 파악 |
compact_free_scanned | Free scanner가 스캔한 페이지 수 | 스캔 범위 파악 |
compact_isolated | 격리된 페이지 총 수 | 실제 이동 대상 규모 |
compact_daemon_wake | kcompactd 깨어난 횟수 | 백그라운드 compaction 빈도 |
compact_daemon_migrate_scanned | kcompactd migrate scanner 스캔 수 | 데몬 작업량 |
compact_daemon_free_scanned | kcompactd free scanner 스캔 수 | 데몬 작업량 |
# compaction 카운터 모니터링
grep ^compact /proc/vmstat
# compact_migrate_scanned 12345678
# compact_free_scanned 23456789
# compact_isolated 345678
# compact_stall 123
# compact_fail 45
# compact_success 78
# compact_daemon_wake 234
# compact_daemon_migrate_scanned 5678901
# compact_daemon_free_scanned 8901234
# 주기적 모니터링 (10초 간격)
watch -n 10 'grep ^compact /proc/vmstat'
# compact_stall 증가 속도 확인 (성능 문제 진단)
awk '/compact_stall/ {print $2}' /proc/vmstat
sleep 60
awk '/compact_stall/ {print $2}' /proc/vmstat
# 차이가 크면 direct compaction이 빈번하게 발생 중
핵심 비율 분석
compact_success / (compact_success + compact_fail)가 50% 이하이면 구조적 단편화 문제가 있습니다.
compact_stall이 빠르게 증가하면 proactive compaction을 강화하거나 THP를 조정해야 합니다.
compact_isolated / compact_migrate_scanned가 매우 낮으면(1% 이하) UNMOVABLE 페이지가 존을 지배하고 있다는 신호입니다.
Compaction Tunable 파라미터
커널은 여러 sysctl과 sysfs 파라미터를 통해 compaction 동작을 조정할 수 있습니다.
| 파라미터 | 경로 | 기본값 | 설명 |
|---|---|---|---|
compaction_proactiveness | /proc/sys/vm/ | 20 | 0(비활성)~100(최대). proactive compaction 적극성 |
extfrag_threshold | /proc/sys/vm/ | 500 | fragindex 이 값 이하이면 compaction 건너뜀 (메모리 부족 판단) |
compact_memory | /proc/sys/vm/ | - | 1을 쓰면 수동으로 모든 존 compaction 트리거 |
defrag | /sys/kernel/mm/transparent_hugepage/ | madvise | THP compaction 트리거 모드 |
compact_unevictable_allowed | /proc/sys/vm/ | 1 | 1이면 unevictable LRU의 페이지도 compaction 대상 |
min_free_kbytes | /proc/sys/vm/ | 가변 | 간접 영향: 워터마크 높이를 결정하여 compaction 트리거에 영향 |
시나리오별 튜닝 가이드
# 1. THP를 많이 사용하는 HPC/VM 서버: proactive 강화
echo 50 > /proc/sys/vm/compaction_proactiveness
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# 2. 지연 민감 DB 서버: direct compaction 최소화
echo 70 > /proc/sys/vm/compaction_proactiveness # 사전 예방 강화
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
echo 300 > /proc/sys/vm/extfrag_threshold # compaction 진입 기준 낮춤
# 3. 임베디드/IoT: 메모리 절약 우선
echo 0 > /proc/sys/vm/compaction_proactiveness # proactive 비활성
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 4. 수동 compaction 트리거 (유지보수 시간)
echo 1 > /proc/sys/vm/compact_memory
# 5. 존별 수동 compaction (debugfs)
echo 1 > /sys/kernel/debug/compaction/compact_node_0
설명
compaction_proactiveness를 높이면 kcompactd가 더 자주 동작하여 direct compaction 발생을 줄입니다. 대신 CPU 사용량이 약간 증가합니다.
extfrag_threshold를 낮추면 더 적은 단편화에서도 compaction을 시작하여 고차 할당 성공률이 높아집니다.
compact_memory에 1을 쓰면 모든 노드, 모든 존에 대해 즉시 compaction을 수행합니다 (운영 시간에는 부하 유발 가능).
Compaction vs Page Migration 비교
Compaction과 Page Migration은 모두 migrate_pages()를 사용하지만, 목적과 대상이 다릅니다.
| 비교 항목 | Compaction | Page Migration |
|---|---|---|
| migrate_reason | MR_COMPACTION | MR_NUMA_MISPLACED, MR_SYSCALL 등 |
| 대상 선택 | Two-Scanner 자동 선택 | NUMA fault, 사용자 mbind, cpuset |
| 목표 위치 | free scanner가 찾은 빈 위치 (동일 존) | 다른 NUMA 노드의 빈 위치 |
| syscall | compact_memory sysctl | migrate_pages(2), move_pages(2) |
| GFP 플래그 | GFP_KERNEL 기반 | 호출자 지정 |
| 최대 재시도 | priority 3단계 | 보통 1~10회 |
커널 CONFIG 옵션
| CONFIG 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_COMPACTION | y | Memory Compaction 전체 활성화/비활성화 |
CONFIG_MIGRATION | y | Page Migration 지원 (compaction의 전제 조건) |
CONFIG_TRANSPARENT_HUGEPAGE | y | THP 지원 (compaction의 주요 소비자) |
CONFIG_CMA | y (ARM 등) | CMA 지원 (MIGRATE_CMA 타입 활성화) |
CONFIG_CMA_DEBUG | n | CMA 디버깅 (UNMOVABLE 오염 감지) |
CONFIG_MEMORY_HOTPLUG | 가변 | 메모리 핫플러그 (compaction 인프라 활용) |
CONFIG_NUMA | 가변 | NUMA 지원 (노드별 kcompactd) |
CONFIG_PAGE_REPORTING | n | 빈 페이지 보고 (가상화(Virtualization) 환경에서 활용) |
의존성 관계
# CONFIG_COMPACTION의 의존성
config COMPACTION
bool "Allow for memory compaction"
depends on MMU
select MIGRATION
help
Allows the compaction of memory for the allocation of huge pages.
# CONFIG_MIGRATION은 CONFIG_COMPACTION에 의해 자동 선택됨
config MIGRATION
bool
depends on MMU
설명
CONFIG_COMPACTION을 활성화하면 CONFIG_MIGRATION이 자동 선택됩니다.
CONFIG_COMPACTION=n으로 빌드하면 compact_zone(), kcompactd, direct compaction 경로가 모두 제거됩니다.
임베디드 시스템에서 THP가 불필요하면 compaction을 비활성화하여 커널 코드 크기와 런타임 오버헤드(Overhead)를 줄일 수 있습니다.
운영 플레이북
실제 운영 환경에서 compaction 관련 문제를 진단하고 해결하는 단계별 절차입니다.
1단계: 문제 진단
#!/bin/bash
# compaction 건강 상태 종합 진단 스크립트
echo "=== 1. Buddy Allocator 상태 ==="
cat /proc/buddyinfo
echo ""
echo "=== 2. Pagetypeinfo (migrate type별 분포) ==="
cat /proc/pagetypeinfo | head -30
echo ""
echo "=== 3. Compaction 카운터 ==="
grep ^compact /proc/vmstat
echo ""
echo "=== 4. 단편화 인덱스 ==="
cat /sys/kernel/debug/extfrag/extfrag_index 2>/dev/null || echo "debugfs 미마운트"
echo ""
echo "=== 5. THP defrag 모드 ==="
cat /sys/kernel/mm/transparent_hugepage/defrag
echo ""
echo "=== 6. Proactive compaction 설정 ==="
cat /proc/sys/vm/compaction_proactiveness
cat /proc/sys/vm/extfrag_threshold
echo ""
echo "=== 7. 성공률 계산 ==="
SUCCESS=$(awk '/compact_success/ {print $2}' /proc/vmstat)
FAIL=$(awk '/compact_fail/ {print $2}' /proc/vmstat)
STALL=$(awk '/compact_stall/ {print $2}' /proc/vmstat)
TOTAL=$((SUCCESS + FAIL))
if [ $TOTAL -gt 0 ]; then
RATE=$((SUCCESS * 100 / TOTAL))
echo "compact_success=$SUCCESS compact_fail=$FAIL compact_stall=$STALL"
echo "성공률: ${RATE}%"
else
echo "compaction 이력 없음"
fi
시나리오 1: compact_stall 급증
compact_stall이 초당 수십 건 증가, 애플리케이션 지연 증가
# 원인 분석
# 1. THP defrag 모드 확인 -- always이면 원인일 가능성 높음
cat /sys/kernel/mm/transparent_hugepage/defrag
# 2. 즉시 조치: THP defrag 낮추기
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# 3. proactive compaction 강화 (장기 대책)
echo 50 > /proc/sys/vm/compaction_proactiveness
# 4. 모니터링: stall 감소 확인
watch -n 5 'awk "/compact_stall/ {print \$2}" /proc/vmstat'
시나리오 2: compact_fail 비율 높음 (50% 이상 실패)
# 원인 분석: UNMOVABLE 오염 확인
cat /proc/pagetypeinfo | grep -E "Unmovable|Movable|Reclaimable"
# Unmovable 블록이 과도하게 많으면 문제
# 1. 어떤 slab이 UNMOVABLE을 많이 사용하는지 확인
cat /proc/slabinfo | sort -k3 -rn | head -20
# 2. extfrag_threshold 낮추기 (더 적극적으로 compaction 시도)
echo 300 > /proc/sys/vm/extfrag_threshold
# 3. 수동 compaction 시도
echo 1 > /proc/sys/vm/compact_memory
# 4. 장기 대책: min_free_kbytes 증가 (예비 메모리 확보)
echo 262144 > /proc/sys/vm/min_free_kbytes
시나리오 3: CMA 할당 실패
# CMA 상태 확인
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB
# CmaFree: 12288 kB <-- 매우 낮으면 문제
# CMA 디버깅 (CONFIG_CMA_DEBUG 필요)
dmesg | grep -i cma
# ftrace로 CMA 마이그레이션 추적
echo 1 > /sys/kernel/debug/tracing/events/migrate/enable
echo 1 > /sys/kernel/debug/tracing/events/compaction/enable
cat /sys/kernel/debug/tracing/trace_pipe &
# CMA 할당 트리거하여 로그 확인
지속 모니터링 설정
# Prometheus node_exporter 커스텀 텍스트 파일 생성
cat > /etc/node_exporter/compaction.prom << 'EOF'
#!/bin/bash
SUCCESS=$(awk '/compact_success/ {print $2}' /proc/vmstat)
FAIL=$(awk '/compact_fail/ {print $2}' /proc/vmstat)
STALL=$(awk '/compact_stall/ {print $2}' /proc/vmstat)
DAEMON=$(awk '/compact_daemon_wake/ {print $2}' /proc/vmstat)
echo "# HELP node_compaction_success_total Total compaction successes"
echo "# TYPE node_compaction_success_total counter"
echo "node_compaction_success_total $SUCCESS"
echo "# HELP node_compaction_fail_total Total compaction failures"
echo "# TYPE node_compaction_fail_total counter"
echo "node_compaction_fail_total $FAIL"
echo "# HELP node_compaction_stall_total Total direct compaction stalls"
echo "# TYPE node_compaction_stall_total counter"
echo "node_compaction_stall_total $STALL"
echo "# HELP node_compaction_daemon_wake_total kcompactd wakeups"
echo "# TYPE node_compaction_daemon_wake_total counter"
echo "node_compaction_daemon_wake_total $DAEMON"
EOF
chmod +x /etc/node_exporter/compaction.prom
고급: Compaction 내부 최적화
커널은 compaction 효율을 높이기 위해 여러 최적화를 적용합니다.
Pageblock Skip 비트
이미 스캔한 pageblock을 다시 방문하지 않도록 PB_migrate_skip 비트를 설정합니다:
/* mm/compaction.c - skip 비트 관리 (단순화) */
static void update_pageblock_skip(struct compact_control *cc,
struct page *page,
unsigned long pfn)
{
if (cc->no_set_skip_hint)
return;
/* 이 pageblock에 이동 가능 페이지가 없으면 skip 표시 */
set_pageblock_skip(page);
/* 스캐너 캐시 위치 갱신 */
if (pfn > cc->zone->compact_cached_migrate_pfn[cc->sync])
cc->zone->compact_cached_migrate_pfn[cc->sync] = pfn;
}
Deferred Compaction
연속된 compaction 실패 시 지수적 백오프(exponential backoff)로 재시도 간격을 늘립니다:
/* mm/compaction.c - deferred compaction 로직 (단순화) */
static bool compaction_deferred(struct zone *zone, int order)
{
unsigned int defer_shift = zone->compact_defer_shift;
unsigned int max_retries = 1UL << defer_shift;
if (zone->compact_considered < max_retries) {
zone->compact_considered++;
return true; /* 아직 연기 중 */
}
return false; /* 충분히 기다림, 재시도 허용 */
}
/* compaction 실패 시 defer_shift 증가 */
static void defer_compaction(struct zone *zone, int order)
{
zone->compact_considered = 0;
zone->compact_defer_shift++;
if (zone->compact_defer_shift > COMPACT_MAX_DEFER_SHIFT)
zone->compact_defer_shift = COMPACT_MAX_DEFER_SHIFT;
/* COMPACT_MAX_DEFER_SHIFT = 6 => 최대 64회 건너뜀 */
}
설명
compact_defer_shift는 실패마다 1씩 증가하여, 재시도까지 건너뛰는 횟수가 2배로 늘어납니다(1, 2, 4, 8, ..., 최대 64).
성공하면 compact_defer_shift가 0으로 리셋됩니다. 이 메커니즘은 반복적으로 실패하는 존에서 CPU 낭비를 방지합니다.
비동기 vs 동기 모드
| 속성 | ASYNC | SYNC_LIGHT | SYNC_FULL |
|---|---|---|---|
| 잠금 경쟁 시 | 즉시 포기 | 일부 대기 | 대기 후 재시도 |
| Writeback 페이지 | 건너뜀 | 건너뜀 | 완료 대기 |
| 스캔 범위 | 제한적 | 중간 | 전체 존 |
| 사용 컨텍스트 | kcompactd | direct (1차) | direct (마지막 시도) |
| need_resched() 확인 | 확인 후 양보(Yield) | 확인 후 양보 | 확인하지만 계속 |
Compaction 진화 역사
| 커널 버전 | 주요 변경 | 배경 |
|---|---|---|
| v2.6.35 | Memory Compaction 최초 도입 | Mel Gorman, lumpy reclaim 대체 |
| v3.3 | 비동기 compaction 추가 | direct compaction 지연 감소 |
| v3.5 | 스캐너 위치 캐싱 | 반복 스캔 비용 절감 |
| v3.18 | kcompactd 도입 | 백그라운드 compaction 데몬 |
| v4.6 | skip 비트 개선 | 불필요한 재스캔 방지 |
| v4.8 | deferred compaction 개선 | 지수적 백오프 |
| v5.1 | COMPACT_PRIO_SYNC_LIGHT 추가 | 3단계 priority 체계 |
| v5.9 | Proactive Compaction 도입 | Vlastimil Babka, 사전 예방적 접근 |
| v5.13 | Folio 기반 마이그레이션 시작 | compound page 지원 개선 |
| v6.0+ | per-zone proactive scoring | 존별 단편화 점수 세분화 |
성능 특성과 오버헤드
Compaction 비용 분석
| 비용 요소 | 크기 | 영향 |
|---|---|---|
| 페이지 복사 | 4KB memcpy per page | 대역폭(Bandwidth) 소비, 캐시 오염 |
| PTE 갱신 | rmap walk per page | fork 많은 환경에서 비용 증가 |
| TLB flush | IPI per migration batch | CPU 수 비례 오버헤드 |
| LRU lock | zone->lru_lock 경쟁 | 다수 CPU 동시 접근 시 병목(Bottleneck) |
| 존 스캔 | pageblock 순회 | 존 크기에 비례 |
# compaction 비용 측정 (perf)
perf stat -e 'compaction:*' -a sleep 60
# mm_compaction_begin: 42
# mm_compaction_end: 42
# mm_compaction_migratepages: 42 (nr_migrated=12345, nr_failed=678)
# compaction 함수별 CPU 시간
perf top -g -p $(pgrep kcompactd)
# compact_zone [kernel] 15.2%
# isolate_migratepages [kernel] 8.7%
# migrate_pages [kernel] 6.3%
# unmap_and_move [kernel] 4.1%
- ASYNC 모드: 1~50ms (스캔 범위에 따라)
- SYNC_LIGHT: 10~200ms
- SYNC_FULL: 50~500ms (worst case, 대용량 존)
NUMA 환경에서의 Compaction
NUMA 시스템에서는 각 노드마다 독립적인 kcompactd 스레드(Thread)가 동작하며, 존별로 독립적인 compaction을 수행합니다.
# NUMA 노드별 kcompactd 확인
ps -eo pid,comm | grep kcompactd
# 34 kcompactd0 <-- Node 0
# 35 kcompactd1 <-- Node 1
# 노드별 buddyinfo 확인
cat /proc/buddyinfo
# Node 0, zone Normal 312 156 78 39 12 3 1 0 0 0 0
# Node 1, zone Normal 456 228 114 57 28 14 7 3 1 0 0
# Node 1의 단편화가 더 심함 -> kcompactd1이 더 활발히 동작
compact_stall만 증가합니다. /proc/vmstat은 전체 합산이므로, 노드별 분석이 필요하면 /sys/devices/system/node/node*/vmstat을 확인하세요.
Compaction 안티패턴
안티패턴 1: UNMOVABLE 페이지 확산
/* 잘못된 패턴: GFP_KERNEL로 대량 kmalloc */
/* GFP_KERNEL은 MIGRATE_UNMOVABLE로 할당됨 */
for (i = 0; i < 10000; i++)
buffers[i] = kmalloc(PAGE_SIZE, GFP_KERNEL);
/* -> 다수의 pageblock이 UNMOVABLE로 오염 */
/* 개선: slab 대신 vmalloc 또는 alloc_pages(GFP_HIGHUSER_MOVABLE) 활용 */
/* 또는 메모리 풀 사전 할당으로 UNMOVABLE 확산 제어 */
안티패턴 2: THP always + 지연 민감 워크로드
# 잘못된 설정: DB 서버에서 THP always
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo always > /sys/kernel/mm/transparent_hugepage/defrag
# -> 모든 mmap에서 direct compaction 발생 가능
# 올바른 설정: DB 서버
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
# -> 명시적 MADV_HUGEPAGE 영역에서만 THP/compaction 시도
안티패턴 3: compact_memory 남용
# 잘못된 cron:
# */5 * * * * echo 1 > /proc/sys/vm/compact_memory
# -> 5분마다 전체 존 강제 compaction, 시스템 부하 급증
# 올바른 대안: proactive compaction 활용
echo 40 > /proc/sys/vm/compaction_proactiveness
# -> 커널이 필요할 때만 자동으로 적절한 수준의 compaction 수행
핵심 자료 구조
/* include/linux/compaction.h - struct compact_control */
struct compact_control {
struct list_head freepages; /* 격리된 빈 페이지 리스트 */
struct list_head migratepages; /* 격리된 이동 대상 페이지 리스트 */
unsigned int nr_freepages; /* freepages 리스트 크기 */
unsigned int nr_migratepages; /* migratepages 리스트 크기 */
unsigned long free_pfn; /* free scanner 현재 위치 */
unsigned long migrate_pfn; /* migrate scanner 현재 위치 */
struct zone *zone; /* 대상 존 */
unsigned long total_migrate_scanned; /* 총 migrate scanner 스캔 수 */
unsigned long total_free_scanned; /* 총 free scanner 스캔 수 */
int order; /* 요청된 할당 order */
gfp_t gfp_mask; /* 할당 GFP 플래그 */
int highest_zoneidx; /* 최고 존 인덱스 */
enum migrate_mode mode; /* MIGRATE_ASYNC/SYNC_LIGHT/SYNC */
bool whole_zone; /* 전체 존 스캔 여부 */
bool contended; /* 잠금 경쟁 발생 여부 */
bool finish_pageblock; /* 현재 pageblock 완료 여부 */
bool alloc_contig; /* alloc_contig_range 사용 여부 */
int migratetype; /* 대상 migrate type */
enum compact_result result; /* compaction 결과 */
};
설명
struct compact_control은 하나의 compaction 세션 전체를 제어하는 핵심 구조체(Struct)입니다.
freepages/migratepages 리스트는 격리 단계에서 수집된 페이지들을 담고, migrate_pages()에서 소비됩니다.
free_pfn/migrate_pfn은 두 스캐너의 현재 위치를 추적합니다.
mode는 비동기/동기 모드를 결정하며, contended는 잠금 경쟁 발생 시 조기 종료를 위해 사용됩니다.
Folio 기반 Compaction 전환
커널 v5.13부터 시작된 folio 전환은 compaction 경로에도 영향을 미칩니다. struct folio는 compound page를 직접 표현하여 THP compaction의 효율성을 높입니다.
/* mm/migrate.c - folio 기반 마이그레이션 (v6.0+, 단순화) */
static int migrate_folio_unmap(new_folio_t get_new_folio,
free_folio_t put_new_folio,
unsigned long private,
struct folio *src, struct folio *dst,
enum migrate_mode mode, enum migrate_reason reason,
struct list_head *ret)
{
/* folio 잠금 */
if (!folio_trylock(src))
return -EAGAIN;
/* rmap으로 모든 매핑 제거 */
try_to_migrate(src, TTU_BATCH_FLUSH);
/* migration entry 설치 완료 확인 */
if (folio_mapped(src)) {
/* 아직 매핑 남아있으면 복구 */
remove_migration_ptes(src, src, false);
folio_unlock(src);
return -EAGAIN;
}
return MIGRATEPAGE_UNMAP;
}
static int migrate_folio_move(free_folio_t put_new_folio,
unsigned long private,
struct folio *src, struct folio *dst,
enum migrate_mode mode, enum migrate_reason reason,
struct list_head *ret)
{
/* 실제 페이지 내용 복사 */
folio_copy(dst, src);
/* 메타데이터 이전 */
migrate_folio_undo_src(src, ...);
migrate_folio_done(src, ...);
/* 새 folio로 PTE 교체 */
remove_migration_ptes(src, dst, false);
return MIGRATEPAGE_SUCCESS;
}
설명
folio 기반 마이그레이션은 두 단계(unmap + move)로 분리되어, batch processing이 가능합니다. 여러 folio의 unmap을 먼저 수행하고 TLB flush를 한 번에 처리한 후, 순차적으로 move를 수행하여 IPI 오버헤드를 줄입니다. THP(order-9 folio)의 경우 512 페이지를 한 번의folio_copy()로 처리할 수 있어 compound page 분해/재조립 비용이 없습니다.
Batch TLB Flush 최적화
folio 기반 마이그레이션에서는 여러 페이지의 unmap을 모아서 TLB flush를 batch로 처리합니다:
/* unmap 단계에서 TTU_BATCH_FLUSH 사용 */
try_to_migrate(src, TTU_BATCH_FLUSH);
/* -> TLB flush를 즉시 수행하지 않고 지연 */
/* 모든 folio의 unmap 완료 후 한 번에 flush */
try_to_unmap_flush();
/* -> arch_tlbbatch_flush()로 IPI 최소화 */
compact_zone() 호출 체인 분석
페이지 할당 실패(또는 kcompactd 깨어남)가 발생하면 커널은 다단계 호출 체인을 통해 compact_zone()에 도달합니다. 이 체인을 추적하면 compaction이 언제, 왜, 어떤 경로로 진입하는지를 정확히 파악할 수 있습니다.
/* mm/compaction.c - try_to_compact_pages(): zonelist를 순회하며 compaction 수행 */
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 **page)
{
int zoneid;
struct zoneref *z;
struct zone *zone;
enum compact_result rc = COMPACT_SKIPPED;
/* zonelist의 모든 zone을 순회 */
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist,
ac->highest_zoneidx, ac->nodemask) {
enum compact_result status;
/* zone별 compaction 수행 */
status = compact_zone_order(zone, order, gfp_mask,
prio, alloc_flags,
ac->highest_zoneidx, page);
/* 성공하면 즉시 반환 */
if (status == COMPACT_SUCCESS) {
rc = COMPACT_SUCCESS;
break;
}
rc = max(status, rc); /* 가장 진행된 결과 보존 */
}
return rc;
}
코드 설명
- 1~5행
try_to_compact_pages()는 할당 컨텍스트(alloc_context)에서 전달받은 zonelist를 순회합니다.prio는 ASYNC/SYNC_LIGHT/SYNC_FULL 중 하나로, 시도 강도를 결정합니다. - 12~14행
for_each_zone_zonelist_nodemask()매크로로 NUMA 노드마스크(Nodemask)를 고려하여 적합한 zone을 순회합니다. 할당 요청의highest_zoneidx보다 높은 zone은 건너뜁니다. - 18~20행
compact_zone_order()를 호출하여 해당 zone에서 compaction을 수행합니다. 이 함수가struct compact_control을 초기화하고compact_zone()을 호출합니다. - 23~26행한 zone에서라도
COMPACT_SUCCESS가 반환되면 루프를 종료합니다. 실패 시 가장 진행된 결과(max)를 보존하여 호출자가 재시도 여부를 판단할 수 있게 합니다.
/* mm/compaction.c - compact_zone_order(): compact_control 초기화 */
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,
.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 == COMPACT_PRIO_SYNC_FULL),
};
/* 적합성 사전 확인 */
if (compaction_deferred(zone, order))
return COMPACT_DEFERRED;
/* 핵심 compaction 루프 실행 */
return compact_zone(&cc);
}
코드 설명
- 8~18행
struct compact_control을 스택에 초기화합니다.order는 요청된 할당 크기,mode는 priority에 따라 ASYNC 또는 SYNC_LIGHT로 설정됩니다.whole_zone이 true이면 전체 zone을 처음부터 끝까지 스캔합니다. - 21~22행
compaction_deferred()로 최근 연속 실패에 의한 지수적 백오프(Exponential Backoff)가 적용 중인지 확인합니다. 아직 대기 중이면COMPACT_DEFERRED를 반환하여 불필요한 CPU 소모를 방지합니다. - 25행모든 사전 검사를 통과하면
compact_zone()을 호출하여 실제 Two-Scanner 알고리즘을 실행합니다. 반환된compact_result가 그대로 호출자에게 전달됩니다.
compact_zone() 함수 구현 분석
compact_zone()은 Memory Compaction의 핵심 루프를 담당합니다. migrate scanner와 free scanner를 교대로 실행하면서, 이동 가능한 페이지를 빈 페이지 위치로 마이그레이션합니다. 이 함수의 상세 흐름을 분석합니다.
/* mm/compaction.c - compact_zone() 핵심 루프 (단순화된 ~35줄) */
static enum compact_result compact_zone(struct compact_control *cc)
{
enum compact_result ret;
unsigned long start_pfn = cc->zone->zone_start_pfn;
unsigned long end_pfn = zone_end_pfn(cc->zone);
int err;
/* 스캐너 시작 위치 설정 (캐시 또는 zone 시작/끝) */
cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[cc->sync];
cc->free_pfn = cc->zone->compact_cached_free_pfn;
/* whole_zone 모드이면 캐시 무시, zone 전체 스캔 */
if (cc->whole_zone) {
cc->migrate_pfn = start_pfn;
cc->free_pfn = end_pfn;
}
migrate_prep(); /* LRU 드레인 등 사전 준비 */
/* 메인 compaction 루프 */
while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
/* 1단계: migrate scanner로 이동 가능 페이지 격리 */
isolate_migratepages(cc);
if (cc->nr_migratepages == 0) {
/* 이동할 페이지가 없으면 다음 pageblock으로 */
if (cc->direct_compaction && need_resched())
break; /* 비동기 모드: 스케줄러에 양보 */
continue;
}
/* 2단계: migrate_pages()로 실제 마이그레이션 수행 */
err = migrate_pages(&cc->migratepages,
compaction_alloc,
compaction_free,
(unsigned long)cc,
cc->mode, MR_COMPACTION, NULL);
/* 마이그레이션 실패한 페이지를 LRU로 복귀 */
if (err) {
putback_movable_pages(&cc->migratepages);
cc->nr_migratepages = 0;
}
/* 잠금 경쟁 발생 시 조기 종료 */
if (cc->contended)
break;
}
/* 스캐너 위치 캐시 갱신 */
cc->zone->compact_cached_migrate_pfn[cc->sync] = cc->migrate_pfn;
cc->zone->compact_cached_free_pfn = cc->free_pfn;
return ret;
}
코드 설명
- 10~11행이전 compaction에서 캐시된 스캐너 위치를 복원합니다.
compact_cached_migrate_pfn은 ASYNC/SYNC별로 별도 캐시를 유지하여 서로 간섭하지 않습니다. - 14~17행
whole_zone모드(SYNC_FULL priority)에서는 캐시를 무시하고 zone의 처음부터 끝까지 전체를 스캔합니다. 최후의 수단으로 사용됩니다. - 19행
migrate_prep()은 per-CPU LRU 캐시를 드레인(Drain)하여, 이후isolate_migratepages()에서 누락되는 페이지를 최소화합니다. - 22행
compact_finished()가COMPACT_CONTINUE를 반환하는 한 루프를 계속합니다. 이 함수는 두 스캐너가 만났는지(migrate_pfn >= free_pfn), 요청 order 블록이 확보되었는지를 확인합니다. - 24행
isolate_migratepages()는 내부적으로isolate_migratepages_block()을 호출하여 pageblock 단위로 이동 가능 페이지를cc->migratepages리스트에 수집합니다. - 33~38행
migrate_pages()의compaction_alloc콜백은 free scanner가 격리한 빈 페이지를 대상 페이지로 제공합니다.compaction_free는 마이그레이션 실패 시 빈 페이지를 반환하는 콜백입니다. - 41~44행마이그레이션에 실패한 페이지(잠금 경쟁, writeback 중 등)는
putback_movable_pages()로 원래 LRU 리스트에 되돌립니다. - 51~52행루프 종료 시 현재 스캐너 위치를 zone의 캐시에 저장합니다. 다음 compaction은 이 위치부터 재개하여 이미 처리한 영역을 다시 스캔하지 않습니다.
Dual Scanner 구현 상세: isolate_migratepages + isolate_freepages
Two-Scanner 알고리즘의 두 축인 isolate_migratepages()와 isolate_freepages()는 서로 독립적으로 동작하면서, struct compact_control의 리스트를 통해 데이터를 교환합니다.
isolate_migratepages() 구현
/* mm/compaction.c - migrate scanner: 이동 가능 페이지 수집 */
static isolate_migrate_t isolate_migratepages(
struct compact_control *cc)
{
unsigned long block_start_pfn;
unsigned long block_end_pfn;
unsigned long low_pfn = cc->migrate_pfn;
/* pageblock 단위로 순회 */
for (; low_pfn < cc->free_pfn;
low_pfn = block_end_pfn) {
block_start_pfn = pageblock_start_pfn(low_pfn);
block_end_pfn = pageblock_end_pfn(low_pfn);
/* skip 비트가 설정된 pageblock 건너뛰기 */
if (!cc->finish_pageblock &&
get_pageblock_skip(pfn_to_page(block_start_pfn)))
continue;
/* pageblock의 migrate type 확인 */
if (!suitable_migration_source(cc, pfn_to_page(block_start_pfn)))
continue;
/* 이 pageblock에서 이동 가능 페이지 격리 */
isolate_migratepages_block(cc, low_pfn, block_end_pfn,
ISOLATE_UNEVICTABLE);
/* 충분한 페이지를 수집했으면 중단 */
if (cc->nr_migratepages >= COMPACT_CLUSTER_MAX)
break;
}
cc->migrate_pfn = low_pfn; /* 스캐너 위치 갱신 */
return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE;
}
코드 설명
- 10~11행migrate scanner는
cc->migrate_pfn부터cc->free_pfn(free scanner 위치)까지 pageblock 단위로 순회합니다. 두 스캐너가 만나면 자연스럽게 루프가 종료됩니다. - 17~19행
PB_migrate_skip비트가 설정된 pageblock은 이미 이전 compaction에서 처리되었으므로 건너뜁니다. 단,finish_pageblock이 true(SYNC_FULL)이면 skip 비트를 무시합니다. - 22~23행
suitable_migration_source()는 pageblock의 migrate type이 UNMOVABLE이 아닌지, CMA 영역인지 등을 확인합니다. UNMOVABLE pageblock의 페이지는 이동할 수 없으므로 건너뜁니다. - 30~31행
COMPACT_CLUSTER_MAX(기본 32)개의 페이지를 수집하면 중단합니다. 한 번에 너무 많은 페이지를 격리하면 LRU 잠금 보유 시간이 길어져 다른 경로에 영향을 줍니다. - 34행현재 스캔 위치를
cc->migrate_pfn에 기록하여, 다음 루프 반복에서 이어서 스캔합니다.
isolate_freepages() 구현
/* mm/compaction.c - free scanner: 빈 페이지 수집 (단순화) */
static void isolate_freepages(struct compact_control *cc)
{
unsigned long block_start_pfn, block_end_pfn;
unsigned long isolate_start_pfn;
/* zone 끝(높은 PFN)부터 아래로 스캔 */
for (block_start_pfn = pageblock_start_pfn(cc->free_pfn);
block_start_pfn >= cc->migrate_pfn;
block_start_pfn -= pageblock_nr_pages) {
block_end_pfn = block_start_pfn + pageblock_nr_pages;
/* skip 비트 확인 */
if (get_pageblock_skip(pfn_to_page(block_start_pfn)))
continue;
/* Buddy에서 빈 페이지 격리 */
isolate_start_pfn = block_start_pfn;
isolate_freepages_block(cc, &isolate_start_pfn,
block_end_pfn, true);
/* 충분한 빈 페이지를 확보했으면 중단 */
if (cc->nr_freepages >= cc->nr_migratepages)
break;
}
cc->free_pfn = block_start_pfn; /* free scanner 위치 갱신 */
}
코드 설명
- 8~10행free scanner는
cc->free_pfn(zone의 높은 PFN 쪽)부터cc->migrate_pfn(migrate scanner 위치)까지 역방향으로 pageblock 단위로 스캔합니다. - 20~21행
isolate_freepages_block()은 Buddy Allocator의 free_area에서 빈 페이지를 분리하여cc->freepages리스트에 추가합니다. Buddy의 freelist에서 제거되므로 다른 할당자가 이 페이지를 사용할 수 없습니다. - 24~25행격리된 빈 페이지 수(
nr_freepages)가 이동 대상 페이지 수(nr_migratepages) 이상이면 충분하므로 스캔을 중단합니다. 필요 이상의 빈 페이지를 격리하면 Buddy의 가용 메모리가 줄어들어 다른 할당에 영향을 줍니다.
struct compact_control 필드별 해설
struct compact_control은 하나의 compaction 세션 전체를 제어합니다. 각 필드의 역할과 생명주기를 정확히 이해하면 커널 소스를 추적할 때 큰 도움이 됩니다.
/* include/linux/compaction.h - struct compact_control 필드별 해설 */
struct compact_control {
/* === 페이지 리스트 (격리 단계 산출물) === */
struct list_head freepages; /* free scanner가 격리한 빈 페이지 */
struct list_head migratepages; /* migrate scanner가 격리한 이동 대상 */
/* === 카운터 (리스트 크기 추적) === */
unsigned int nr_freepages; /* freepages 리스트의 현재 크기 */
unsigned int nr_migratepages; /* migratepages 리스트의 현재 크기 */
/* === 스캐너 위치 (PFN 단위) === */
unsigned long migrate_pfn; /* migrate scanner 현재 위치 */
unsigned long free_pfn; /* free scanner 현재 위치 */
/* === 통계 (디버깅/ftrace용) === */
unsigned long total_migrate_scanned; /* 총 스캔한 migrate 페이지 수 */
unsigned long total_free_scanned; /* 총 스캔한 free 페이지 수 */
/* === 대상 zone과 할당 정보 === */
struct zone *zone; /* compaction 대상 zone */
int order; /* 요청된 할당 order (-1=전체) */
gfp_t gfp_mask; /* 할당 GFP 플래그 */
int highest_zoneidx; /* 최고 허용 zone 인덱스 */
int migratetype; /* 대상 migrate type */
/* === 동작 모드 제어 === */
enum migrate_mode mode; /* ASYNC / SYNC_LIGHT / SYNC */
bool whole_zone; /* true: 캐시 무시, zone 전체 스캔 */
bool contended; /* true: 잠금 경쟁으로 조기 종료 */
bool finish_pageblock; /* true: 현재 pageblock 강제 완료 */
bool alloc_contig; /* true: alloc_contig_range 경로 */
bool direct_compaction; /* true: 할당 경로에서 호출 */
/* === 결과 === */
enum compact_result result; /* compaction 최종 결과 */
};
코드 설명
- freepages / migratepages두 리스트는 compaction의 핵심 데이터 흐름입니다.
isolate_migratepages()가migratepages에 페이지를 추가하면,migrate_pages()의compaction_alloc()콜백이freepages에서 대상 페이지를 꺼내 마이그레이션합니다. - migrate_pfn / free_pfn두 스캐너의 현재 위치(PFN)입니다.
migrate_pfn은 증가 방향,free_pfn은 감소 방향으로 이동합니다.compact_finished()에서migrate_pfn >= free_pfn이면 두 스캐너가 만난 것으로 판단합니다. - mode
MIGRATE_ASYNC: 잠금 경쟁 시 즉시 포기, writeback 페이지 건너뜀.MIGRATE_SYNC_LIGHT: 일부 대기 허용.MIGRATE_SYNC: writeback 완료 대기까지 수행. kcompactd는 ASYNC, direct compaction은 priority에 따라 단계적으로 강도를 높입니다. - contendedzone->lock이나 lru_lock 경쟁이 감지되면 true로 설정되어,
compact_zone()의 메인 루프가 조기 종료됩니다. 높은 부하 상황에서 compaction이 다른 경로를 blocking하는 것을 방지합니다. - alloc_contig / direct_compaction
alloc_contig가 true이면 CMAalloc_contig_range()경로에서 호출된 것으로, UNMOVABLE 페이지도 이동 대상에 포함할 수 있습니다.direct_compaction은 페이지 할당 slow path에서 직접 호출된 경우를 나타냅니다.
migrate_pages() 경로 분석
migrate_pages()는 격리된 페이지 리스트를 순회하면서 각 페이지를 새 위치로 복사하고, PTE(Page Table Entry)를 갱신하며, 원본을 해제합니다. v6.0 이후 folio 기반으로 전환되면서 unmap과 move가 분리되어 batch 처리가 가능해졌습니다.
/* mm/migrate.c - migrate_pages() 메인 루프 (v6.0+ 단순화) */
int migrate_pages(struct list_head *from,
new_folio_t get_new_folio,
free_folio_t put_new_folio,
unsigned long private,
enum migrate_mode mode,
int reason, struct list_head *ret_folios)
{
struct folio *src, *dst, *src2;
int rc, nr_failed = 0, nr_succeeded = 0;
int pass = 0;
LIST_HEAD(unmap_folios); /* unmap 완료된 folio 리스트 */
LIST_HEAD(dst_folios); /* 대상 folio 리스트 */
retry:
list_for_each_entry_safe(src, src2, from, lru) {
/* 대상 folio 할당 (compaction_alloc 콜백) */
dst = get_new_folio(src, private);
if (!dst) {
nr_failed++;
continue;
}
/* Phase 1: unmap (migration entry 설치) */
rc = migrate_folio_unmap(get_new_folio, put_new_folio,
private, src, dst, mode, reason,
&unmap_folios);
if (rc == MIGRATEPAGE_UNMAP) {
list_move_tail(&dst->lru, &dst_folios);
continue; /* 다음 folio의 unmap으로 진행 */
}
}
/* batch TLB flush: 모든 unmap을 한 번에 flush */
try_to_unmap_flush();
/* Phase 2: move (folio 내용 복사 + PTE 교체) */
list_for_each_entry_safe(src, src2, &unmap_folios, lru) {
dst = list_first_entry(&dst_folios, struct folio, lru);
rc = migrate_folio_move(put_new_folio, private,
src, dst, mode, reason, ret_folios);
if (rc == MIGRATEPAGE_SUCCESS)
nr_succeeded++;
}
/* 일부 실패 시 재시도 (최대 10회) */
if (!list_empty(from) && pass++ < 10)
goto retry;
return nr_failed;
}
코드 설명
- 12~13행
unmap_folios와dst_folios는 Phase 1(unmap)과 Phase 2(move) 사이의 버퍼 역할을 합니다. unmap이 완료된 folio를 모아두었다가 한꺼번에 move를 수행합니다. - 18~19행
get_new_folio콜백은 compaction 경로에서compaction_alloc()으로 설정됩니다. 이 함수는cc->freepages리스트에서 free scanner가 격리한 빈 페이지를 꺼내 반환합니다. - 25~28행
migrate_folio_unmap()은 rmap을 통해 원본 folio의 모든 PTE를 migration entry로 교체합니다.TTU_BATCH_FLUSH플래그로 TLB flush를 지연시킵니다. - 35행
try_to_unmap_flush()로 지연된 TLB flush를 한 번에 수행합니다. N개 folio의 unmap에 대해 IPI가 1~2회로 줄어듭니다. - 38~43행
migrate_folio_move()는move_to_new_folio()로 내용을 복사하고,remove_migration_ptes()로 migration entry를 새 folio의 PTE로 교체합니다. 이후 원본 folio는 Buddy에 반환됩니다. - 46~47행일시적 실패(잠금 경쟁, 참조 카운트 변동 등)를 위해 최대 10회 재시도합니다. 반복적으로 실패하는 페이지는
ret_folios에 남아 호출자가 처리합니다.
Proactive Compaction 커널 구현 상세
Proactive Compaction은 커널 v5.9에서 Nitin Gupta에 의해 도입되었습니다. kcompactd의 메인 루프에서 주기적으로 단편화 점수를 확인하고, 임계치를 초과하면 할당 실패 없이도 사전에 compaction을 수행합니다.
단편화 점수 계산과 워터마크
/* mm/compaction.c - proactive compaction 점수 계산 */
static unsigned int fragmentation_score_zone(struct zone *zone)
{
unsigned long score = 0;
unsigned long managed = zone_managed_pages(zone);
if (!managed)
return 0;
/* 각 order에 대해 외부 단편화 점수 가중 합산 */
for (int order = 1; order < MAX_ORDER; order++) {
int index;
struct contig_page_info info;
fill_contig_page_info(zone, order, &info);
index = __fragmentation_index(order, &info);
/* 음수(충분)이면 0으로 치환 */
if (index <= 0)
continue;
/* order가 클수록 가중치를 높여 합산 */
score += index;
}
return div_u64(score * 100, managed);
}
static unsigned int fragmentation_score_node(struct pg_data_t *pgdat)
{
unsigned int score = 0;
for (int zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
struct zone *zone = &pgdat->node_zones[zoneid];
score += fragmentation_score_zone_weighted(zone);
}
return score;
}
/* kcompactd 루프에서 호출되는 판단 함수 */
static bool should_proactive_compact_node(struct pg_data_t *pgdat)
{
int wmark_high;
if (!sysctl_compaction_proactiveness)
return false; /* 0이면 완전 비활성 */
/* wmark_low = 100 - proactiveness */
/* wmark_high = wmark_low + 10 */
wmark_high = (100 - sysctl_compaction_proactiveness) + 10;
return fragmentation_score_node(pgdat) > wmark_high;
}
코드 설명
- fragmentation_score_zone()zone 내 각 order(1~MAX_ORDER)에 대해
__fragmentation_index()를 호출하여 단편화 점수를 합산합니다. order가 높을수록(큰 블록일수록) 단편화의 영향이 크므로 가중치가 부여됩니다. - fragmentation_score_node()노드의 모든 zone에 대해 가중 점수를 합산합니다. 이 값이
wmark_high를 초과하면 proactive compaction이 트리거됩니다. - should_proactive_compact_node()
sysctl_compaction_proactiveness기본값 20에서:wmark_low = 80,wmark_high = 90. 노드 점수가 90을 초과하면 시작, 80 이하로 떨어지면 중지합니다. 값을 50으로 높이면wmark_high = 60이 되어 더 적극적으로 compaction합니다.
kcompactd의 proactive compaction 루프
/* mm/compaction.c - kcompactd에서 proactive compaction 수행 */
static void proactive_compact_node(struct pg_data_t *pgdat)
{
struct compact_control cc = {
.order = -1, /* 특정 order 없음, 전반적 최적화 */
.mode = MIGRATE_SYNC_LIGHT,
.whole_zone = true, /* 전체 zone 스캔 */
.gfp_mask = GFP_KERNEL,
};
int zoneid;
for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
struct zone *zone = &pgdat->node_zones[zoneid];
if (!populated_zone(zone))
continue;
cc.zone = zone;
compact_zone(&cc);
/* 점수가 wmark_low 이하로 내려가면 중단 */
if (fragmentation_score_zone(zone) <=
(100 - sysctl_compaction_proactiveness))
continue; /* 이 zone은 충분히 정리됨 */
}
}
코드 설명
- 5행
order = -1은 특정 order의 블록 확보가 목적이 아니라, zone 전반의 단편화를 낮추는 것이 목표임을 나타냅니다.compact_finished()에서 order 기반 종료 조건을 건너뜁니다. - 6~7행
MIGRATE_SYNC_LIGHT모드와whole_zone = true를 사용합니다. 백그라운드 작업이므로 SYNC_FULL까지는 사용하지 않지만, ASYNC보다는 적극적으로 페이지를 이동합니다. - 21~23행각 zone의 compaction 후 단편화 점수를 재확인합니다.
wmark_low이하로 떨어진 zone은 추가 작업이 필요 없으므로 건너뜁니다. 이 히스테리시스(Hysteresis) 설계로 과도한 compaction을 방지합니다.
/proc/sys/vm/compaction_proactiveness 값은 워크로드에 따라 조정합니다.
- THP 집약 서버 (HPC, VM): 40~60으로 설정하여 order-9 블록을 사전 확보
- 지연 민감 서버 (DB, RT): 20~30으로 유지하되,
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag로 THP 트리거를 제한 - compaction 오버헤드 최소화: 0으로 설정하여 proactive compaction 완전 비활성화
참고자료
커널 문서
- Memory Management Admin Guide -- 메모리 관리 관리자 가이드입니다
- Page Migration -- 페이지 마이그레이션 메커니즘을 설명합니다
LWN 기사
- Memory compaction (Mel Gorman, 2010) -- 메모리 compaction의 기본 개념과 설계를 소개합니다
- Proactive compaction (2012) -- 선제적 compaction에 대한 초기 논의입니다
- Making kernel pages movable (2016) -- 커널 페이지를 이동 가능하게 만드는 방법을 다룹니다
- Proactive compaction for the kernel (2020) -- 커널의 선제적 compaction 구현을 설명합니다
커널 소스
- mm/compaction.c -- compaction 핵심 구현 코드입니다
- mm/page_isolation.c -- 페이지 격리 코드입니다
- mm/migrate.c -- 페이지 마이그레이션 구현입니다
- include/linux/compaction.h -- compaction API 헤더 파일입니다
서적
- Mel Gorman, Understanding the Linux Virtual Memory Manager -- 물리 페이지 관리에 대한 참조 자료입니다