관련 소스:mm/huge_memory.c (4978줄),mm/khugepaged.c (2873줄),include/linux/huge_mm.h (807줄)
관련 문서: VMA / mmap · Buddy Allocator · Folio / Page Cache · Compaction · 메모리 관리 개요
Linux 커널의 Transparent Huge Pages (THP) 은 익명 메모리 영역에 대해 4KB 대신 2MB (PMD 크기) 또는 그보다 큰 거대 페이지를 자동으로 할당/병합하는 메커니즘입니다. THP는 TLB(Translation Lookaside Buffer) 미스를 줄여 메모리 접근 성능을 향상시키며, khugepaged 백그라운드 스레드가 기존 4KB 페이지 세트를 거대 페이지로 "병합(collapse)"합니다.
Huge Pages는 크게 두 가지 유형이 있습니다:
Linux 7.0에서는 THP의 order 범위가 확장되어 PMD 크기(2MB)뿐 아니라 그보다 작은 mTHP(multi-size THP, large folio) (order-2 ~ order-9)도 지원합니다. khugepaged는 익명 페이지와 파일 기반 페이지 모두에서 병합을 시도합니다. 또한 compound page는 커널 5.16+에서 folio 추상화로 감싸져 head/tail page 구분이 제거되었습니다.
일상 비유: Huge Page는 일반 도서관 책장(4KB 페이지) 대신 대형 사전(2MB)을 꺼내는 것과 같습니다. 자주 찾는 페이지를 하나로 묶으면 책장 넘기는 횟수(TLB 미스)가 줄어듭니다. HugeTLB는 미리 대형 사전을 예약하는 것이고, THP는 필요할 때 자동으로 대형 사전으로 묶어주는 것입니다.
/* 주요 소스 파일 경로 */
mm/huge_memory.c /* THP 폴트 처리, sysfs 인터페이스, 제로 페이지 관리 */
mm/khugepaged.c /* 백그라운드 병합 스레드 (khugepaged) */
include/linux/huge_mm.h /* THP 관련 자료구조, 매크로, API 선언 */
# THP 활성화 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never ← 현재 THP 모드
# THP 디프래그(defrag) 정책 확인
cat /sys/kernel/mm/transparent_hugepage/defrag
# [always] defer defer+madvise madvise never
# PMD 크기 확인 (보통 2MB = 512 * 4KB)
cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size
# 2097152 (2MB in bytes)
# khugepaged 스캔/병합 통계
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed
cat /sys/kernel/mm/transparent_hugepage/khugepaged/full_scans
# 시스템 HugePages 상태
grep -i huge /proc/meminfo
# 프로세스별 THP 사용량 (RSS 내 large folio)
cat /proc/<pid>/smaps | grep -A 5 -i huge
# THP 관련 vmstat 이벤트
cat /proc/vmstat | grep -E 'thp_|khugepaged'
# MADV_HUGEPAGE 적용된 VMA 확인
cat /proc/<pid>/smaps | grep -B 2 -i huge
THP 동작을 제어하는 전역 플래그입니다. 각 비트는 enum transparent_hugepage_flag로 정의됩니다.
/* mm/huge_memory.c:59-68 — THP 전역 플래그 초기화 */
unsigned long transparent_hugepage_flags __read_mostly =
#ifdef CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS
(1<<TRANSPARENT_HUGEPAGE_FLAG)|
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE_MADVISE
(1<<TRANSPARENT_HUGEPAGE_REQ_MADV_FLAG)|
#endif
(1<<TRANSPARENT_HUGEPAGE_DEFRAG_REQ_MADV_FLAG)|
(1<<TRANSPARENT_HUGEPAGE_DEFRAG_KHUGEPAGED_FLAG)|
(1<<TRANSPARENT_HUGEPAGE_USE_ZERO_PAGE_FLAG);
/* include/linux/huge_mm.h:49-59 — THP 플래그 열거형 */
enum transparent_hugepage_flag {
TRANSPARENT_HUGEPAGE_UNSUPPORTED,
TRANSPARENT_HUGEPAGE_FLAG, /* always 모드 */
TRANSPARENT_HUGEPAGE_REQ_MADV_FLAG, /* madvise 모드 */
TRANSPARENT_HUGEPAGE_DEFRAG_DIRECT_FLAG,
TRANSPARENT_HUGEPAGE_DEFRAG_KSWAPD_FLAG,
TRANSPARENT_HUGEPAGE_DEFRAG_KSWAPD_OR_MADV_FLAG,
TRANSPARENT_HUGEPAGE_DEFRAG_REQ_MADV_FLAG,
TRANSPARENT_HUGEPAGE_DEFRAG_KHUGEPAGED_FLAG,
TRANSPARENT_HUGEPAGE_USE_ZERO_PAGE_FLAG,
};
khugepaged가 순회할 mm 목록의 커서 역할을 합니다.
/* mm/khugepaged.c:107-123 — 스캔 커서 구조체 */
struct khugepaged_scan {
struct list_head mm_head; /* mm 목록 머리 */
struct mm_slot *mm_slot; /* 현재 스캔 중인 mm_slot */
unsigned long address; /* 다음 스캔할 가상 주소 */
};
static struct khugepaged_scan khugepaged_scan = {
.mm_head = LIST_HEAD_INIT(khugepaged_scan.mm_head),
};
병합 작업의 성격과 NUMA 노드별 로드 정보를 관리합니다.
/* mm/khugepaged.c:97-105 — 병합 제어 구조체 */
struct collapse_control {
bool is_khugepaged; /* khugepaged initiated vs MADV_COLLAPSE */
u32 node_load[MAX_NUMNODES]; /* 노드별 스캔된 페이지 수 */
nodemask_t alloc_nmask; /* 할당 폴백 nodemask */
};
PTE 영역 스캔 시 각 조건별 결과를 정의합니다. khugepaged는 이 값에 따라 병합을 시도하거나 중단합니다.
/* mm/khugepaged.c:31-63 — 스캔 결과 열거형 (실제 소스 기준) */
enum scan_result {
SCAN_FAIL, /* 일반 실패 */
SCAN_SUCCEED, /* 스캔 성공 → 병합 진행 */
SCAN_NO_PTE_TABLE, /* PMD가 없음 (PTE 테이블 미할당) */
SCAN_PMD_MAPPED, /* 이미 THP로 매핑됨 */
SCAN_EXCEED_NONE_PTE, /* 빈 PTE 허용 초과 (max_ptes_none) */
SCAN_EXCEED_SWAP_PTE, /* 스왑 PTE 초과 (max_ptes_swap) */
SCAN_EXCEED_SHARED_PTE, /* 공유 페이지 초과 (max_ptes_shared) */
SCAN_PTE_NON_PRESENT, /* 존재하지 않는 PTE */
SCAN_PTE_UFFD_WP, /* userfaultfd 쓰기 보호 */
SCAN_PTE_MAPPED_HUGEPAGE, /* PTE로 매핑된 hugepage */
SCAN_LACK_REFERENCED_PAGE, /* 접근된 페이지 부족 */
SCAN_SCAN_ABORT, /* NUMA 로드 기반 스캔 중단 */
SCAN_PAGE_COUNT, /* ref count 불일치 */
SCAN_PAGE_LRU, /* LRU에 없음 */
SCAN_PAGE_LOCK, /* 페이지 잠김 */
SCAN_PAGE_ANON, /* 익명 페이지 아님 */
SCAN_PAGE_COMPOUND, /* compound page (이미 THP) */
SCAN_ANY_PROCESS, /* 프로세스 검사 실패 */
SCAN_VMA_NULL, /* VMA가 NULL */
SCAN_VMA_CHECK, /* VMA 검사 실패 */
SCAN_ADDRESS_RANGE, /* 주소 범위 오류 */
SCAN_DEL_PAGE_LRU, /* LRU에서 제거 실패 */
SCAN_ALLOC_HUGE_PAGE_FAIL, /* hugepage 할당 실패 */
SCAN_CGROUP_CHARGE_FAIL, /* memcg 충전 실패 */
SCAN_TRUNCATED, /* 잘린 매핑 */
SCAN_PAGE_HAS_PRIVATE, /* private 데이터 존재 (파일 기반) */
SCAN_STORE_FAILED, /* PTE 저장 실패 */
SCAN_COPY_MC, /* 복사 중 메모리 침해 */
SCAN_PAGE_FILLED, /* 페이지가 이미 채워짐 */
SCAN_PAGE_DIRTY_OR_WRITEBACK,/* 더럽거나 writeback 중 */
};
/* include/linux/huge_mm.h:79-95 — THP 지원 order 정의 */
/* 익명 THP: order-2 ~ PMD_ORDER (order-1은 THP 구조 제약으로 제외) */
#define THP_ORDERS_ALL_ANON ((BIT(PMD_ORDER + 1) - 1) & ~(BIT(0) | BIT(1)))
/* 특수(DAX/PFNMAP): PMD + PUD 크기만 지원 */
#define THP_ORDERS_ALL_SPECIAL (BIT(PMD_ORDER) | BIT(PUD_ORDER))
/* 파일 THP: MAX_PAGECACHE_ORDER까지 지원 (order-0 제외) */
#define THP_ORDERS_ALL_FILE_DEFAULT \
((BIT(MAX_PAGECACHE_ORDER + 1) - 1) & ~BIT(0))
커널 내부에서 Huge Page는 compound page(커널 5.16+에서는 folio 추상화)로 관리됩니다. 연속된 물리 페이지를 하나의 논리 단위로 묶어 첫 번째 페이지(head page)가 전체를 대표합니다. 2MB huge page는 512개의 연속 struct page(order-9)로 구성됩니다.
folio는 compound page의 추상화로, head/tail page 구분을 제거하여 코드 안전성과 성능을 개선합니다. struct folio * 반환 API를 사용하는 것이 권장됩니다.
mTHP는 기존 PMD 크기(2MB) THP뿐 아니라 16KB/32KB/64KB 등 중간 크기의 large folio도 PTE 레벨에서 매핑할 수 있게 하는 메커니즘입니다. 2MB 연속 확보가 어려운 환경에서 유용하며, khugepaged의 large folio 병합과 연동됩니다.
PMD 레벨의 익명 페이지 폴트를 처리하는 진입점입니다. 읽기 전용 접근이면 zero page를 매핑하고, 쓰기 접근이면 새 THP를 할당합니다.
/* mm/huge_memory.c:1461-1516 — 익명 THP 폴트 메인 핸들러 */
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
vm_fault_t ret;
/* 1. VMA가 THP에 적합한지 검사 */
if (!thp_vma_suitable_order(vma, haddr, PMD_ORDER))
return VM_FAULT_FALLBACK; /* 4KB 페이지로 폴백 */
ret = vmf_anon_prepare(vmf);
if (ret)
return ret;
khugepaged_enter_vma(vma, vma->vm_flags);
/* 2. 읽기 전용 + zero page 사용 가능 → 제로 페이지 매핑 */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm) &&
transparent_hugepage_use_zero_page()) {
/* ... huge zero folio 매핑 ... */
return ret;
}
/* 3. 새 THP 할당 및 매핑 */
return __do_huge_pmd_anonymous_page(vmf);
}
THP에 대한 쓰기 폴트(COW)를 처리합니다. 페이지가 exclusive이면 재사용하고, 그렇지 않으면 THP를 쪼개서 4KB로 폴백합니다.
/* mm/huge_memory.c:2060-2152 — THP COW 처리 */
vm_fault_t do_huge_pmd_wp_page(struct vm_fault *vmf)
{
/* ... */
page = pmd_page(orig_pmd);
folio = page_folio(page);
/* 1. exclusive 페이지 → 재사용 (복사 불필요) */
if (PageAnonExclusive(page))
goto reuse;
/* 2. 참조 카운트가 1이면 exclusive 확보 가능 */
if (folio_ref_count(folio) == 1) {
folio_move_anon_rmap(folio, vma);
SetPageAnonExclusive(page);
goto reuse;
}
/* 3. 여러 프로세스가 공유 중 → THP를 쪼개고 4KB COW로 폴백 */
fallback:
__split_huge_pmd(vma, vmf->pmd, vmf->address, false);
return VM_FAULT_FALLBACK;
}
khugepaged 또는 MADV_COLLAPSE에 의해 호출되며, 기존 4KB 페이지 세트를 하나의 THP로 병합합니다.
/* mm/khugepaged.c:1078-1231 — THP 병합 핵심 */
static enum scan_result collapse_huge_page(struct mm_struct *mm,
unsigned long address, int referenced, int unmapped,
struct collapse_control *cc)
{
/* 1. mmap_lock 해제 후 hugepage 할당 (시간이 걸릴 수 있음) */
mmap_read_unlock(mm);
result = alloc_charge_folio(&folio, mm, cc);
/* 2. VMA/PMD 재검증 */
mmap_read_lock(mm);
result = hugepage_vma_revalidate(mm, address, true, &vma, cc);
/* 3. 스왑인 처리 (unmapped PTE가 있는 경우) */
if (unmapped)
result = __collapse_huge_page_swapin(mm, vma, address, pmd, referenced);
/* 4. mmap_write_lock 획득 → 안전한 페이지 테이블 수정 */
mmap_write_lock(mm);
_pmd = pdp_collapse_flush(vma, address, pmd); /* 기존 PMD 제거 */
/* 5. PTE 페이지 격리 */
result = __collapse_huge_page_isolate(vma, address, pte, cc, &compound_pagelist);
/* 6. 메모리 복사 (4KB → THP) */
result = __collapse_huge_page_copy(pte, folio, pmd, _pmd, vma, address, pte_ptl, ...);
/* 7. 새 THP 매핑 설치 */
map_anon_folio_pmd_nopf(folio, pmd, vma, address);
}
PMD 영역의 512개 PTE를 순회하며 병합 가능한지 판별합니다.
/* mm/khugepaged.c:1233-1401 — PMD 영역 스캔 */
static enum scan_result hpage_collapse_scan_pmd(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long start_addr, ...)
{
/* 512개 PTE 순회 */
for (addr = start_addr, _pte = pte; _pte < pte + HPAGE_PMD_NR; _pte++) {
if (pte_none_or_zero(pteval)) {
++none_or_zero;
/* none 초과 → 스캔 중단 */
}
if (!pte_present(pteval)) {
++unmapped;
/* swap PTE 초과 → 스캔 중단 */
}
if (folio_maybe_mapped_shared(folio)) {
++shared;
/* 공유 페이지 초과 → 스캔 중단 */
}
/* NUMA 노드별 로드 기록 */
cc->node_load[folio_nid(folio)]++;
}
/* referenced 페이지 부족 → 병합 안 함 */
if (!referenced || (unmapped && referenced < HPAGE_PMD_NR / 2))
result = SCAN_LACK_REFERENCED_PAGE;
else
result = collapse_huge_page(mm, start_addr, referenced, unmapped, cc);
}
백그라운드에서 주기적으로 모든 등록된 mm을 스캔하여 병합을 시도합니다.
/* mm/khugepaged.c:2612-2631 — khugepaged 메인 루프 */
static int khugepaged(void *none)
{
set_freezable();
set_user_nice(current, MAX_NICE); /* 최저 우선순위 */
while (!kthread_should_stop()) {
khugepaged_do_scan(&khugepaged_collapse_control);
khugepaged_wait_work(); /* scan_sleep_millisecs 동안 대기 */
}
return 0;
}
CPU 폴트 발생
→ handle_mm_fault()
→ __handle_mm_fault()
→ handle_pmd_fault()
→ do_huge_pmd_anonymous_page() ← THP 진입점
├─ thp_vma_suitable_order() VMA 정렬/크기 검사
├─ vmf_anon_prepare() anon_vma 준비
├─ khugepaged_enter_vma() mm 등록 (미등록 시)
├─ zero page 경로:
│ └─ set_huge_zero_folio() 제로 페이지 매핑
└─ 일반 경로:
└─ __do_huge_pmd_anonymous_page()
├─ vma_alloc_anon_folio_pmd() 새 THP 할당
├─ pte_alloc_one() 하위 PTE 테이블 할당
└─ map_anon_folio_pmd_pf() PMD 매핑 설치
쓰기 폴트 (THP 영역)
→ handle_mm_fault()
→ __handle_mm_fault()
→ handle_pmd_fault()
→ do_huge_pmd_wp_page() ← THP COW
├─ is_huge_zero_pmd()?
│ └─ do_huge_zero_wp_pmd() 제로 페이지 COW
├─ PageAnonExclusive? → reuse exclusive면 재사용
├─ folio_ref_count == 1? → reuse 단독 사용이면 재사용
└─ fallback:
├─ __split_huge_pmd() THP를 4KB로 분할
└─ VM_FAULT_FALLBACK 일반 4KB COW로 재시도
khugepaged 스레드 (백그라운드)
→ khugepaged()
→ khugepaged_do_scan()
→ khugepaged_scan_mm_slot() mm 순회
→ hpage_collapse_scan_pmd() PMD 영역 스캔
├─ find_pmd_or_thp_or_none() PMD 상태 확인
├─ PTE 512개 순회:
│ ├─ none_or_zero 체크
│ ├─ swap/unmapped 체크
│ ├─ shared 체크
│ └─ referenced 체크
└─ collapse_huge_page() 병합 실행
├─ alloc_charge_folio() THP 할당 + memcg 충전
├─ hugepage_vma_revalidate() VMA 재검증
├─ __collapse_huge_page_swapin() 스왑인
├─ pdp_collapse_flush() 기존 PMD 제거
├─ __collapse_huge_page_isolate() PTE 격리
├─ __collapse_huge_page_copy() 메모리 복사
└─ map_anon_folio_pmd_nopf() 새 THP 매핑
| 정책 | GFP 플래그 | 동작 | 적합한 경우 |
|---|---|---|---|
| **always** | `GFP_TRANSHUGE` | 즉시 compaction 시도, 실패 시 재시도 | 대용량 메모리 서버, 지연 허용 |
| **defer** | `GFP_TRANSHUGE_LIGHT + __GFP_KSWAPD_RECLAIM` | kcompactd에 위임, 빠른 실패 | 일반 데스크톱, 반응성 중요 |
| **defer+madvise** | `GFP_TRANSHUGE_LIGHT` | MADV_HUGEPAGE는 직접, 아니면 kcompactd | 혼합 워크로드 |
| **madvise** | `GFP_TRANSHUGE_LIGHT` | MADV_HUGEPAGE만 직접 compaction | 앱이 THP 의도 지정 |
| **never** | `GFP_TRANSHUGE_LIGHT` | compaction 없이 즉시 실패 | 실시간, 임베디드 |
| 모드 | 플래그 | 동작 |
|---|---|---|
| **always** | `TRANSPARENT_HUGEPAGE_FLAG` | 모든 VMA에 THP 적용 시도 |
| **madvise** | `TRANSPARENT_HUGEPAGE_REQ_MADV_FLAG` | `MADV_HUGEPAGE`가 설정된 VMA만 적용 |
| **never** | (both cleared) | THP 비활성화 (hugepage_madvise만 허용) |
| 스캔 결과 | khugepaged 동작 | MADV_COLLAPSE 반환값 |
|---|---|---|
| `SCAN_SUCCEED` | `collapse_huge_page()` 호출 | 성공 |
| `SCAN_ALLOC_HUGE_PAGE_FAIL` | alloc_sleep 후 재시도 또는 중단 | `-ENOMEM` |
| `SCAN_EXCEED_NONE_PTE` | 다음 PMD로 건너뜀 | `-EBUSY` |
| `SCAN_PAGE_COUNT`, `SCAN_PAGE_LOCK` | 다음 PMD로 건너뜀 | `-EAGAIN` |
| `SCAN_LACK_REFERENCED_PAGE` | 다음 PMD로 건너뜀 | `-EINVAL` |
| `SCAN_PTE_UFFD_WP` | 다음 PMD로 건너뜀 | `-EINVAL` |
| 유형 | 할당 시점 | 크기 | 관리 방식 | 비고 |
|---|---|---|---|---|
| **HugeTLB** | 부팅 시 사전 예약 (mmap) | 2MB / 1GB | 사용자가 명시적 mmap | DPDK, KVM, DB에 적합 |
| **THP (PMD)** | 폴트 시/병합 시 동적 | 2MB (512 * 4KB) | `khugepaged` 자동 병합 | compaction 비용 발생 가능 |
| **mTHP (large folio)** | 폴트 시 동적 | order-2 ~ order-9 (16KB~2MB) | PTE 레벨 large folio | 커널 6.8+ 신규, 2MB 확보 어려울 때 유용 |
| **folio** | 커널 5.16+ 추상화 | 모든 크기 | compound page 추상화 | head/tail 구분 제거, API 안정성 개선 |
| 파일 | 기본값 | 설명 |
|---|---|---|
| `enabled` | `madvise` | THP 활성화 모드 (always/madvise/never) |
| `defrag` | `madvise` | 디프래그 정책 |
| `use_zero_page` | `1` | 읽기 전용 THP에 zero page 사용 여부 |
| `hpage_pmd_size` | `2097152` | PMD 크기 (bytes) |
| `khugepaged/pages_to_scan` | `HPAGE_PMD_NR * 8` | 一次 스캔 시 스캔할 PTE 수 |
| `khugepaged/scan_sleep_millisecs` | `10000` | 스캔 간 대기 시간 (ms) |
| `khugepaged/alloc_sleep_millisecs` | `60000` | 할당 실패 후 대기 시간 (ms) |
| `khugepaged/max_ptes_none` | `HPAGE_PMD_NR - 1` | 허용 최대 빈 PTE 수 |
| `khugepaged/max_ptes_swap` | `HPAGE_PMD_NR / 8` | 허용 최대 스왑 PTE 수 |
| `khugepaged/max_ptes_shared` | `HPAGE_PMD_NR / 2` | 허용 최대 공유 PTE 수 |