페이지 할당자 (Buddy Allocator)
Linux 커널의 Buddy Allocator는 물리 메모리를 페이지 단위(4KB)로 관리하는 1차 할당자입니다. 2^n 블록 할당, Buddy Coalescing, Zone 관리, GFP 플래그 체계를 종합적으로 다룹니다.
핵심 요약
- 2^n 블록 — 1, 2, 4, 8, ..., 1024 페이지 단위로 관리합니다.
- Buddy Coalescing — 인접한 빈 블록을 자동으로 병합하여 외부 단편화를 줄입니다.
- Zone 기반 관리 — ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM으로 물리 메모리를 구분합니다.
- GFP 플래그 — GFP_KERNEL, GFP_ATOMIC 등으로 할당 정책을 제어합니다.
- 빠른 할당 — Per-CPU 페이지 캐시로 lock-free 할당을 제공합니다.
단계별 이해
- 핵심 요소 확인
이 문서에서 다루는 자료구조/API를 먼저 정리합니다. - 처리 흐름 추적
요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다. - 문제 지점 점검
실패 경로, 경합 구간, 성능 병목을 체크합니다.
개요 (Overview)
Buddy Allocator는 1963년 Kenneth C. Knowlton이 제안한 알고리즘을 기반으로 합니다. 리눅스 커널에서는 물리 메모리 관리의 1차 할당자(primary allocator)로 사용되며, mm/page_alloc.c에 핵심 구현이 있습니다. Slab Allocator, vmalloc, Huge Pages 등 모든 상위 할당자가 최종적으로 Buddy Allocator를 통해 물리 페이지를 확보합니다.
- 물리 메모리 조직 — Node → Zone → Free Lists (order 0~10)
- 할당 단위 — 페이지 프레임(4KB on x86, 16KB on ARM64 optional)
- 분할과 병합 — 큰 블록을 쪼개고(split), 작은 블록을 합침(coalesce)
- MAX_ORDER — 기본값 11(order 0~10), 최대 블록 크기 2^10 = 1024 pages = 4MB
- 이동성 그룹 — 외부 단편화 방지를 위해 페이지를 UNMOVABLE/MOVABLE/RECLAIMABLE로 분류
왜 Buddy인가? 2^n 주소 공간에서 같은 order의 인접 블록은 주소의 n+1번째 비트만 다릅니다. 이런 짝(buddy)을 빠르게 찾아 병합할 수 있어 "Buddy" Allocator라 불립니다. buddy의 주소는 XOR 연산 한 번으로 계산 가능하여 O(1) 시간에 합병 여부를 판단합니다.
Buddy Allocator의 시간 복잡도는 다음과 같습니다:
| 연산 | 시간 복잡도 | 설명 |
|---|---|---|
| 할당 (order 0, PCP hit) | O(1) | Per-CPU 캐시에서 즉시 할당 |
| 할당 (order n, 정확한 order) | O(1) | 해당 order free list에서 꺼냄 |
| 할당 (split 필요) | O(MAX_ORDER - n) | 상위 order에서 재귀적 분할 |
| 해제 + coalesce | O(MAX_ORDER - n) | buddy 확인 후 재귀적 합병 |
| Buddy 주소 계산 | O(1) | XOR 연산 한 번 |
자료구조 (Data Structures)
Free Area
Buddy Allocator의 핵심 자료구조는 free_area 배열입니다. 각 Zone은 MAX_ORDER개의 free_area 엔트리를 가지며, 각 엔트리는 해당 order의 빈 블록들을 Migrate Type별로 분리한 링크드 리스트로 관리합니다.
/* include/linux/mmzone.h */
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; /* migrate type별 리스트 */
unsigned long nr_free; /* 이 order의 총 free 블록 수 */
};
/* zone 구조체 내 free_area 배열 */
struct zone {
/* Watermarks */
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;
/* Lowmem reserve: 상위 zone 요청이 하위 zone을 고갈시키는 것 방지 */
long lowmem_reserve[MAX_NR_ZONES];
/* Per-CPU page cache */
struct per_cpu_pages __percpu *per_cpu_pageset;
/* Buddy free area: order 0~10 */
struct free_area free_area[MAX_ORDER];
/* Zone 통계 */
unsigned long zone_start_pfn; /* 시작 PFN */
unsigned long spanned_pages; /* 포함하는 전체 페이지 수 (구멍 포함) */
unsigned long present_pages; /* 실제 존재하는 페이지 수 */
unsigned long managed_pages; /* Buddy가 관리하는 페이지 수 */
const char *name; /* "DMA", "Normal" 등 */
/* ... */
};
메모리 Zone
리눅스 커널은 물리 메모리를 여러 Zone으로 분류합니다. 이는 하드웨어 제약(DMA 주소 범위)과 소프트웨어 정책(메모리 핫플러그)에 기반합니다. NUMA 시스템에서는 각 Node가 독립적인 Zone 집합을 가집니다.
| Zone | 범위 (x86_64) | 용도 | CONFIG 옵션 |
|---|---|---|---|
ZONE_DMA |
0 ~ 16MB | ISA 디바이스 DMA (24비트 주소) | CONFIG_ZONE_DMA |
ZONE_DMA32 |
16MB ~ 4GB | 32비트 주소 DMA 디바이스 | CONFIG_ZONE_DMA32 |
ZONE_NORMAL |
4GB ~ 끝 | 일반 커널/유저 용도 | 항상 존재 |
ZONE_HIGHMEM |
896MB 이상 (32비트 전용) | 직접 매핑 불가 영역 | CONFIG_HIGHMEM |
ZONE_MOVABLE |
(논리적, 커널 부트 파라미터) | 메모리 핫플러그, 이동 가능 페이지만 | 항상 존재 (비어 있을 수 있음) |
ZONE_DEVICE |
(디바이스 메모리) | PMEM, GPU 메모리, HMM | CONFIG_ZONE_DEVICE |
Migrate Type
외부 단편화를 줄이기 위해 Buddy Allocator는 페이지를 이동 가능성에 따라 구분합니다. 같은 종류의 페이지끼리 모아두면 Memory Compaction이 훨씬 효율적으로 작동합니다:
/* include/linux/mmzone.h */
enum migratetype {
MIGRATE_UNMOVABLE, /* 이동 불가: 커널 모듈, kmalloc */
MIGRATE_MOVABLE, /* 이동 가능: 유저 프로세스 페이지 */
MIGRATE_RECLAIMABLE, /* 회수 가능: 파일 페이지 캐시 */
MIGRATE_PCPTYPES, /* Per-CPU 캐시 대상 (위 3가지) */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, /* GFP_ATOMIC 긴급 예비 */
MIGRATE_CMA, /* CMA 연속 메모리 (카메라/동영상 DMA) */
MIGRATE_ISOLATE, /* 격리 중 (Compaction / Offlining) */
MIGRATE_TYPES,
};
| Migrate Type | 설명 | 대표 예시 |
|---|---|---|
MIGRATE_UNMOVABLE |
이동 불가 (물리 주소 고정) | 커널 모듈, kmalloc |
MIGRATE_MOVABLE |
이동 가능 (가상 주소로 접근) | 유저 프로세스 페이지 |
MIGRATE_RECLAIMABLE |
회수 가능 (재생성 가능) | 파일 페이지 캐시 |
MIGRATE_HIGHATOMIC |
긴급 원자 할당용 예비 | GFP_ATOMIC 실패 방지 |
MIGRATE_CMA |
CMA 연속 메모리 영역 | 카메라/동영상 DMA |
MIGRATE_ISOLATE |
격리 중 (Compaction / Offlining) | 메모리 핫플러그 |
Fallback 전략: 요청한 Migrate Type의 free block이 없으면 fallbacks[] 배열 순서에 따라 다른 타입에서 블록을 빌려옵니다. UNMOVABLE 페이지를 Movable block에서 가져올 경우 단편화가 악화되므로, 이 경우 최소 pageblock 크기(2MB on x86_64) 단위로만 스틸합니다.
Zone 아키텍처 심화
Zone은 단순한 주소 범위 분류가 아닙니다. 각 Zone은 독립적인 워터마크, Per-CPU 캐시, 통계 카운터, 그리고 free_area[] 배열을 유지합니다. 할당 요청 시 Zone fallback 리스트를 따라 여러 Zone을 순회하며 페이지를 찾습니다.
Zone 워터마크와 lowmem_reserve
각 Zone은 lowmem_reserve[] 배열을 통해 상위 Zone의 요청이 하위 Zone을 고갈시키는 것을 방지합니다. 예를 들어, ZONE_NORMAL 요청이 ZONE_DMA32로 fallback할 때, DMA32의 lowmem_reserve[ZONE_NORMAL]만큼의 페이지는 예약되어 DMA32 전용 요청을 위해 보호됩니다.
/* mm/page_alloc.c - zone_watermark_fast (6.x 단순화) */
static inline bool zone_watermark_fast(
struct zone *z, unsigned int order,
unsigned long mark, int highest_zoneidx,
unsigned int alloc_flags, gfp_t gfp_mask)
{
long free_pages = zone_page_state(z, NR_FREE_PAGES);
long min = mark;
/* lowmem_reserve 추가: 하위 zone 보호 */
min += z->lowmem_reserve[highest_zoneidx];
/* CMA 영역은 MOVABLE 요청에만 사용 가능 */
if (!(alloc_flags & ALLOC_CMA))
free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES);
/* order 0 fast check */
if (free_pages > min + z->_watermark[WMARK_LOW])
return true;
/* order > 0: 해당 order 이상의 블록이 존재하는지 확인 */
return __zone_watermark_ok(z, order, mark,
highest_zoneidx, alloc_flags, free_pages);
}
/* lowmem_reserve 계산: /proc/sys/vm/lowmem_reserve_ratio로 조정 */
/* 기본값: 256 256 32 (DMA:DMA32 비율, DMA32:Normal 비율, ...) */
# lowmem_reserve 현재 값 확인
cat /proc/zoneinfo | grep -A2 "protection"
# protection: (0, 2831, 15789, 15789)
# → DMA zone은 DMA32용 2831 pages, Normal용 15789 pages를 예약
# lowmem_reserve_ratio 조정
cat /proc/sys/vm/lowmem_reserve_ratio
# 256 256 32
Zone Fallback과 lowmem_reserve: GFP_KERNEL 요청은 ZONE_NORMAL → ZONE_DMA32 → ZONE_DMA 순으로 시도합니다. 각 fallback 시 lowmem_reserve만큼의 여유분이 있어야 해당 Zone에서 할당할 수 있습니다. 이는 DMA 전용 장치가 메모리 부족으로 동작하지 못하는 상황을 방지합니다.
버디 알고리즘 분할(Split) 상세
Buddy Allocator에서 요청한 order의 free 블록이 없으면, 상위 order의 블록을 찾아 재귀적으로 이진 분할(binary split)합니다. 이 과정은 __rmqueue() → __rmqueue_smallest() → expand() 함수 체인으로 구현됩니다.
/* mm/page_alloc.c - expand(): 상위 order 블록을 분할 */
static inline void expand(
struct zone *zone, struct page *page,
int low, int high, /* low=요청 order, high=실제 블록 order */
struct free_area *area,
int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
high--;
size >>= 1; /* 블록 크기 절반 */
area--; /* 하위 order의 free_area로 이동 */
/* 상위 절반(buddy)을 해당 order의 free list에 추가 */
add_to_free_list(
&page[size], /* buddy page (후반부) */
zone, high, migratetype);
area->nr_free++;
/* buddy의 private에 order 기록 (합병 시 사용) */
set_buddy_order(&page[size], high);
}
/* 남은 page가 요청한 order의 블록 → 할당됨 */
}
/* mm/page_alloc.c - __rmqueue_smallest(): 최소 order에서 블록 찾기 */
static __always_inline struct page *__rmqueue_smallest(
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/* 요청 order부터 MAX_ORDER-1까지 순회 */
for (current_order = order;
current_order < MAX_ORDER; current_order++) {
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype);
if (!page)
continue; /* 이 order에 free 블록 없음 */
/* free list에서 제거 */
del_page_from_free_list(page, zone, current_order);
/* 필요한 만큼만 분할 */
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL; /* 모든 order에서 실패 */
}
분할 오버헤드: expand()의 while 루프는 최대 MAX_ORDER - 1번(10번) 반복합니다. 각 반복에서 하는 일은 링크드 리스트 삽입뿐이므로 O(1)이며, 전체 분할 비용은 O(MAX_ORDER)입니다. 실제로는 대부분의 할당이 order 0이고 PCP에서 처리되므로 분할이 발생하는 경우는 드뭅니다.
버디 알고리즘 합체(Coalesce) 상세
페이지가 해제되면 Buddy Allocator는 해당 블록의 buddy가 free인지 확인하고, free이면 두 블록을 합쳐서 한 단계 큰 order의 블록으로 만듭니다. 이 과정을 buddy coalescing이라 하며, __free_one_page() 함수에서 구현됩니다.
Buddy 주소 계산
/* include/linux/mmzone.h - buddy PFN 계산 */
static inline unsigned long __find_buddy_pfn(
unsigned long page_pfn, unsigned int order)
{
/*
* buddy pfn = page_pfn ^ (1 << order)
*
* 예: page_pfn=0x100, order=2
* buddy = 0x100 ^ 0x4 = 0x104
*
* 예: page_pfn=0x104, order=2
* buddy = 0x104 ^ 0x4 = 0x100
*
* → XOR은 대칭적이므로 A의 buddy의 buddy는 항상 A
*/
return page_pfn ^ (1 << order);
}
/* buddy가 합병 가능한지 확인 */
static inline bool page_is_buddy(
struct page *page, struct page *buddy,
unsigned int order)
{
/* 1. buddy가 free_area의 free list에 있는가? */
if (!page_is_guard(buddy) &&
!PageBuddy(buddy)) /* PG_buddy 플래그 */
return false;
/* 2. buddy의 order가 같은가? */
if (buddy_order(buddy) != order)
return false;
/* 3. 같은 zone에 속하는가? */
if (page_zone_id(page) != page_zone_id(buddy))
return false;
return true;
}
합체 과정 (Coalescing Loop)
/* mm/page_alloc.c - __free_one_page() 핵심 로직 (단순화) */
static inline void __free_one_page(
struct page *page, unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned long buddy_pfn;
struct page *buddy;
while (order < MAX_ORDER - 1) {
/* buddy PFN 계산 (XOR) */
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
/* buddy가 합병 가능한지 확인 */
if (!page_is_buddy(page, buddy, order))
break; /* buddy가 사용 중이거나 다른 order */
/* buddy를 현재 free list에서 제거 */
del_page_from_free_list(buddy, zone, order);
/* 합병: 작은 주소가 새 블록의 시작 */
unsigned long combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++; /* 한 단계 큰 order */
}
/* 최종 블록을 해당 order의 free list에 추가 */
add_to_free_list(page, zone, order, migratetype);
area->nr_free++;
set_buddy_order(page, order);
}
| PFN (16진수) | Order | Buddy PFN | XOR 계산 |
|---|---|---|---|
0x100 |
0 | 0x101 |
0x100 ^ 0x1 = 0x101 |
0x100 |
1 | 0x102 |
0x100 ^ 0x2 = 0x102 |
0x100 |
2 | 0x104 |
0x100 ^ 0x4 = 0x104 |
0x104 |
2 | 0x100 |
0x104 ^ 0x4 = 0x100 (대칭) |
0x100 |
3 | 0x108 |
0x100 ^ 0x8 = 0x108 |
페이지 이동성 그룹 (Page Mobility)
외부 단편화의 핵심 원인은 이동 불가능한(unmovable) 페이지가 연속 영역 중간에 박혀 있는 것입니다. 리눅스 커널은 이를 해결하기 위해 물리 메모리를 pageblock 단위(보통 2MB, huge page 크기)로 나누고, 각 pageblock에 이동성 유형을 부여합니다.
/* include/linux/mmzone.h */
#define MIGRATE_UNMOVABLE 0 /* 이동 불가: 커널 slab, 모듈, DMA 버퍼 */
#define MIGRATE_MOVABLE 1 /* 이동 가능: 유저 프로세스 페이지, 파일 맵 */
#define MIGRATE_RECLAIMABLE 2 /* 회수 가능: 페이지 캐시, dentry 캐시 */
#define MIGRATE_PCPTYPES 3 /* PCP가 캐시하는 타입 수 (위 3개) */
#define MIGRATE_HIGHATOMIC 3 /* GFP_ATOMIC 전용 예비 (= PCPTYPES) */
#define MIGRATE_CMA 4 /* CMA 연속 영역 (DMA용) */
#define MIGRATE_ISOLATE 5 /* 격리 중 (offlining, compaction) */
/* pageblock_order: 이동성 그룹의 단위 (보통 huge page order) */
/* x86_64: pageblock_order = 9 (512 pages = 2MB) */
/* ARM64 4K: pageblock_order = 9 (2MB) */
/* ARM64 64K: pageblock_order = 13 (512MB, 아키텍처 의존) */
Fallback 배열
/* mm/page_alloc.c - fallback 순서 */
static int fallbacks[MIGRATE_TYPES][3] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
};
/*
* UNMOVABLE 요청 시:
* 1. UNMOVABLE free list에서 시도
* 2. 실패 → RECLAIMABLE에서 steal
* 3. 실패 → MOVABLE에서 steal
*
* Steal 시 가능하면 전체 pageblock을 전환 (단편화 최소화)
*/
Migrate Type Steal과 단편화: Fallback으로 다른 migrate type에서 블록을 빌리면(steal) 단편화가 악화됩니다. mm_page_alloc_extfrag tracepoint로 steal 빈도를 모니터링하세요. 빈번하다면 /proc/pagetypeinfo로 migrate type 분포를 확인하고, 적절한 vm.min_free_kbytes 값을 설정하세요.
API (Application Programming Interface)
페이지 할당
/* include/linux/gfp.h */
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t gfp_mask);
/* 사용 예 */
struct page *page = alloc_pages(GFP_KERNEL, 2); /* 2^2 = 4 pages = 16KB */
void *addr = page_address(page);
페이지 해제
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
/* 사용 예 */
__free_pages(page, 2);
NUMA 인식 할당 API
/* include/linux/gfp.h - NUMA 노드 지정 할당 */
struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order);
struct page *alloc_pages_current(gfp_t gfp_mask, unsigned int order);
/* Migrate Type 명시 할당 (내부 API) */
struct page *alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask);
/* 현재 CPU가 속한 NUMA 노드에서 할당 */
struct page *page = alloc_pages_node(numa_node_id(), GFP_KERNEL, 0);
/* GFP_THISNODE: 지정 노드에서만 할당, 실패 시 NULL 반환 */
page = alloc_pages_node(target_nid, GFP_KERNEL | __GFP_THISNODE, 0);
alloc_pages() vs __get_free_pages(): alloc_pages()는 struct page *를 반환하므로 고급 페이지 조작(복합 페이지, 마이그레이션)에 적합합니다. __get_free_pages()는 가상 주소(unsigned long)를 반환하므로 직접 메모리 접근이 필요할 때 편리합니다. 내부적으로는 동일한 __alloc_pages()를 호출합니다.
GFP 플래그 완전 분류
GFP(Get Free Pages) 플래그는 Buddy Allocator의 할당 정책을 제어하는 비트마스크입니다. 복합 플래그(compound flags)와 기본 비트 플래그(__GFP_ prefix)로 나뉩니다. 올바른 GFP 플래그 선택은 시스템 안정성에 직결됩니다.
복합 GFP 플래그 (Compound Flags)
| 플래그 | 구성 비트 | 사용 컨텍스트 | 대기 가능 |
|---|---|---|---|
GFP_ATOMIC |
__GFP_HIGH | __GFP_KSWAPD_RECLAIM |
인터럽트, softirq, spinlock 내부 | 불가 |
GFP_KERNEL |
__GFP_RECLAIM | __GFP_IO | __GFP_FS |
프로세스 컨텍스트 일반 | 가능 |
GFP_KERNEL_ACCOUNT |
GFP_KERNEL | __GFP_ACCOUNT |
kmemcg 과금 대상 | 가능 |
GFP_NOWAIT |
__GFP_KSWAPD_RECLAIM |
비차단 할당 (실패 허용) | 불가 |
GFP_NOIO |
__GFP_RECLAIM |
블록 I/O 경로 (교착 방지) | 가능 (I/O 제외) |
GFP_NOFS |
__GFP_RECLAIM | __GFP_IO |
파일시스템 경로 (교착 방지) | 가능 (FS 제외) |
GFP_USER |
__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL |
유저 공간 대리 할당 | 가능 |
GFP_HIGHUSER |
GFP_USER | __GFP_HIGHMEM |
유저 매핑 페이지 (32비트) | 가능 |
GFP_HIGHUSER_MOVABLE |
GFP_HIGHUSER | __GFP_MOVABLE |
유저 페이지 (이동 가능) | 가능 |
GFP_DMA |
__GFP_DMA |
ISA DMA 버퍼 (0~16MB) | 불가 |
GFP_DMA32 |
__GFP_DMA32 |
32비트 DMA 버퍼 (0~4GB) | 불가 |
GFP_TRANSHUGE |
GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN |
Transparent Huge Page | 가능 (제한) |
기본 __GFP 비트 플래그
| 비트 플래그 | 역할 | 설명 |
|---|---|---|
__GFP_HIGH |
우선순위 | 긴급 할당, HIGHATOMIC 예비 사용 가능 |
__GFP_IO |
회수 정책 | 블록 I/O를 통한 페이지 기록 허용 |
__GFP_FS |
회수 정책 | 파일시스템 콜 허용 (inode shrink 등) |
__GFP_DIRECT_RECLAIM |
회수 정책 | 직접 메모리 회수 가능 (대기 가능) |
__GFP_KSWAPD_RECLAIM |
회수 정책 | kswapd 비동기 회수 트리거 |
__GFP_RECLAIM |
회수 정책 | __GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM |
__GFP_MOVABLE |
이동성 | 이동 가능한 페이지 (compaction 대상) |
__GFP_RECLAIMABLE |
이동성 | 회수 가능한 페이지 (shrink 대상) |
__GFP_DMA |
Zone 선택 | ZONE_DMA에서 할당 |
__GFP_DMA32 |
Zone 선택 | ZONE_DMA32에서 할당 |
__GFP_HIGHMEM |
Zone 선택 | ZONE_HIGHMEM 허용 (32비트 전용) |
__GFP_ZERO |
초기화 | 반환 전 0으로 초기화 |
__GFP_COMP |
구조 | Compound page로 구성 |
__GFP_NORETRY |
실패 정책 | 직접 회수 1회만 시도, OOM 미발동 |
__GFP_RETRY_MAYFAIL |
실패 정책 | 여러 번 재시도하되 무한 반복 금지 |
__GFP_NOFAIL |
실패 정책 | 반드시 성공 (무한 재시도) |
__GFP_NOWARN |
보고 | 할당 실패 시 경고 메시지 억제 |
__GFP_ACCOUNT |
과금 | kmemcg 메모리 사용량 추적 |
__GFP_THISNODE |
NUMA | 지정된 NUMA 노드에서만 할당 |
__GFP_NOMEMALLOC |
예비 | 긴급 예비(reserves) 사용 금지 |
__GFP_HARDWALL |
NUMA | cpuset 제한 준수 |
GFP 플래그 선택 가이드:
- 프로세스 컨텍스트, 일반 할당:
GFP_KERNEL(가장 흔함) - 인터럽트/softirq 컨텍스트:
GFP_ATOMIC(대기 불가) - 블록 I/O 레이어:
GFP_NOIO(I/O 재귀 방지) - 파일시스템 레이어:
GFP_NOFS(FS 재귀 방지) - 유저 페이지:
GFP_HIGHUSER_MOVABLE(compaction 가능) - 실패 허용 대형 할당:
GFP_KERNEL | __GFP_RETRY_MAYFAIL - 반드시 성공해야 하는 할당:
GFP_KERNEL | __GFP_NOFAIL(극히 주의)
알고리즘 (Algorithm)
블록 분할 (Split)
요청: order 1 (2 pages)
가용: order 3 (8 pages) 하나만 존재
1. order 3 블록을 꺼냄 (8 pages)
2. 반으로 쪼갬: 2개의 order 2 블록 (4 pages each)
3. 하나를 다시 쪼갬: 2개의 order 1 블록 (2 pages each)
4. 하나는 할당, 나머지는 free list에 반환
결과:
- 할당: order 1 (2 pages)
- 반환: order 1 (2 pages), order 2 (4 pages)
블록 병합 (Coalesce)
반환: order 1 블록 (주소 0x1000)
1. Buddy 주소 계산: 0x1000 XOR (2 << 12) = 0x3000
2. Buddy가 free이고 같은 order인가? → YES
3. 두 블록을 병합하여 order 2 생성
4. 재귀적으로 반복 (order 2의 buddy 확인)
결과: 최대한 큰 블록으로 병합되어 외부 단편화 감소
시각적 이해: 분할 과정
할당 Fast Path vs Slow Path
alloc_pages() 호출은 두 단계로 처리됩니다. Fast Path에서 즉시 성공하면 Slow Path의 대기 비용을 치르지 않습니다. Fast Path는 WMARK_LOW를 기준으로, Slow Path는 WMARK_MIN까지 허용하며 메모리 회수/압축을 시도합니다:
Fast Path
/* mm/page_alloc.c - 단순화한 흐름 */
struct page *__alloc_pages(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct alloc_context ac = {};
struct page *page;
/* order 0: Per-CPU 캐시에서 lock-free 할당 시도 */
if (likely(order == 0)) {
page = get_page_from_freelist(gfp, 0, ALLOC_WMARK_LOW, &ac);
if (likely(page))
return page;
}
/* 일반 Free List에서 시도 (WMARK_LOW 이상인 경우) */
page = get_page_from_freelist(gfp, order, ALLOC_WMARK_LOW, &ac);
if (likely(page))
return page;
/* Fast path 실패 → Slow path */
return __alloc_pages_slowpath(gfp, order, &ac);
}
get_page_from_freelist() 상세
Fast Path와 Slow Path 모두 get_page_from_freelist()를 호출합니다. 이 함수는 Zone fallback 리스트를 순회하며, 워터마크 검사를 통과하는 Zone에서 페이지를 할당합니다.
/* mm/page_alloc.c - get_page_from_freelist() 핵심 로직 (단순화) */
static struct page *get_page_from_freelist(
gfp_t gfp_mask, unsigned int order,
int alloc_flags, const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
struct page *page;
/* Zone fallback 리스트 순회 */
for_next_zone_zonelist_nodemask(zone, z,
ac->highest_zoneidx, ac->nodemask) {
/* NUMA: cpuset 제한 확인 */
if (cpusets_enabled() &&
(alloc_flags & ALLOC_CPUSET) &&
!__cpuset_zone_allowed(zone, gfp_mask))
continue;
/* 워터마크 검사 */
if (!zone_watermark_fast(zone, order,
min_wmark_pages(zone) +
z->lowmem_reserve[ac->highest_zoneidx],
ac->highest_zoneidx, alloc_flags, gfp_mask))
continue;
/* 이 Zone에서 할당 시도 */
page = rmqueue(ac->preferred_zoneref->zone,
zone, order, gfp_mask, alloc_flags,
ac->migratetype);
if (page) {
prep_new_page(page, order, gfp_mask, alloc_flags);
return page;
}
}
return NULL;
}
/* rmqueue(): order에 따라 PCP 또는 Buddy에서 할당 */
static inline struct page *rmqueue(...)
{
if (likely(order == 0)) {
/* order 0: PCP 캐시에서 할당 */
page = rmqueue_pcplist(...);
} else {
/* order > 0: Zone lock 잡고 Buddy freelist에서 할당 */
spin_lock_irqsave(&zone->lock, flags);
page = __rmqueue(zone, order, migratetype, alloc_flags);
spin_unlock_irqrestore(&zone->lock, flags);
}
return page;
}
Slow Path
/* mm/page_alloc.c - __alloc_pages_slowpath 핵심 로직 */
static struct page *__alloc_pages_slowpath(gfp_t gfp_mask,
unsigned int order, struct alloc_context *ac)
{
/* 1단계: kswapd 깨우기 (비동기 메모리 회수 시작) */
wake_all_kswapds(order, gfp_mask, ac);
/* 2단계: ALLOC_WMARK_MIN으로 낮춰서 재시도 */
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
/* 3단계: 메모리 Compaction 후 재시도 */
page = __alloc_pages_direct_compact(gfp_mask, order,
alloc_flags, ac, compact_priority, &compact_result);
if (page)
goto got_pg;
/* 4단계: 직접 메모리 회수 (프로세스 차단) */
page = __alloc_pages_direct_reclaim(gfp_mask, order,
alloc_flags, ac, &did_some_progress);
if (page)
goto got_pg;
/* 5단계: OOM Killer 호출 (최후 수단) */
if (!(gfp_mask & __GFP_NORETRY)) {
page = __alloc_pages_may_oom(gfp_mask, order, ac,
&did_some_progress);
}
got_pg:
return page;
}
GFP_ATOMIC과 Slow Path: GFP_ATOMIC으로 할당 시 Slow Path의 대기(kswapd, Direct Reclaim, OOM)를 수행하지 않습니다. 대신 MIGRATE_HIGHATOMIC에 예약된 블록을 사용합니다. 따라서 GFP_ATOMIC 할당이 반복 실패하면 시스템 메모리가 심각하게 부족한 상태입니다.
Slow Path 상세 동작 순서
| 단계 | 함수 | 동작 | 대기 |
|---|---|---|---|
| 1 | wake_all_kswapds() |
kswapd 커널 스레드 깨우기 (비동기 회수 시작) | 비차단 |
| 2 | get_page_from_freelist(WMARK_MIN) |
워터마크를 MIN으로 낮춰서 재시도 | 비차단 |
| 3 | __alloc_pages_direct_compact() |
메모리 compaction 수행 후 재시도 | 차단 가능 |
| 4 | __alloc_pages_direct_reclaim() |
직접 메모리 회수 (프로세스 차단) | 차단 (I/O 대기) |
| 5 | __alloc_pages_may_oom() |
OOM Killer 발동 (프로세스 종료) | 차단 (프로세스 종료 대기) |
각 단계에서 성공하면 즉시 반환합니다. __GFP_NORETRY가 설정된 경우 단계 4 이후 포기합니다. __GFP_RETRY_MAYFAIL은 여러 번 반복하되 무한 루프는 피합니다. __GFP_NOFAIL은 성공할 때까지 무한 반복합니다 (극히 주의).
__GFP_NOFAIL 사용 주의: 이 플래그는 할당이 반드시 성공해야 하는 경우에만 사용합니다 (예: 파일시스템 저널). 메모리가 정말로 고갈된 상태에서 __GFP_NOFAIL이 설정되면 커널이 무한 루프에 빠져 시스템이 응답하지 않을 수 있습니다. order 1 이상의 __GFP_NOFAIL 할당은 WARN_ON을 발생시킵니다.
부팅 시 Buddy 초기화
시스템 부팅 과정에서 Buddy Allocator는 다음 순서로 초기화됩니다. 이 과정이 완료되어야 alloc_pages()가 동작합니다.
/* 부팅 초기화 순서 (아키텍처별로 약간 다름) */
/* 1. memblock에서 물리 메모리 레이아웃 파악 */
memblock_add(base, size); /* 펌웨어가 보고한 메모리 영역 등록 */
memblock_reserve(base, size); /* 커널 이미지, initrd 등 예약 */
/* 2. Node/Zone 구조 초기화 */
free_area_init(); /* zone별 free_area, watermark 초기화 */
/* 3. memblock에서 Buddy로 페이지 이관 */
memblock_free_all(); /* 미예약 영역의 페이지를 Buddy에 추가 */
/* → __free_pages_core() → __free_one_page() */
/* → 가능한 한 큰 order로 합체하여 free_area에 추가 */
/* 4. Per-CPU 페이지 캐시 초기화 */
setup_per_cpu_pageset(); /* 각 CPU의 per_cpu_pages 구조체 설정 */
/* 5. 워터마크 계산 */
init_per_zone_wmark_min(); /* min_free_kbytes 기반 워터마크 설정 */
/* dmesg에서 확인 가능한 메시지 */
/* "Zone ranges:" → Zone별 PFN 범위 */
/* "Movable zone start for each node" */
/* "Early memory node ranges" */
/* "Memory: 16384000K/16777216K available" → Buddy에 이관된 메모리 */
# 부팅 시 Buddy 초기화 로그 확인
dmesg | grep -E "(Zone ranges|free_area_init|Memory:)"
# [ 0.000000] Zone ranges:
# [ 0.000000] DMA [mem 0x0000000000001000-0x0000000000ffffff]
# [ 0.000000] DMA32 [mem 0x0000000001000000-0x00000000ffffffff]
# [ 0.000000] Normal [mem 0x0000000100000000-0x00000003ffffffff]
# [ 0.000000] Memory: 16127744K/16777216K available
# → (16777216 - 16127744) KB = 약 634MB가 커널/예약으로 사용됨
# memblock 레이아웃 확인 (부팅 시에만)
dmesg | grep -E "(memblock|reserve)" | head -n 20
Zone 워터마크 (Zone Watermark)
각 Zone은 세 가지 워터마크를 유지합니다. 여유 페이지가 워터마크 아래로 떨어지면 메모리 회수가 시작됩니다:
| 워터마크 | 의미 | 진입 시 동작 |
|---|---|---|
WMARK_HIGH |
여유 충분 | 정상 할당, kswapd 재설정 |
WMARK_LOW |
부족 시작 | kswapd 깨워 비동기 회수 |
WMARK_MIN |
긴급 부족 | Direct Reclaim (프로세스 차단) |
| min 미만 | 극도 부족 | OOM Killer 발동 |
/* include/linux/mmzone.h */
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
WMARK_PROMO, /* 6.1+: promotion threshold */
NR_WMARK
};
/* 워터마크 검사 (mm/page_alloc.c) */
static inline bool zone_watermark_ok(struct zone *z,
unsigned int order, unsigned long mark,
int highest_zoneidx, unsigned int alloc_flags)
{
long min = mark;
long free_pages = zone_page_state(z, NR_FREE_PAGES);
return free_pages >= min + z->lowmem_reserve[highest_zoneidx];
}
# 현재 워터마크 확인
cat /proc/zoneinfo | grep -A8 "zone Normal"
# pages free 45321
# min 1440 ← WMARK_MIN
# low 1800 ← WMARK_LOW
# high 2160 ← WMARK_HIGH
# 워터마크 조정 (vm.min_free_kbytes가 WMARK_MIN 기준)
sysctl vm.min_free_kbytes
# vm.min_free_kbytes = 67584
# 즉시 메모리 Compaction 강제 실행
echo 1 > /proc/sys/vm/compact_memory
Watermark 시스템 상세
워터마크는 각 Zone의 메모리 압박 수준을 정량적으로 표현하는 임계값입니다. 워터마크 값은 부팅 시 vm.min_free_kbytes로부터 계산되며, 런타임에 watermark_boost로 동적 조정됩니다.
워터마크 계산
/* mm/page_alloc.c - __setup_per_zone_wmarks() */
/*
* 계산 공식:
* WMARK_MIN = min_free_kbytes / PAGE_SIZE / zone 수 (비율)
* WMARK_LOW = WMARK_MIN + WMARK_MIN / 4 (= 1.25 × MIN)
* WMARK_HIGH = WMARK_MIN + WMARK_MIN / 2 (= 1.5 × MIN)
*
* watermark_scale_factor (/proc/sys/vm/watermark_scale_factor)
* → 0.1% ~ 10% 범위로 LOW/HIGH 간격 조정
*/
static void __setup_per_zone_wmarks(void)
{
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
for_each_zone(zone) {
unsigned long min_pages, tmp;
/* managed_pages 비율로 분배 */
tmp = (u64)pages_min * zone_managed_pages(zone);
do_div(tmp, total_managed);
min_pages = tmp;
zone->_watermark[WMARK_MIN] = min_pages;
/* LOW = MIN + (MIN/4) 또는 watermark_scale_factor 기반 */
tmp = max(min_pages / 4,
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));
zone->_watermark[WMARK_LOW] = min_pages + tmp;
/* HIGH = MIN + (MIN/2) 또는 2배 scale */
zone->_watermark[WMARK_HIGH] = min_pages + tmp * 2;
zone->watermark_boost = 0;
}
}
Watermark Boost
커널 5.0+에서 도입된 watermark_boost는 외부 단편화가 감지되면 (migrate type fallback 발생 시) 해당 Zone의 HIGH 워터마크를 일시적으로 상향하여 kswapd가 더 적극적으로 메모리를 회수하도록 유도합니다.
/* mm/page_alloc.c - boost_watermark() */
static void boost_watermark(struct zone *zone)
{
unsigned long max_boost;
if (!watermark_boost_factor)
return;
/* boost 상한: WMARK_HIGH의 watermark_boost_factor배 */
max_boost = mult_frac(zone->_watermark[WMARK_HIGH],
watermark_boost_factor, 10000);
/* pageblock 단위만큼 boost 증가 */
if (zone->watermark_boost < max_boost)
zone->watermark_boost += pageblock_nr_pages;
}
/* boost된 워터마크 읽기 */
static inline unsigned long wmark_pages(
const struct zone *z, enum zone_watermarks w)
{
return z->_watermark[w] + z->watermark_boost;
}
| sysctl 매개변수 | 기본값 | 역할 |
|---|---|---|
vm.min_free_kbytes |
RAM 의존 (수 MB) | WMARK_MIN의 기준 (모든 Zone 합산) |
vm.watermark_scale_factor |
10 (0.1%) | LOW와 HIGH 간격 조정 (1~1000, 0.01%~10%) |
vm.watermark_boost_factor |
15000 (150%) | 단편화 시 HIGH 워터마크 boost 상한 |
vm.lowmem_reserve_ratio |
256 256 32 | Zone 간 lowmem_reserve 비율 |
vm.percpu_pagelist_high_fraction |
0 (자동) | PCP high 워터마크를 Zone 페이지의 1/N로 설정 |
min_free_kbytes 튜닝: 너무 낮으면 GFP_ATOMIC 할당이 실패할 수 있고, 너무 높으면 사용 가능한 메모리가 줄어듭니다. 일반적으로 전체 RAM의 0.1%~1% 범위가 적절합니다. NUMA 시스템에서는 노드당 균등 분배되므로 노드 수를 고려하세요.
PCP (Per-CPU Page) 캐싱 상세
Order 0 (단일 페이지) 할당은 리눅스 커널에서 가장 빈번하게 발생합니다 (전체 할당의 90% 이상). 매번 Zone lock을 잡고 Buddy free list에 접근하면 심각한 경합이 발생하므로, 커널은 CPU별 페이지 캐시(PCP, Per-CPU Pages)를 유지합니다.
/* mm/page_alloc.c - PCP 관련 구조체 (6.x) */
struct per_cpu_pages {
spinlock_t lock; /* local lock (CPU 전환 방지) */
int count; /* 현재 캐시된 총 페이지 수 */
int high; /* 상한: 초과 시 free_pcppages_bulk() */
int high_min; /* 최소 high 값 */
int high_max; /* 최대 high 값 (동적 조절 상한) */
int batch; /* Buddy로 한 번에 교환할 페이지 수 */
short free_count; /* free_pcppages_bulk 카운터 */
struct list_head lists[NR_PCP_LISTS]; /* migrate type별 리스트 */
};
/* NR_PCP_LISTS = MIGRATE_PCPTYPES * (pcp_order_max + 1)
* 기본: 3 × 1 = 3 (order 0만)
* CONFIG_TRANSPARENT_HUGEPAGE: 3 × (THP_order + 1) */
/* PCP에서 할당 (order 0) */
static struct page *rmqueue_pcplist(
struct zone *preferred_zone,
struct zone *zone,
gfp_t gfp_flags,
int migratetype,
unsigned int alloc_flags)
{
struct per_cpu_pages *pcp;
struct list_head *list;
pcp = this_cpu_ptr(zone->per_cpu_pageset);
list = &pcp->lists[order_to_pindex(migratetype, 0)];
if (list_empty(list)) {
/* 캐시 고갈: Buddy에서 batch만큼 보충 */
int nr = rmqueue_bulk(zone, 0,
READ_ONCE(pcp->batch),
list, migratetype, alloc_flags);
pcp->count += nr;
}
page = list_first_entry(list, struct page, pcp_list);
list_del(&page->pcp_list);
pcp->count--;
/* high 초과 시 batch만큼 Buddy에 반환 */
if (pcp->count >= pcp->high)
free_pcppages_bulk(zone,
READ_ONCE(pcp->batch),
pcp, migratetype);
return page;
}
| PCP 매개변수 | 계산 방식 | 조정 방법 |
|---|---|---|
batch |
Zone managed_pages / 1024, 최소 1, 최대 512 | /proc/sys/vm/percpu_pagelist_fraction (deprecated) |
high |
batch * 6 (기본) | /proc/sys/vm/percpu_pagelist_high_fraction |
high_min |
batch * 4 | 메모리 압박 시 자동 축소 하한 |
high_max |
batch * 8 또는 fraction 기반 | 유휴 시 자동 확장 상한 |
PCP drain: CPU가 오프라인되거나 drain_all_pages() 호출 시 해당 CPU의 PCP가 전부 Buddy로 반환됩니다. echo 1 > /proc/sys/vm/drop_caches는 PCP를 drain하지 않으며, 명시적인 PCP drain은 drain_all_pages(NULL) 내부에서만 발생합니다.
컴팩션 (Compaction)
메모리 Compaction은 흩어진 free 페이지를 한쪽으로 모아 연속 영역을 만드는 과정입니다. 고차 할당(order 1 이상)이 실패할 때 Slow Path에서 호출되며, 별도 커널 스레드 kcompactd가 백그라운드에서도 수행합니다.
/* mm/compaction.c - compact_zone() 핵심 로직 (단순화) */
static enum compact_result compact_zone(
struct compact_control *cc)
{
/*
* 두 스캐너가 양끝에서 중앙으로 이동:
* - migrate_scanner: zone 시작에서 → (사용 중인 MOVABLE 페이지 수집)
* - free_scanner: zone 끝에서 ← (free 페이지 수집)
* 두 스캐너가 만나면 완료
*/
while (cc->migrate_pfn < cc->free_pfn) {
/* 1. 이동 대상 페이지 수집 (MOVABLE만) */
nr_migrated = isolate_migratepages(cc);
if (!nr_migrated)
continue;
/* 2. free 페이지 수집 (목적지) */
nr_freepages = isolate_freepages(cc);
if (!nr_freepages) {
putback_movable_pages(&cc->migratepages);
break;
}
/* 3. 페이지 이동 (migrate_pages) */
err = migrate_pages(&cc->migratepages,
compaction_alloc, compaction_free,
(unsigned long)cc, cc->mode,
MR_COMPACTION);
/* 4. 충분한 고차 블록이 생겼는지 확인 */
if (compact_zone_order_suitable(cc))
return COMPACT_SUCCESS;
}
return cc->result;
}
# compaction 통계 확인
cat /proc/vmstat | grep compact
# compact_stall 1234 ← direct compaction 호출 횟수
# compact_success 890 ← 성공 횟수
# compact_fail 344 ← 실패 횟수
# compact_daemon_wake 567 ← kcompactd 기동 횟수
# compact_migrate_scanned 45678 ← 스캔한 페이지 수
# compact_free_scanned 89012 ← free 스캔 페이지 수
# kcompactd 상태 확인
ps aux | grep kcompactd
# root ... [kcompactd0] ← Node 0의 compaction 데몬
# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory
Compaction 오버헤드: Direct Compaction은 할당 경로에서 수행되므로 latency spike를 유발할 수 있습니다. 실시간 시스템에서는 /proc/sys/vm/compact_memory로 사전 compaction을 수행하거나, CMA/Huge Pages 사전 예약으로 고차 할당 요구를 줄이세요.
struct page와 folio
struct page는 리눅스 커널에서 모든 물리 페이지 프레임의 메타데이터를 담는 기본 자료구조입니다. 시스템의 모든 물리 페이지마다 하나의 struct page가 존재하며, mem_map[] 배열로 관리됩니다. Buddy Allocator는 이 구조체의 여러 필드를 활용합니다.
/* include/linux/mm_types.h - struct page (주요 필드만, 6.x) */
struct page {
unsigned long flags; /* PG_locked, PG_buddy, PG_slab, ... */
union {
struct { /* Buddy allocator 사용 시 */
union {
struct list_head buddy_list; /* free list 연결 */
struct list_head pcp_list; /* PCP list 연결 */
};
unsigned long private; /* buddy order (PG_buddy 시) */
};
struct { /* Page cache / anon page 사용 시 */
struct list_head lru; /* LRU list 연결 */
struct address_space *mapping; /* 소속 address_space */
pgoff_t index; /* 매핑 내 오프셋 */
unsigned long private; /* buffer_head 등 */
};
struct { /* Slab allocator 사용 시 */
struct kmem_cache *slab_cache;
void *freelist;
union {
unsigned long counters;
struct { unsigned inuse:16; unsigned objects:15; };
};
};
struct { /* Compound page (tail page) */
unsigned long compound_head; /* head page 포인터 + 1 */
};
};
union {
atomic_t _mapcount; /* 페이지 테이블 매핑 수 (-1 = 미매핑) */
unsigned int page_type; /* PageBuddy, PageOffline 등 */
};
atomic_t _refcount; /* 참조 카운트 (0이면 free 가능) */
};
Compound Page와 Folio
/* Compound Page: order > 0 할당 시 생성 (__GFP_COMP 사용) */
/*
* order 2 compound page (4 pages):
* page[0]: head page (PG_head set)
* - compound_dtor: destructor 인덱스
* - compound_order: order 값
* - compound_nr: 총 페이지 수 (1 << order)
* page[1..3]: tail pages
* - compound_head = &page[0] | 1 (최하위 비트로 tail 표시)
*/
/* include/linux/page-flags.h */
static inline bool PageCompound(struct page *page)
{
return test_bit(PG_head, &page->flags) ||
READ_ONCE(page->compound_head) & 1;
}
/* include/linux/mm_types.h - folio (6.x) */
/*
* struct folio = compound page의 head page를 타입 안전하게 감싼 것
* page cache, file-backed page에서 점차 struct page를 대체
*/
struct folio {
union {
struct {
unsigned long flags;
union {
struct list_head lru;
struct { void *__filler; unsigned int mlock_count; };
};
struct address_space *mapping;
pgoff_t index;
void *private;
atomic_t _mapcount;
atomic_t _refcount;
};
struct page page; /* struct page와 메모리 호환 */
};
};
/* folio ↔ page 변환 */
struct folio *page_folio(struct page *page);
struct page *folio_page(struct folio *folio, size_t n);
size_t folio_size(struct folio *folio);
size_t folio_nr_pages(struct folio *folio);
| 비교 항목 | struct page | struct folio |
|---|---|---|
| 도입 시기 | 초기 리눅스 | 5.16+ (Matthew Wilcox) |
| 크기 인식 | 항상 PAGE_SIZE 가정 가능성 | compound size를 명시적으로 반영 |
| tail page 혼동 | head/tail 구분 필요 | 항상 head page만 가리킴 |
| Page Cache API | find_get_page() 등 | filemap_get_folio() 등 |
| Buddy Allocator | 직접 사용 | folio_alloc() → 내부적으로 alloc_pages() |
folio 전환 현황: 리눅스 커널은 점진적으로 struct page API를 struct folio API로 전환하고 있습니다. Buddy Allocator 자체는 여전히 struct page를 사용하지만, 상위 레이어(Page Cache, 파일시스템)에서는 folio가 표준입니다. folio_alloc(gfp, order)는 내부적으로 alloc_pages()를 호출합니다.
메모리 핫플러그 연동
메모리 핫플러그(Memory Hotplug)는 시스템 운영 중에 물리 메모리를 추가/제거하는 기능입니다. 클라우드 환경, 가상화, DIMM 교체 등에서 사용됩니다. Buddy Allocator는 핫플러그된 메모리를 ZONE_MOVABLE에 배치하여 나중에 안전하게 제거할 수 있도록 합니다.
/* mm/memory_hotplug.c - 온라인 경로 (단순화) */
int online_pages(unsigned long pfn, unsigned long nr_pages,
struct zone *zone, struct memory_group *group)
{
/* 1. 페이지를 Buddy에 등록 */
move_pfn_range_to_zone(zone, pfn, nr_pages, NULL, MIGRATE_ISOLATE);
/* 2. memmap 초기화 */
memmap_init_range(nr_pages, nid, zone_idx(zone), pfn,
MEMINIT_HOTPLUG, NULL, MIGRATE_MOVABLE);
/* 3. 페이지를 Buddy free list에 추가 */
for (pfn = start; pfn < end; pfn += pageblock_nr_pages) {
set_pageblock_migratetype(
pfn_to_page(pfn), MIGRATE_MOVABLE);
}
/* 4. Zone 통계 업데이트 */
zone->present_pages += nr_pages;
zone->managed_pages += nr_pages;
setup_per_zone_wmarks(); /* 워터마크 재계산 */
return 0;
}
/* mm/memory_hotplug.c - 오프라인 경로 (단순화) */
int offline_pages(unsigned long start_pfn, unsigned long nr_pages,
struct zone *zone)
{
/* 1. 해당 영역의 pageblock을 MIGRATE_ISOLATE로 변경 */
start_isolate_page_range(start_pfn, end_pfn,
MIGRATE_MOVABLE, MEMORY_OFFLINE);
/* 2. 사용 중인 페이지를 다른 영역으로 이동 */
do {
ret = scan_movable_pages(pfn, end_pfn, &pfn);
if (!ret)
do_migrate_range(pfn, end_pfn); /* compaction과 유사 */
} while (!ret);
/* 3. Buddy에서 페이지 해제 */
dissolve_free_huge_pages(start_pfn, end_pfn);
undo_isolate_page_range(start_pfn, end_pfn, MIGRATE_MOVABLE);
/* 4. Zone 통계 업데이트 */
zone->present_pages -= nr_pages;
zone->managed_pages -= nr_pages;
setup_per_zone_wmarks();
return 0;
}
# 메모리 블록 상태 확인
ls /sys/devices/system/memory/
# memory0 memory1 memory2 ... memory255
cat /sys/devices/system/memory/memory32/state
# online
# 메모리 블록 오프라인 (이동 가능한 페이지만 포함된 경우)
echo offline > /sys/devices/system/memory/memory32/state
# 메모리 블록 온라인 (ZONE_MOVABLE에 배치)
echo online_movable > /sys/devices/system/memory/memory32/state
# ZONE_MOVABLE 범위 확인 (커널 부트 파라미터)
# kernelcore=4G → 4GB는 ZONE_NORMAL, 나머지는 ZONE_MOVABLE
# movablecore=8G → 8GB를 ZONE_MOVABLE로 설정
cat /proc/zoneinfo | grep -A5 "zone Movable"
오프라인 실패 원인: UNMOVABLE 페이지(커널 slab, 모듈)가 해당 메모리 블록에 존재하면 오프라인이 실패합니다. 이를 방지하려면 핫플러그 대상 메모리를 ZONE_MOVABLE에 배치하거나, kernelcore= 파라미터로 커널 전용 영역을 분리하세요.
CMA (Contiguous Memory Allocator)
CMA는 디바이스 드라이버가 필요로 하는 대형 연속 물리 메모리를 보장하기 위한 할당자입니다. 부팅 시 특정 영역을 CMA용으로 예약하지만, 평소에는 MOVABLE 페이지에 사용할 수 있어 메모리 낭비를 최소화합니다.
/* drivers/dma-buf/dma-alloc.c (이전: mm/cma.c) */
/* CMA 영역 선언 (Device Tree 또는 커널 파라미터) */
/* cma=256M → 기본 CMA 영역 256MB 예약 */
/* cma=256M@0-4G → 4GB 이내에서 256MB 예약 */
/* CMA 할당 API */
struct page *cma_alloc(struct cma *cma,
size_t count, /* 요청 페이지 수 */
unsigned int align, /* 정렬 (order) */
bool no_warn);
bool cma_release(struct cma *cma,
const struct page *pages, unsigned int count);
/* 일반적인 사용: dma_alloc_coherent() 내부에서 CMA 호출 */
void *dma_alloc_coherent(struct device *dev,
size_t size, dma_addr_t *dma_handle, gfp_t gfp);
/* CMA 동작 과정:
* 1. CMA 영역에서 요청 크기의 연속 PFN 범위 찾기 (bitmap 검색)
* 2. 해당 범위의 pageblock을 MIGRATE_ISOLATE로 변경
* 3. 사용 중인 MOVABLE 페이지를 다른 영역으로 이주 (migrate_pages)
* 4. 연속 free 영역 확보 → 호출자에게 반환
*/
# CMA 영역 확인
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB ← 예약된 CMA 영역
# CmaFree: 245760 kB ← 현재 사용 가능 (MOVABLE로 사용 중일 수 있음)
# CMA 할당 디버깅
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_start/enable
echo 1 > /sys/kernel/debug/tracing/events/cma/cma_alloc_finish/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 커널 부트 파라미터로 CMA 크기 지정
# cma=256M → 기본 CMA 256MB
# cma=0 → CMA 비활성화
# hugetlb_cma=1G → CMA를 통한 huge page 할당
| CMA 매개변수 | 설명 | 기본값 |
|---|---|---|
cma= (부트 파라미터) |
기본 CMA 영역 크기 | 아키텍처/배포판 의존 (0~64MB) |
CONFIG_CMA_SIZE_MBYTES |
커널 빌드 시 기본 CMA 크기 | 16 또는 0 |
CONFIG_CMA_ALIGNMENT |
CMA 영역 정렬 (order) | 8 (= 1MB) |
CONFIG_CMA_AREAS |
최대 CMA 영역 수 | 7 (기본 + DT 정의) |
CMA vs Huge Pages: CMA는 필요할 때만 연속 영역을 확보하므로 메모리 효율이 높지만, 이주 비용이 있습니다. Huge Pages(HugeTLB)는 부팅 시 고정 예약하므로 즉시 사용 가능하지만 다른 용도로 전환할 수 없습니다. 6.x 커널에서는 hugetlb_cma= 파라미터로 CMA를 통해 huge page를 동적으로 할당할 수 있습니다.
모니터링 (Monitoring)
buddyinfo
cat /proc/buddyinfo
# Node 0, zone DMA 0 0 0 1 2 1 1 0 1 1 3
# Node 0, zone DMA32 1029 537 243 108 34 12 3 1 0 1 1
# Node 0, zone Normal 45321 12890 3210 823 211 52 13 3 1 0 0
# ^ ^
# order 0 (4KB) order 10 (4MB)
#
# 해석 방법:
# - 왼쪽(order 0)이 크면 정상 (소형 블록 충분)
# - 오른쪽(order 8~10)이 0이면 고차 할당 실패 가능 → 외부 단편화
# - 총 free 페이지 = sum(count[i] * 2^i)
# 예: Normal = 45321*1 + 12890*2 + 3210*4 + 823*8 + 211*16 + 52*32 + 13*64 + 3*128 + 1*256
pagetypeinfo
cat /proc/pagetypeinfo
# Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
# Node 0, zone Normal, type Unmovable 102 47 12 3 1 0 0 0 0 0 0
# Node 0, zone Normal, type Movable 4021 980 245 62 15 4 1 0 0 0 0
# Node 0, zone Normal, type Reclaimable 890 321 87 21 5 1 0 0 0 0 0
# Node 0, zone Normal, type HighAtomic 23 5 1 0 0 0 0 0 0 0 0
# Node 0, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
# Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0
#
# 핵심 분석:
# - Unmovable 고차 블록이 0이면 커널 할당에 단편화 영향
# - Movable에 고차 블록 존재 → compaction 불필요
# - HighAtomic이 비어 있으면 GFP_ATOMIC 할당 위험
vmstat 핵심 카운터
# 할당/해제 카운터
cat /proc/vmstat | grep -E "^(pgalloc|pgfree|pgactivate|pgdeactivate)"
# pgalloc_normal 12345678 ← ZONE_NORMAL에서 할당된 총 페이지 수
# pgfree 12340000 ← 해제된 총 페이지 수
# pgalloc - pgfree ≈ 현재 사용 중인 페이지 수
# Compaction 카운터
cat /proc/vmstat | grep compact
# compact_stall 45 ← direct compaction으로 인한 stall 횟수
# compact_fail 12 ← compaction 실패 횟수
# compact_success 33 ← 성공률: 33/(33+12) = 73%
# Reclaim 카운터
cat /proc/vmstat | grep -E "^(allocstall|pgscan|pgsteal)"
# allocstall_normal 89 ← ZONE_NORMAL에서 direct reclaim stall
# pgscan_kswapd 567890 ← kswapd가 스캔한 페이지 수
# pgsteal_kswapd 345678 ← kswapd가 회수한 페이지 수
# pgscan_direct 12340 ← direct reclaim 스캔 (높으면 문제)
# OOM 카운터
cat /proc/vmstat | grep oom
# oom_kill 0 ← OOM killer 발동 횟수 (0이 정상)
zoneinfo 상세 분석
# Zone 상세 정보 (Normal zone 예시)
cat /proc/zoneinfo | grep -A30 "zone Normal"
# Node 0, zone Normal
# pages free 45321
# boost 0 ← watermark_boost 현재 값
# min 1440 ← WMARK_MIN
# low 3600 ← WMARK_LOW
# high 5760 ← WMARK_HIGH
# spanned 4194304 ← zone이 포함하는 전체 PFN 범위
# present 4128768 ← 실제 존재하는 페이지 수 (구멍 제외)
# managed 3932160 ← Buddy가 관리하는 페이지 수
# cma 65536 ← CMA 예약 페이지 수
# protection: (0, 0, 0, 0) ← lowmem_reserve
# pagesets
# cpu: 0
# count: 47
# high: 186
# batch: 31
# ...
#
# 건강 상태 판단:
# free > high → 정상
# low < free < high → kswapd 활동 중
# min < free < low → direct reclaim 가능
# free < min → 위험 (OOM 가능)
성능 최적화 (Performance)
Buddy Allocator의 성능은 크게 두 가지 요소에 좌우됩니다: (1) PCP 캐시 hit rate와 (2) Zone lock 경합 수준입니다. 대부분의 할당은 order 0이므로, PCP 최적화가 전체 성능에 가장 큰 영향을 미칩니다.
할당/해제 성능 비교
| 경로 | 대략적 비용 | Lock | 발생 빈도 |
|---|---|---|---|
| PCP hit (order 0) | ~15 ns | local_lock (IRQ disable) | 매우 높음 (90%+) |
| PCP refill (order 0, 캐시 고갈) | ~80 ns | zone->lock (spinlock) | 낮음 (batch 단위) |
| Buddy freelist (order 1+) | ~60 ns | zone->lock | 보통 |
| Buddy split 필요 (상위 order) | ~100 ns | zone->lock | 낮음 |
| Slow Path (kswapd wake) | ~1 us | zone->lock + IPI | 드뭄 |
| Direct Reclaim | ~100 us ~ 수 ms | 다수 lock | 드뭄 |
| Direct Compaction | ~1 ms ~ 수십 ms | 다수 lock + 페이지 이동 | 드뭄 |
| OOM Killer | ~10 ms+ | oom_lock + 프로세스 종료 | 매우 드뭄 |
Per-CPU 페이지 캐시 (PCP)
/* mm/page_alloc.c */
struct per_cpu_pages {
int count; /* 현재 캐시된 페이지 수 */
int high; /* 고수위 (초과 시 batch만큼 반환) */
int batch; /* Buddy로 한 번에 채우거나 반환할 크기 */
struct list_head lists[MIGRATE_PCPTYPES]; /* Migrate Type별 리스트 */
};
/* order 0 할당: Per-CPU 캐시 → lock-free 경로 */
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
struct zone *zone, gfp_t gfp_flags,
enum migratetype migratetype)
{
struct per_cpu_pages *pcp;
struct list_head *list;
struct page *page;
local_lock(&pagesets.lock); /* CPU-local lock만 필요 */
pcp = this_cpu_ptr(zone->per_cpu_pageset);
list = &pcp->lists[migratetype];
if (list_empty(list)) {
/* 캐시 고갈 시 Buddy에서 batch 단위로 보충 */
pcp->count += rmqueue_bulk(zone, 0, pcp->batch, list, migratetype);
}
page = list_first_entry(list, struct page, lru);
list_del(&page->lru);
pcp->count--;
local_unlock(&pagesets.lock);
return page;
}
| 매개변수 | 기본값 | 설명 |
|---|---|---|
batch |
RAM 크기 비례 | Buddy로 한 번에 채우는 페이지 수 |
high |
batch × 6 | 캐시 상한 (초과 시 batch만큼 Buddy에 반환) |
PCP 튜닝: /proc/sys/vm/percpu_pagelist_high_fraction 값을 늘리면 PCP 고수위가 높아져 Buddy lock 경합이 줄어듭니다. 단, per-CPU 메모리 사용량도 함께 증가합니다.
단편화 문제 (Fragmentation)
Buddy Allocator의 가장 큰 적은 외부 단편화(external fragmentation)입니다. 전체 free 메모리는 충분하지만 연속된 큰 블록이 없어 고차 할당이 실패하는 현상입니다. 이는 시간이 지남에 따라 UNMOVABLE 페이지가 여기저기 박히면서 악화됩니다.
단편화 유형 비교
| 유형 | 원인 | 영향 | 해결책 |
|---|---|---|---|
| 외부 단편화 | free 블록이 흩어져 있음 | 고차(order 2+) 할당 실패 | Compaction, Migrate Type 분리, CMA |
| 내부 단편화 | 요청보다 큰 2^n 블록 할당 | 사용하지 않는 페이지 낭비 | Slab Allocator (소형 객체), sub-page 관리 |
| NUMA 단편화 | 특정 노드만 고갈 | 원격 노드 접근으로 latency 증가 | NUMA balancing, zone_reclaim_mode |
단편화 정도 측정
/* mm/vmstat.c - 단편화 지수 계산 */
/*
* extfrag_index: -1 ~ 1000
* -1: 할당 실패가 메모리 부족 때문 (단편화 아님)
* 0: 완벽한 연속 (단편화 없음)
* 500: extfrag_threshold 기본값 (compaction 트리거)
* 1000: 극심한 단편화
*
* 계산:
* free_blocks = order 이상의 총 free 블록 수
* total_free = 전체 free 페이지 수
* required = 1 << order
*
* if (total_free < required) → -1 (메모리 부족)
* index = 1000 - (1000 * free_blocks * required / total_free)
*/
/* /sys/kernel/debug/extfrag/extfrag_index 출력 예:
* Node 0, zone Normal -1 -1 -1 -1 0 0 512 750 890 950 980
* ^ ^ ^
* order 0 order 4 order 10
* → order 6 이상에서 단편화 시작, order 10은 심각
*/
# 단편화 정도를 파악하는 종합 명령
# 1. 단편화 지수 (order별)
cat /sys/kernel/debug/extfrag/extfrag_index
# 2. 비정상적 단편화 경고 (unusual_extfrag)
cat /sys/kernel/debug/extfrag/unusable_index
# 3. compaction 효과 측정 (전후 비교)
cat /proc/buddyinfo > /tmp/before_compact
echo 1 > /proc/sys/vm/compact_memory
cat /proc/buddyinfo > /tmp/after_compact
diff /tmp/before_compact /tmp/after_compact
# 4. vmstat에서 compaction 관련 통계
cat /proc/vmstat | grep -E "compact|pgmigrate|allocstall"
# compact_stall: direct compaction 호출 횟수 (높으면 문제)
# compact_success: 성공률 확인
# compact_fail: 실패율 확인
# pgmigrate_success: 이동 성공 페이지 수
# pgmigrate_fail: 이동 실패 페이지 수
# allocstall_normal: ZONE_NORMAL direct reclaim stall 횟수
해결책 정리:
- 메모리 Compaction — MOVABLE 페이지를 이동하여 빈 공간 통합 (
echo 1 > /proc/sys/vm/compact_memory) - Anti-fragmentation — Migrate type별 분리 (Unmovable/Movable/Reclaimable pageblock)
- Huge Pages 사전 예약 — 부팅 시 hugepage를 미리 확보하여 런타임 고차 할당 회피
- CMA 영역 설정 — DMA용 연속 영역을 사전 예약하여 필요 시 보장
- min_free_kbytes 조정 — 여유 메모리 확보로 단편화 발생 가능성 감소
- watermark_scale_factor 증가 — kswapd가 더 일찍/적극적으로 회수
단편화 비상 대응: 고차 할당이 반복 실패하고 compaction도 효과가 없다면:
/proc/pagetypeinfo에서 UNMOVABLE 블록이 대부분인지 확인- UNMOVABLE이 과다하면 커널 slab 누수 의심 →
/proc/slabinfo확인 - 급한 경우
echo 3 > /proc/sys/vm/drop_caches로 pagecache 해제 후 compaction - 장기 해결:
vm.min_free_kbytes상향,vm.watermark_scale_factor증가
디버깅 및 진단 (Debugging)
단편화 상태 확인
# 1. Buddy Allocator 상태 확인
cat /proc/buddyinfo
# Node 0, zone Normal 10 5 3 2 1 1 0 0 0 0 0
# → 큰 order(오른쪽)의 값이 0이면 외부 단편화 심각
# 2. Migrate Type별 블록 분포 확인
cat /proc/pagetypeinfo
# 3. Zone 워터마크 수준 확인
cat /proc/zoneinfo | grep -E "(zone|pages free|min|low|high)"
# 4. 외부 단편화 지수 (0=완벽, 1000=최악)
cat /sys/kernel/debug/extfrag/extfrag_index
# 5. 즉시 Compaction 강제 실행
echo 1 > /proc/sys/vm/compact_memory
할당 실패 추적 (ftrace)
# mm_page_alloc_extfrag: Migrate Type Fallback 발생 시 기록
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc_extfrag/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예: mm_page_alloc_extfrag: page=... alloc_order=2
# alloc_migratetype=0 fallback_migratetype=1 change_ownership=1
# mm_page_alloc: 모든 페이지 할당 추적
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable
커널 트레이스포인트
/* include/trace/events/kmem.h */
/* 페이지 할당 성공 시 발생 */
TRACE_EVENT(mm_page_alloc,
TP_PROTO(struct page *page, unsigned int order,
gfp_t gfp_flags, enum migratetype migratetype),
...
);
/* Migrate Type Fallback 시 발생 → 단편화 악화 신호 */
TRACE_EVENT(mm_page_alloc_extfrag,
TP_PROTO(struct page *page,
int alloc_order, int fallback_order,
int alloc_migratetype, int fallback_migratetype),
...
);
OOM 발생 전 확인 체크리스트:
/proc/buddyinfo— 큰 order의 free block이 완전히 0인지/proc/meminfo의MemFree,MemAvailable차이 확인dmesg | grep -i "page allocation failure"— 할당 실패 스택 추적dmesg | grep -i "oom"— OOM Killer 발동 이력 확인
Buddy 단편화 대응 플레이북
Buddy allocator 장애의 핵심은 "총 메모리는 충분한데 고차 블록이 없는 상태"입니다. 따라서 free memory 총량보다 order별 가용량을 우선 분석해야 합니다.
진단 단계
- 증상 확인:
dmesg | grep -i "page allocation failure"로 할당 실패 확인 - 고차 블록 확인:
/proc/buddyinfo에서 order 8~10이 0인지 확인 - Migrate Type 분포:
/proc/pagetypeinfo에서 UNMOVABLE이 과다한지 확인 - fallback 빈도 확인:
mm_page_alloc_extfragtracepoint로 steal 빈도 확인 - 회수/압축 상태 확인:
/proc/vmstat에서compact_stall/success/fail비율 점검 - 요청 특성 조정: 큰 연속 물리 메모리 요청 경로(CMA, hugepage) 재검토
대응 조치 우선순위
| 우선순위 | 조치 | 효과 | 부작용 |
|---|---|---|---|
| 1 (즉시) | echo 1 > /proc/sys/vm/compact_memory |
free 블록 통합 | 일시적 latency spike |
| 2 (즉시) | echo 3 > /proc/sys/vm/drop_caches |
pagecache 해제 후 compaction 효과 증대 | I/O 성능 일시 저하 |
| 3 (조정) | vm.min_free_kbytes 상향 |
예비 free 메모리 확보 | 사용 가능 메모리 감소 |
| 4 (조정) | vm.watermark_scale_factor 증가 |
kswapd 조기/적극 회수 | 불필요한 회수 가능 |
| 5 (설계) | CMA 영역 사전 예약 | DMA 연속 할당 보장 | 메모리 유연성 감소 |
| 6 (설계) | HugeTLB 사전 예약 | 고차 할당 요구 제거 | 고정 메모리 사용량 |
# 종합 단편화 진단 스크립트
echo "=== 1. 할당 실패 이력 ==="
dmesg | grep -i "page allocation failure" | tail -5
echo "=== 2. buddyinfo (고차 블록 확인) ==="
cat /proc/buddyinfo
echo "=== 3. Migrate Type 분포 ==="
cat /proc/pagetypeinfo | head -n 30
echo "=== 4. 단편화 지수 ==="
cat /sys/kernel/debug/extfrag/extfrag_index 2>/dev/null || echo "(debugfs 미마운트)"
echo "=== 5. Compaction 통계 ==="
cat /proc/vmstat | grep -E "compact|allocstall"
echo "=== 6. 워터마크 상태 ==="
cat /proc/zoneinfo | grep -E "(^Node|zone |pages free|min |low |high )"
echo "=== 7. OOM 이력 ==="
dmesg | grep -i "oom" | tail -5
bpftrace로 실시간 모니터링:
# order 2 이상 할당 추적 (실시간)
bpftrace -e 'tracepoint:kmem:mm_page_alloc /args->order >= 2/ {
printf("pid=%d comm=%s order=%d gfp=0x%x\n",
pid, comm, args->order, args->gfp_flags);
}'
# migrate type fallback 추적
bpftrace -e 'tracepoint:kmem:mm_page_alloc_extfrag {
printf("order=%d alloc_mt=%d fallback_mt=%d\n",
args->alloc_order, args->alloc_migratetype,
args->fallback_migratetype);
}'
성능 최적화 가이드
Buddy Allocator의 성능은 대부분의 워크로드에서 충분히 빠르지만, 특정 시나리오에서는 튜닝이 필요합니다.
시나리오별 최적화
| 시나리오 | 증상 | 권장 조치 |
|---|---|---|
| 네트워크 패킷 처리 (높은 order 0 할당률) | Zone lock 경합, compact_stall 증가 |
PCP batch/high 증가, percpu_pagelist_high_fraction 조정 |
| 대규모 데이터베이스 (THP 사용) | THP 할당 실패, compact_stall 높음 |
transparent_hugepage=madvise, 사전 hugepage 예약 |
| 가상화 호스트 (메모리 핫플러그) | 오프라인 실패, UNMOVABLE 페이지 | kernelcore=로 ZONE_MOVABLE 분리 |
| 임베디드 DMA 디바이스 | 연속 메모리 할당 실패 | CMA 영역 사전 설정 (cma=256M) |
| NUMA 서버 (불균형 할당) | 특정 노드 고갈, 원격 접근 증가 | vm.zone_reclaim_mode=1, numactl 활용 |
| 실시간 시스템 (latency 요구) | direct reclaim/compaction latency spike | min_free_kbytes 대폭 상향, GFP_ATOMIC 사용 |
| 메모리 집약 작업 (빈번한 OOM) | OOM killer 발동 | vm.overcommit_memory=2, swap 확보, 프로세스 OOM score 조정 |
PCP 튜닝 상세
# 현재 PCP 상태 확인 (CPU별)
cat /proc/zoneinfo | grep -A5 "cpu:"
# cpu: 0
# count: 47 ← 현재 캐시된 페이지
# high: 186 ← 상한
# batch: 31 ← 교환 단위
# PCP high를 Zone managed 페이지의 1/8로 설정
echo 8 > /proc/sys/vm/percpu_pagelist_high_fraction
# → high = managed_pages / 8 / num_online_cpus
# 기본 자동 조정으로 되돌리기
echo 0 > /proc/sys/vm/percpu_pagelist_high_fraction
# PCP 강제 비우기 (진단용)
# 커널 내부: drain_all_pages(NULL)
# 유저 공간에서 직접 접근 불가, CPU offline/online으로 간접 trigger
커널 빌드 관련 CONFIG 옵션
| CONFIG 옵션 | 기본값 | 영향 |
|---|---|---|
CONFIG_COMPACTION |
y | 메모리 compaction 활성화 |
CONFIG_MIGRATION |
y | 페이지 이동(migrate_pages) 활성화 |
CONFIG_CMA |
y (배포판 의존) | CMA 활성화 |
CONFIG_MEMORY_HOTPLUG |
y (서버) | 메모리 핫플러그 지원 |
CONFIG_MEMORY_HOTREMOVE |
y (서버) | 메모리 핫리무브 지원 |
CONFIG_TRANSPARENT_HUGEPAGE |
y | THP 활성화 (compaction 의존) |
CONFIG_ZONE_DMA |
y (x86) | ZONE_DMA (0~16MB) 활성화 |
CONFIG_ZONE_DMA32 |
y (64비트) | ZONE_DMA32 (0~4GB) 활성화 |
CONFIG_ZONE_DEVICE |
y (서버) | ZONE_DEVICE (PMEM/GPU) 활성화 |
CONFIG_DEBUG_PAGEALLOC |
n | 해제된 페이지를 독(poison)으로 채움 (UAF 감지, 성능 저하) |
CONFIG_PAGE_OWNER |
n | 페이지 할당 추적 (메모리 누수 디버깅) |
# PAGE_OWNER 활성화 시 페이지 할당 추적
# 부트 파라미터: page_owner=on
cat /sys/kernel/debug/page_owner | head -n 50
# Page allocated via order 0, mask 0x1100cca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO)
# PFN 262144 type Movable Block 512 type Movable
# [page_owner_save_stack+0x38/0x60]
# [post_alloc_hook+0x1c8/0x200]
# [__alloc_pages+0x2d4/0x350]
# ...
# 상위 할당자별 페이지 사용량 요약
cat /sys/kernel/debug/page_owner | sort | uniq -c | sort -rn | head -20
성능 측정: Buddy Allocator의 성능을 측정하려면 perf를 사용하세요:
# __alloc_pages 호출 빈도 및 시간 측정
perf stat -e 'kmem:mm_page_alloc' -a sleep 10
# → 10초간 페이지 할당 횟수
# zone lock 경합 확인
perf lock record -a sleep 5
perf lock report
sysctl/proc 레퍼런스
Buddy Allocator 관련 주요 sysctl 매개변수와 /proc 파일을 종합적으로 정리합니다.
| 경로 | R/W | 설명 |
|---|---|---|
/proc/buddyinfo |
R | Node/Zone별 order별 free 블록 수 |
/proc/pagetypeinfo |
R | Migrate Type별 order별 free 블록 수 |
/proc/zoneinfo |
R | Zone별 상세 정보 (워터마크, 통계, PCP) |
/proc/vmstat |
R | VM 통계 (pgalloc, pgfree, compact, pgmigrate 등) |
/proc/meminfo |
R | 시스템 메모리 개요 (MemFree, MemAvailable, CmaTotal 등) |
vm.min_free_kbytes |
RW | WMARK_MIN 기준 (모든 Zone 합산 kB) |
vm.watermark_scale_factor |
RW | LOW/HIGH 워터마크 간격 (0.01%~10%) |
vm.watermark_boost_factor |
RW | 단편화 시 HIGH 워터마크 boost 상한 |
vm.lowmem_reserve_ratio |
RW | Zone 간 lowmem_reserve 비율 |
vm.percpu_pagelist_high_fraction |
RW | PCP high를 Zone 페이지의 1/N으로 설정 |
vm.compact_memory |
W | 1 기록 시 전체 메모리 compaction 수행 |
vm.extfrag_threshold |
RW | compaction 트리거 단편화 임계값 (기본 500) |
vm.drop_caches |
W | 1=pagecache, 2=dentries/inodes, 3=둘 다 해제 |
vm.overcommit_memory |
RW | 0=heuristic, 1=always, 2=never 오버커밋 |
vm.zone_reclaim_mode |
RW | NUMA: 로컬 zone 회수 정책 (0=off, 1~7) |
# Buddy Allocator 상태 종합 진단 스크립트
echo "=== buddyinfo ==="
cat /proc/buddyinfo
echo "=== Zone Watermarks ==="
cat /proc/zoneinfo | grep -E "(^Node|zone |pages free|min |low |high |managed)"
echo "=== VM Statistics (allocation) ==="
cat /proc/vmstat | grep -E "(pgalloc|pgfree|pgsteal|compact|allocstall)"
echo "=== CMA ==="
grep Cma /proc/meminfo
echo "=== PCP per zone ==="
cat /proc/zoneinfo | grep -A3 "cpu:" | head -n 40
echo "=== Key sysctl values ==="
sysctl vm.min_free_kbytes vm.watermark_scale_factor vm.watermark_boost_factor
커널 소스 및 참고 자료
주요 소스 파일
| 파일 | 역할 |
|---|---|
mm/page_alloc.c |
Buddy Allocator 핵심 (alloc/free/split/coalesce/PCP) |
include/linux/mmzone.h |
zone, free_area, per_cpu_pages 정의 |
include/linux/gfp.h |
GFP 플래그, alloc_pages() 선언 |
include/linux/mm_types.h |
struct page, struct folio 정의 |
include/linux/page-flags.h |
PG_buddy, PG_slab 등 페이지 플래그 |
mm/compaction.c |
메모리 compaction (compact_zone, kcompactd) |
mm/memory_hotplug.c |
메모리 핫플러그 (online/offline) |
mm/cma.c |
CMA 할당/해제 |
mm/page_isolation.c |
pageblock 격리 (MIGRATE_ISOLATE) |
mm/internal.h |
내부 헬퍼 함수 (set_buddy_order 등) |
외부 참고 자료
- Kernel Documentation: Memory Management (admin-guide)
- Kernel Documentation: Memory Allocation Guide
- Kernel Documentation: Page Allocator
- LWN: Anti-fragmentation (Mel Gorman, 2006)
- LWN: Memory Compaction (Mel Gorman, 2010)
- LWN: Folio pages (Matthew Wilcox, 2021)
- LWN: CMA (Contiguous Memory Allocator)
- Mel Gorman, Understanding the Linux Virtual Memory Manager (kernel.org)