MMU & TLB
MMU (Memory Management Unit)와 TLB (Translation Lookaside Buffer)는 가상 메모리(Virtual Memory)의 하드웨어 핵심입니다.
x86-64/ARM64 주소 변환(Address Translation), 4/5단계 페이지 테이블(Page Table) 워크, PTE 비트필드, TLB 내부 구조, mmu_gather 배칭,
페이지 폴트(Page Fault) 핸들링, COW(Copy-on-Write), KPTI(Kernel Page Table Isolation),
Huge Pages/THP TLB 최적화, PCID/ASID, 커널 주소 공간(Address Space) 레이아웃,
TLB 관련 보안 취약점(Vulnerability)과 완화 기법, 실전 성능 분석 사례까지 종합적으로 다룹니다.
핵심 요약
- MMU — 가상 주소(Virtual Address)를 물리 주소(Physical Address)로 변환하는 하드웨어 유닛입니다. x86-64에서는 CR3 레지스터가 페이지 테이블 베이스를 가리킵니다.
- TLB — 최근 주소 변환을 캐시(Cache)하여 페이지 테이블 워크를 생략합니다. 일반적으로 L1 ITLB/DTLB + L2 STLB 계층으로 구성됩니다.
- 페이지 테이블 워크 — TLB Miss 시 4~5단계 메모리 접근이 필요합니다. 커널은
pgd_offset()→p4d_offset()→pud_offset()→pmd_offset()→pte_offset_map()순으로 워크합니다. - TLB Shootdown — 페이지 테이블 변경 시
flush_tlb_mm()/flush_tlb_range()로 모든 CPU의 TLB를 무효화합니다. IPI(Inter-Processor Interrupt) 기반으로 비용이 큽니다. - ASID/PCID — Context Switch 시 TLB 전체 플러시(Flush)를 피하는 최적화 기법입니다. x86-64는 PCID(12비트), ARM64는 ASID(8/16비트)를 사용합니다.
- KPTI — Meltdown 대응으로 유저/커널 페이지 테이블을 분리하여 커널 매핑 노출을 차단합니다.
- Huge Pages — 2MB/1GB 페이지로 TLB 커버리지를 극대화하여 TLB Miss를 줄입니다.
# MMU/TLB 상태 확인
cat /proc/cpuinfo | grep -E 'pse|pge|pcid|la57' # MMU 관련 CPU 플래그
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses -- sleep 1 # TLB Miss 카운터
cat /proc/vmstat | grep -E 'nr_tlb|pgfault|thp' # TLB/폴트 통계
getconf PAGE_SIZE # 페이지 크기 (보통 4096)
설명
perf stat의 TLB Miss 카운터는 워크로드의 메모리 접근 패턴을 진단하는 핵심 지표입니다. dTLB-load-misses가 높으면 데이터 접근 지역성(Data Locality)이 낮거나 워킹셋이 TLB 용량을 초과하는 상태입니다. Huge Pages 적용이나 데이터 구조 최적화를 검토해야 합니다.
단계별 이해
- 핵심 요소 확인
자료구조(pgd_t/p4d_t/pud_t/pmd_t/pte_t)와 하드웨어 레지스터(CR3/TTBR0/TTBR1)를 먼저 정리합니다. - 주소 변환 흐름 추적
CPU 메모리 접근 → TLB Lookup → (Hit) 물리 주소 반환 / (Miss) Page Walker가 CR3 기반으로 PGD → P4D → PUD → PMD → PTE 순서로 메모리를 읽어 변환을 완료합니다. - 페이지 폴트 처리
PTE가 absent이거나 권한 위반이면#PF예외 →do_page_fault()→handle_mm_fault()→__handle_mm_fault()에서 VMA를 찾아 적절한 핸들러(do_anonymous_page(),do_fault(),do_wp_page())를 호출합니다. - TLB 관리
페이지 테이블 변경 후flush_tlb_range()/flush_tlb_page()로 stale 엔트리를 무효화합니다.mmu_gather프레임워크가 배칭하여 IPI 횟수를 줄입니다. - 성능 병목 점검
perf stat의 dTLB/iTLB Miss 카운터,/proc/vmstat의pgfault/pgmajfault,nr_tlb_remote_flush로 TLB Shootdown 빈도를 확인합니다.
개요 (Overview)
MMU (Memory Management Unit)는 CPU 코어 내부에 위치하며, 모든 메모리 접근을 가로채어 가상 주소를 물리 주소로 변환합니다. 이 과정은 완전히 하드웨어에서 수행되며, 커널은 페이지 테이블을 설정하고 TLB를 관리하는 역할을 합니다.
MMU 핵심 구성 요소
| 구성 요소 | 역할 | 위치 | 관리 주체 |
|---|---|---|---|
| TLB | 최근 변환 캐시 | CPU 내부 (L1/L2) | 하드웨어 + 커널 |
| Page Walker | TLB Miss 시 페이지 테이블 탐색 | CPU 내부 (전용 회로) | 하드웨어 |
| 페이지 테이블 | VA→PA 매핑(Mapping) 정의 | RAM (per-process) | 커널 소프트웨어 |
| CR3/TTBR | 현재 페이지 테이블 베이스 주소 | CPU 레지스터(Register) | 커널 (Context Switch) |
| PWC (Page Walk Cache) | 중간 테이블 엔트리 캐시 | CPU 내부 | 하드웨어 |
Page Walk Cache (PWC): 최신 프로세서는 TLB 외에도 PGD/PUD/PMD 수준의 중간 테이블 엔트리를 캐시합니다. TLB Miss가 발생해도 PWC Hit가 되면 전체 4단계를 모두 걸을 필요 없이 2~3단계만 걸으면 됩니다. Intel에서는 이를 Paging Structure Cache라 합니다.
Page Walk Cache (PWC) 상세
Page Walk Cache(PWC)는 페이지 테이블 계층의 중간 단계 엔트리를 전용으로 캐시합니다. TLB가 최종 가상→물리 주소 변환 쌍 전체를 저장하는 반면, PWC는 PGD/PUD/PMD 각 단계의 테이블 포인터(물리 주소)를 독립적으로 캐시합니다. 이를 통해 TLB Miss 발생 시에도 페이지 테이블 워크(Page Table Walk)의 깊이를 줄여 메모리 접근 횟수를 감소시킵니다.
Intel 공식 명칭은 Paging Structure Cache이며, 세 가지 하위 캐시로 구성됩니다.
- PML4E 캐시 (PGD Cache): PML4 테이블 엔트리를 캐시합니다. 히트 시 PDPT(PUD) 접근부터 시작하므로 4단계 워크가 3단계로 줄어듭니다.
- PDPTE 캐시 (PUD Cache): PDPT 엔트리를 캐시합니다. 히트 시 PD(PMD) 접근부터 시작하므로 워크가 2단계로 줄어듭니다.
- PDE 캐시 (PMD Cache): Page Directory 엔트리를 캐시합니다. 히트 시 PTE 테이블 한 번만 접근하면 됩니다.
| 시나리오 | 캐시 히트 위치 | 남은 RAM 접근 횟수 | 대략적 지연 |
|---|---|---|---|
| TLB L1 Hit | L1 ITLB/DTLB | 0회 | ~1 cycle |
| TLB L2 Hit | L2 STLB | 0회 | ~8 cycles |
| PWC PMD Hit | PMD(PDE) 캐시 | 1회 (PTE만) | ~40 cycles |
| PWC PUD Hit | PUD(PDPTE) 캐시 | 2회 (PMD+PTE) | ~80 cycles |
| PWC PGD Hit | PGD(PML4E) 캐시 | 3회 (PUD+PMD+PTE) | ~120 cycles |
| Full Miss | 없음 (CR3부터 워크) | 4회 (전 단계) | 200+ cycles |
PWC는 같은 PGD/PUD/PMD를 공유하는 인접한 가상 주소에 매우 효과적입니다. 예를 들어 같은 2MB 영역 내에서 여러 4KB 페이지에 접근하면 PMD Cache 히트로 PTE 한 단계만 워크하게 됩니다. 반면 munmap() 또는 CR3 재로드(Context Switch) 시 PWC도 무효화됩니다. PCID 기능 사용 시에는 프로세스별로 PWC 엔트리가 태깅되어 Context Switch에서도 일부 PWC 엔트리를 보존할 수 있습니다.
/* 커널이 PWC와 상호작용하는 주요 시점 */
/* 1. Context Switch: CR3 재로드 → TLB + PWC 모두 무효화 (PCID 없는 경우) */
static inline void load_new_mm_cr3(pgd_t *pgdir, u16 new_asid, bool noflush)
{
unsigned long new_cr3 = build_cr3(pgdir, new_asid);
/* noflush=true(PCID 활성): TLB/PWC 엔트리 유지, 새 PCID 태깅 */
/* noflush=false: CR3 로드 시 TLB + PWC 전체 플러시 */
if (noflush)
new_cr3 |= X86_CR3_PCID_NOFLUSH;
write_cr3(new_cr3);
}
/* 2. 페이지 테이블 수정 후 TLB/PWC 무효화 */
/* pmd_clear() 등으로 중간 테이블을 변경하면 PWC도 stale 상태가 됨 */
/* invlpg 명령은 해당 VA의 TLB 엔트리만 제거하지만, */
/* 상위 레벨 PWC 엔트리는 CPU가 자체적으로 무효화하거나 */
/* CR3 재로드로 완전 정리해야 함 (아키텍처 의존적) */
flush_tlb_mm_range(mm, addr, end, PAGE_SHIFT, false);
/* 3. INVPCID로 특정 PCID의 TLB+PWC 선택적 무효화 (x86) */
static inline void invpcid_flush_one(unsigned long pcid, unsigned long addr)
{
struct { u64 pcid, addr; } operand = { pcid, addr };
asm("invpcid %0, %1" : : "m"(operand), "r"((unsigned long)0));
}
주요 MMU 관련 레지스터 (x86-64)
x86-64 MMU 관련 제어 레지스터
CR0: 페이징 활성화
| 비트 | 필드 | 설명 |
|---|---|---|
| 31 | PG | Paging Enable - 이 비트가 1이면 MMU 활성 |
| 16 | WP | Write Protect - 커널도 RO 페이지 보호 (COW 필수) |
CR3: 페이지 테이블 베이스
| 비트 | 필드 | 설명 |
|---|---|---|
| [51:12] | PGD | PGD 물리 주소 (4KB 정렬) |
| [11:0] | PCID | Process-Context Identifier |
| 63 | NOFLUSH | PCID 무효화 방지 (INVPCID_NOFLUSH) |
CR4: 페이징 확장 기능
| 비트 | 필드 | 설명 |
|---|---|---|
| 4 | PSE | Page Size Extension (4MB pages) |
| 5 | PAE | Physical Address Extension |
| 7 | PGE | Page Global Enable |
| 12 | LA57 | 5-Level Paging |
| 17 | PCIDE | PCID Enable |
| 20 | SMEP | Supervisor Mode Execution Prevention |
| 21 | SMAP | Supervisor Mode Access Prevention |
| 22 | PKE | Protection Keys Enable |
| 24 | PKS | Protection Keys for Supervisor |
주소 변환 과정
가상 주소: 0x7ffff7a0e000
1. CPU가 가상 주소로 메모리 접근 요청
2. MMU가 TLB 확인
- TLB Hit → 즉시 물리 주소 반환 (1 cycle)
- TLB Miss → 페이지 테이블 워크 (수십~수백 cycles)
3. 물리 주소로 RAM 접근
물리 주소: 0x12345000
성능 차이: TLB Hit는 1 cycle, TLB Miss는 페이지 테이블 워크로 인해 200+ cycles 소요됩니다. TLB 적중률이 99%에서 99.9%로 향상되면 성능이 10% 이상 개선될 수 있습니다.
CPU 파이프라인과 MMU 상호작용
현대 CPU는 명령어를 단계별로 처리하는 파이프라인(Pipeline) 구조를 사용합니다. MMU는 이 파이프라인의 메모리(Memory) 단계에 위치하여, 실행(Execute) 단계에서 계산된 유효 주소(Effective Address)를 물리 주소로 변환한 뒤 캐시 혹은 메모리에 접근합니다. TLB Hit 여부는 파이프라인 전체 처리량에 직접적인 영향을 줍니다.
비순서 실행(Out-of-Order Execution) 프로세서에서는 투기적 로드(Speculative Load)가 TLB에 접근하기도 합니다. 투기적으로 발행된 로드 명령이 TLB Miss를 유발하면 페이지 워커(Page Walker)가 활성화되고, 결과가 커밋(Commit)되지 않더라도 TLB에 변환 결과가 채워집니다. 이 동작은 Spectre 계열 사이드채널 공격의 원인이 되기도 합니다.
파이프라인 스톨(Pipeline Stall)을 최소화하기 위해 하드웨어는 메모리 접근 순서를 재정렬하거나, TLB Miss 중에도 독립적인 다른 명령어를 계속 실행합니다. 그러나 페이지 폴트(Page Fault)처럼 OS 개입이 필요한 예외는 파이프라인을 완전히 드레인(Drain)한 뒤 처리됩니다.
페이지 테이블 워크 (Page Table Walk)
x86-64 4-Level Paging
가상 주소 (48비트):
[47:39] PGD index (9비트)
[38:30] PUD index (9비트)
[29:21] PMD index (9비트)
[20:12] PTE index (9비트)
[11:0] Offset (12비트 = 4KB)
변환 과정:
1. CR3 레지스터 → PGD 베이스 주소
2. PGD[bits 47:39] → PUD 주소
3. PUD[bits 38:30] → PMD 주소
4. PMD[bits 29:21] → PTE 주소
5. PTE[bits 20:12] → Page Frame 주소
6. Page Frame + Offset → 물리 주소
페이지 테이블 엔트리 (PTE)
| 비트 | 이름 | 설명 |
|---|---|---|
| 0 | Present |
페이지(Page)가 RAM에 있음 |
| 1 | Read/Write |
쓰기 가능 여부 |
| 2 | User/Supervisor |
유저 모드 접근 가능 |
| 5 | Accessed |
읽기/쓰기 발생 |
| 6 | Dirty |
쓰기 발생 |
| 63 | NX (No Execute) |
실행 금지 |
커널 PTE 조작 매크로(Macro)
/* include/linux/pgtable.h - PTE 조작 API */
/* PTE 생성/읽기 */
pte_t mk_pte(struct page *page, pgprot_t pgprot);
pte_t pfn_pte(unsigned long pfn, pgprot_t pgprot);
unsigned long pte_pfn(pte_t pte);
/* PTE 플래그 검사 */
int pte_present(pte_t pte); /* Present 비트 */
int pte_write(pte_t pte); /* Read/Write 비트 */
int pte_dirty(pte_t pte); /* Dirty 비트 */
int pte_young(pte_t pte); /* Accessed 비트 */
int pte_exec(pte_t pte); /* NX 비트 반전 */
int pte_huge(pte_t pte); /* Huge Page 비트 */
/* PTE 플래그 설정 */
pte_t pte_mkwrite(pte_t pte); /* R/W = 1 */
pte_t pte_mkdirty(pte_t pte); /* Dirty = 1 */
pte_t pte_mkyoung(pte_t pte); /* Accessed = 1 */
pte_t pte_wrprotect(pte_t pte); /* R/W = 0 (COW에 사용) */
pte_t pte_mkold(pte_t pte); /* Accessed = 0 (LRU에 사용) */
pte_t pte_mkclean(pte_t pte); /* Dirty = 0 */
pte_t pte_mkexec(pte_t pte); /* NX = 0 (실행 허용) */
/* PTE를 테이블에 기록 (배리어 포함) */
void set_pte_at(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte);
/* Atomic PTE 교체 (COW, 페이지 마이그레이션) */
pte_t ptep_get_and_clear(struct mm_struct *mm,
unsigned long addr, pte_t *ptep);
코드 설명
- 3~5행
mk_pte()는struct page와 보호 플래그(pgprot_t)를 결합하여 PTE 값을 생성합니다.pfn_pte()는 PFN(Page Frame Number)에서 직접 PTE를 만들며,pte_pfn()은 역으로 PTE에서 물리 프레임 번호를 추출합니다. 소스:include/linux/pgtable.h - 8~13행PTE 플래그 검사 함수들입니다.
pte_present()는 페이지가 RAM에 존재하는지,pte_young()은 최근 접근 여부(LRU 페이지 회수에 핵심),pte_dirty()는 수정 여부(writeback 판단)를 확인합니다. 이들은 하드웨어 비트를 직접 검사하는 인라인 함수입니다. - 16~22행PTE 플래그 설정 함수들입니다.
pte_wrprotect()는fork()시 COW를 준비하기 위해 쓰기 권한을 제거하고,pte_mkold()는 LRU 에이징(aging)에서 Accessed 비트를 초기화하여 활성 페이지를 판별합니다. 각 함수는 원본 PTE를 수정하지 않고 새 PTE 값을 반환합니다. - 25~26행
set_pte_at()은 PTE를 페이지 테이블에 기록하며, 아키텍처별 메모리 배리어(barrier)를 포함합니다. ARM64에서는dsb(ishst)배리어가 필요하고, x86에서는 store ordering이 보장되므로 단순 대입입니다. - 29~30행
ptep_get_and_clear()는 PTE를 원자적으로 읽고 0으로 클리어합니다. COW 폴트 처리(do_wp_page())와 페이지 마이그레이션에서 기존 PTE를 안전하게 교체할 때 사용됩니다. x86에서는xchg명령어로 구현됩니다.
페이지 테이블 메모리 관리(Memory Management)
/* mm/memory.c - 페이지 테이블 할당 */
/* 각 테이블 레벨은 4KB 페이지 하나를 차지 (512 x 8바이트 = 4096) */
/* PGD: mm_struct 생성 시 할당 */
pgd_t *pgd_alloc(struct mm_struct *mm)
{
pgd_t *pgd = (pgd_t *)__get_free_page(GFP_PGTABLE_USER);
if (pgd)
pgd_ctor(mm, pgd); /* 커널 매핑 복사 */
return pgd;
}
/* PTE 테이블: page fault 시 lazy 할당 */
pte_t *pte_alloc_one_kernel(struct mm_struct *mm)
{
return (pte_t *)__get_free_page(GFP_PGTABLE_KERNEL | __GFP_ZERO);
}
/* 페이지 테이블 메모리 오버헤드:
* 프로세스당 1GB 매핑 시 (4KB 페이지):
* PGD: 1 페이지 (4KB)
* PUD: 1 페이지 (4KB) — 1GB는 PUD 1개
* PMD: 1 페이지 (4KB) — 512 PMD 엔트리
* PTE: 512 페이지 (2MB) — 256K PTE 엔트리
* 총: ~2MB (매핑 메모리의 0.2%)
*/
/proc/[pid]/status의 VmPTE 필드로 프로세스별 페이지 테이블 크기를 확인하세요. Huge Pages는 PTE 레벨을 제거하여 이 비용도 절감합니다.
TLB (Translation Lookaside Buffer)
TLB 아키텍처
| 레벨 | 크기 (x86) | 지연(Latency)시간 | 커버 범위 (4KB) |
|---|---|---|---|
| L1 DTLB | 64 엔트리 | 1 cycle | 256KB |
| L1 ITLB | 128 엔트리 | 1 cycle | 512KB |
| L2 STLB | 1536 엔트리 | 7 cycles | 6MB |
Huge Pages 효과: 2MB Huge Page 사용 시 하나의 TLB 엔트리가 512배 많은 메모리를 커버합니다. 64 엔트리 TLB로 128MB → 64GB를 커버할 수 있습니다!
TLB 관리 명령 (x86)
/* arch/x86/include/asm/tlbflush.h */
/* 전체 TLB 플러시 */
static inline void __native_flush_tlb(void)
{
unsigned long cr3;
cr3 = __read_cr3();
__write_cr3(cr3); /* CR3 재설정 = 전체 플러시 */
}
/* 단일 페이지 무효화 */
static inline void __flush_tlb_one(unsigned long addr)
{
asm volatile("invlpg (%0)" :: "r" (addr) : "memory");
}
/* PCID 사용 시 선택적 플러시 */
static inline void __flush_tlb_one_user(unsigned long addr)
{
asm volatile("invpcid (%0), %1" :: "r"(&desc), "r"(type));
}
TLB Shootdown
문제: 페이지 테이블 변경 시 모든 CPU의 TLB를 무효화해야 함
/* mm/memory.c - mprotect 등에서 호출 */
void flush_tlb_mm_range(struct mm_struct *mm,
unsigned long start,
unsigned long end,
unsigned int stride_shift)
{
/* 1. 로컬 CPU TLB 플러시 */
__flush_tlb_mm_range(mm, start, end, stride_shift);
/* 2. 다른 CPU들에게 IPI 전송 */
smp_call_function_many(mm_cpumask(mm),
flush_tlb_func_remote, &info, 1);
}
Shootdown 비용
| 시나리오 | 비용 | 영향 | IPI 수 |
|---|---|---|---|
| 단일 CPU | ~100 cycles | 무시 가능 | 0 |
| 4 CPU | ~1 us | 낮음 | 3 |
| 64 CPU | ~10 us | 높음 | 63 |
| 256 CPU | ~100 us | 매우 높음 | 255 |
Shootdown 최적화 기법
/* arch/x86/mm/tlb.c - 범위 기반 최적화 */
static void flush_tlb_func_remote(void *info)
{
struct flush_tlb_info *f = info;
/* 최적화 1: mm_cpumask 확인 - 해당 mm을 사용하는 CPU만 플러시 */
if (f->mm != this_cpu_read(cpu_tlbstate.loaded_mm))
return; /* 이 CPU는 해당 mm을 사용하지 않음 */
/* 최적화 2: 범위가 작으면 INVLPG, 크면 전체 플러시 */
if (f->end - f->start <= TLB_FLUSH_THRESHOLD) {
/* 페이지 단위 무효화 (INVLPG) */
unsigned long addr;
for (addr = f->start; addr < f->end; addr += PAGE_SIZE)
__flush_tlb_one_user(addr);
} else {
/* 전체 TLB 플러시 (CR3 재로드) */
__flush_tlb_global();
}
}
/* TLB_FLUSH_THRESHOLD: INVLPG vs 전체 플러시 전환점 */
/* 일반적으로 33 페이지 (132KB) — 이보다 크면 전체 플러시가 유리 */
코드 설명
- 3행
flush_tlb_func_remote()는 IPI를 수신한 원격 CPU에서 실행되는 콜백 함수입니다.struct flush_tlb_info에는 대상mm, 시작/끝 주소, stride 정보가 담겨 있습니다. 소스:arch/x86/mm/tlb.c - 6~7행첫 번째 최적화로, IPI를 받은 CPU가 실제로 해당
mm을 사용 중인지cpu_tlbstate.loaded_mm과 비교합니다. Lazy TLB 모드의 CPU(커널 스레드 실행 중)는 이 검사에서 조기 반환하여 불필요한 플러시를 피합니다. - 10~14행두 번째 최적화로, 무효화 범위가
TLB_FLUSH_THRESHOLD(약 33페이지, 132KB)보다 작으면INVLPG를 페이지 단위로 반복 실행합니다.INVLPG한 번의 비용은 약 100ns이므로, 33번 이상이면 CR3 재로드(전체 플러시)가 더 효율적입니다. - 15~17행범위가 임계치를 초과하면
__flush_tlb_global()로 전체 TLB를 플러시합니다. PCID가 활성화된 경우 현재 PCID의 엔트리만 무효화하므로, 다른 프로세스의 TLB 엔트리는 보존됩니다.
mm_cpumask(mm)는 해당 mm_struct를 현재 사용 중인 CPU 집합을 추적합니다. Context Switch 시 업데이트되며, TLB Shootdown 시 이 마스크에 포함된 CPU에만 IPI를 보냅니다. 256 CPU 시스템에서 프로세스가 4개 CPU에서만 실행 중이라면 IPI는 3개만 전송됩니다.
PCID / ASID
PCID (Process-Context Identifier) - x86
Context Switch 시 TLB 플러시를 피하는 기법:
/* arch/x86/mm/tlb.c */
void switch_mm_irqs_off(struct mm_struct *prev, struct mm_struct *next)
{
if (cpu_feature_enabled(X86_FEATURE_PCID)) {
/* PCID 사용: TLB 유지 */
u16 new_asid = build_cr3(next->pgd, next->context.asid);
__write_cr3(new_asid);
} else {
/* PCID 미지원: 전체 TLB 플러시 */
__write_cr3(__pa(next->pgd));
}
}
ASID (Address Space Identifier) - ARM
ARM의 PCID equivalent:
TTBR0 (User page table) + ASID[0:7]
- 최대 256개 프로세스의 TLB 공존 가능
- Context Switch 시 ASID만 변경
모니터링 (Monitoring)
perf TLB 분석
# TLB Miss 측정 (기본)
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses ./myapp
# 결과 예:
# 1,234,567,890 dTLB-loads
# 12,345,678 dTLB-load-misses # 1% Miss Rate
# 상세 TLB/Page Walk 이벤트 (Intel)
perf stat -e dTLB-loads,dTLB-load-misses,\
dTLB-stores,dTLB-store-misses,\
dtlb_load_misses.walk_completed,\
dtlb_load_misses.walk_active,\
dtlb_load_misses.stlb_hit ./myapp
# TLB miss 핫스팟 프로파일링
perf record -e dTLB-load-misses -ag ./myapp
perf report --sort=dso,symbol
# TLB flush 트레이스포인트 (커널 이벤트)
perf stat -e tlb:tlb_flush -a -- sleep 10
perf record -g -e tlb:tlb_flush -- sleep 5
TLB 정보 확인
# x86 CPUID로 TLB 스펙 확인
cpuid | grep -i TLB
# /proc/cpuinfo에서 TLB 관련 기능 확인
grep -i "tlb\|pcid\|pse\|pge\|la57\|invpcid" /proc/cpuinfo | head -5
# 프로세스별 페이지 테이블 크기
grep VmPTE /proc/$(pgrep -x postgres | head -1)/status
# THP (Transparent Huge Pages) 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /proc/meminfo | grep -i huge
# KPTI 상태
dmesg | grep -i "page table isolation"
# 전체 시스템 TLB 관련 통계
cat /proc/vmstat | grep -E "^(pgfault|pgmajfault|thp_|nr_tlb)"
TLB 관련 /proc/vmstat 지표
| 지표 | 설명 | 주의 기준 |
|---|---|---|
pgfault | 전체 페이지 폴트 횟수 (마이너 + 메이저) | 절대값보다 변화율 확인 |
pgmajfault | 메이저 폴트 (디스크 I/O 발생) | 0이 아니면 I/O 병목 의심 |
thp_fault_alloc | THP 폴트로 할당된 huge page 수 | - |
thp_collapse_alloc | khugepaged가 합체한 횟수 | - |
thp_split_page | huge page 분할 횟수 | 많으면 THP 비효율 |
nr_tlb_remote_flush | 원격 TLB 플러시 횟수 | 높으면 shootdown 병목 |
nr_tlb_remote_flush_received | 원격 플러시 수신 횟수 | CPU 수에 비례 |
nr_tlb_local_flush_all | 로컬 전체 TLB 플러시 | 높으면 Context Switch 과다 |
nr_tlb_local_flush_one | 로컬 단일 페이지 플러시 (INVLPG) | - |
성능 최적화 (Optimization)
모범 사례
- Huge Pages 사용 — TLB 커버리지 512배 증가, 워크 비용 25% 감소
- 메모리 국소성 (Locality) — working set을 TLB 커버리지 이내로 유지
- mprotect/munmap 최소화 — 시스템 콜(System Call) 비용 + TLB Shootdown 비용 감소
- PCID 활성화 — Context Switch 시 TLB 플러시 방지
- 메모리 할당자(Memory Allocator) 튜닝 — jemalloc/tcmalloc의 retain 옵션으로 mmap/munmap 빈도 감소
- MADV_HUGEPAGE — 선택적 THP 활성화로 핵심 데이터 구조에 Huge Pages 적용
- NUMA-aware 할당 — 로컬 노드 메모리 사용으로 TLB miss 후 워크 지연 감소
- 페이지 테이블 크기 모니터링 —
/proc/[pid]/status의 VmPTE 필드 주시
최적화 코드 패턴
/* 사용자 공간: madvise로 Huge Pages 요청 */
#include <sys/mman.h>
void *buf = mmap(NULL, 256 * 1024 * 1024, /* 256MB */
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
/* 또는 THP를 madvise 모드에서 요청 */
void *buf2 = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(buf2, size, MADV_HUGEPAGE);
/* 데이터 구조를 2MB 정렬하여 THP 효율 극대화 */
void *aligned = aligned_alloc(2 * 1024 * 1024, size);
madvise(aligned, size, MADV_HUGEPAGE);
Huge Pages + TLB
| 페이지 크기 | TLB 엔트리 | 커버 범위 | 적중률 |
|---|---|---|---|
| 4KB | 64 | 256KB | 85% |
| 2MB | 64 | 128MB | 99% |
| 1GB | 64 | 64GB | 99.9% |
커널 API
TLB 플러시 API
/* include/asm-generic/tlbflush.h - 아키텍처 독립 TLB 플러시 API */
/* 전체 TLB 플러시 (모든 CPU, 모든 엔트리) */
void flush_tlb_all(void);
/* 용도: 커널 페이지 테이블 변경 (vmalloc 등) */
/* 비용: 매우 높음 - 가능한 피해야 함 */
/* 단일 mm_struct의 TLB 플러시 (해당 mm을 사용하는 CPU만) */
void flush_tlb_mm(struct mm_struct *mm);
/* 용도: exec(), 대규모 매핑 변경 */
/* 범위 플러시 (해당 mm, 특정 주소 범위) */
void flush_tlb_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end);
/* 용도: mprotect(), mremap() */
/* 단일 페이지 플러시 (해당 mm, 단일 주소) */
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr);
/* 용도: COW, 개별 PTE 변경 */
/* 비용: 가장 낮음 - 가능하면 이것을 사용 */
/* 커널 가상 주소 범위 플러시 */
void flush_tlb_kernel_range(unsigned long start, unsigned long end);
/* 용도: vunmap(), ioremap() 해제 */
코드 설명
- flush_tlb_all()모든 CPU의 모든 TLB 엔트리를 무효화합니다.
vmalloc()/vfree()등 커널 전역 매핑 변경 시 사용되며, 비용이 가장 높으므로 최후의 수단입니다. x86에서는 모든 CPU에 IPI를 전송하고, ARM64에서는TLBI VMALLE1IS하나로 브로드캐스트합니다. 소스:include/asm-generic/tlbflush.h - flush_tlb_mm()특정
mm_struct를 사용하는 CPU에만 TLB 플러시를 수행합니다.exec()에서 프로세스의 전체 주소 공간을 교체할 때 적합합니다.mm_cpumask(mm)로 IPI 대상 CPU를 제한하므로flush_tlb_all()보다 효율적입니다. - flush_tlb_range()특정 VMA의 주소 범위만 무효화합니다.
mprotect()나mremap()에서 권한/위치가 변경된 범위에 사용됩니다. 내부적으로 범위 크기에 따라INVLPG반복 또는 전체 플러시를 선택합니다. - flush_tlb_page()단일 페이지의 TLB 엔트리만 무효화하며, 비용이 가장 낮습니다. COW 폴트(
do_wp_page())나 개별 PTE 변경 시 사용됩니다. x86에서는INVLPG1회로 구현됩니다. - flush_tlb_kernel_range()커널 가상 주소(vmalloc 영역 등) 범위를 플러시합니다. 커널 매핑은 모든 프로세스가 공유하므로, 모든 CPU 대상으로 실행됩니다.
vunmap(),ioremap()해제 시 호출됩니다.
플러시 API 선택 가이드
| 시나리오 | 적합한 API | 비고 |
|---|---|---|
| 단일 PTE 변경 (COW, page fault) | flush_tlb_page() | INVLPG 1회 |
| VMA 권한 변경 (mprotect) | flush_tlb_range() | 범위 크기에 따라 INVLPG 또는 전체 플러시 |
| 프로세스 전체 매핑 교체 (exec) | flush_tlb_mm() | CR3 재로드 |
| 커널 매핑 변경 (vmalloc/vfree) | flush_tlb_kernel_range() | 모든 CPU 대상 |
| 대규모 해제 (munmap + free_pgtables) | mmu_gather 배칭 | 자동 최적화 |
| 커널 초기화, 드라이버 매핑 | flush_tlb_all() | 최후의 수단 |
일반적인 문제
높은 TLB Miss Rate
증상: perf에서 dTLB-load-misses > 5%
진단:
# TLB miss rate 측정
perf stat -e dTLB-loads,dTLB-load-misses -p $(pgrep myapp) -- sleep 10
# miss가 발생하는 함수 확인
perf record -e dTLB-load-misses -g -p $(pgrep myapp) -- sleep 10
perf report --sort=symbol
# working set 크기 추정
perf stat -e dTLB-load-misses,dtlb_load_misses.stlb_hit \
-p $(pgrep myapp) -- sleep 10
# STLB hit가 높으면 L1 TLB만 부족 → 적당한 Huge Pages로 해결
# STLB miss도 높으면 working set이 매우 큼 → HugeTLB 필수
해결:
- Huge Pages 활성화 (가장 효과적)
- 메모리 접근 패턴 개선 (배열 순차 접근, 구조체(Struct) 패딩(Padding))
- Working set 크기 감소 (데이터 구조 최적화)
- 핫 데이터를 연속 메모리에 배치
TLB Shootdown Storm
증상: 많은 CPU에서 동시에 mprotect/munmap, nr_tlb_remote_flush 급증
진단:
# Shootdown 빈도 모니터링
watch -n 1 'grep tlb_remote /proc/vmstat'
# mmap/munmap 빈도 추적
bpftrace -e 'tracepoint:syscalls:sys_enter_munmap { @[comm] = count(); } interval:s:5 { print(@); clear(@); }'
해결:
- Batch TLB 무효화 (mmu_gather 자동 적용)
- 페이지 테이블 변경 최소화
madvise(MADV_FREE)사용 (munmap 대신 — 매핑 유지, 실제 해제 지연)- 메모리 할당자 retain 설정 (jemalloc:
retain:true) - 큰 매핑 해제 시 한 번에 처리 (소량 반복 해제 방지)
KPTI 성능 저하
증상: syscall 빈번한 워크로드에서 KPTI 활성화 시 5~30% 성능 저하
확인:
# KPTI 상태 확인
dmesg | grep "page table isolation"
# PCID 지원 확인 (KPTI 비용 절감)
grep -o pcid /proc/cpuinfo | head -1
# syscall 빈도 확인
perf stat -e raw_syscalls:sys_enter -a -- sleep 5
해결:
- PCID 지원 CPU 확인 (Haswell 이후) — PCID가 있으면 성능 저하 1~5%로 감소
- syscall 빈도 감소:
vDSO활용,io_uring사용, 배칭 처리 - Meltdown 영향 없는 CPU에서는
nopti커널 파라미터 (보안 위험 감수 시)
페이지 테이블 비대화
증상: VmPTE가 수백 MB 이상, 물리 메모리(Physical Memory) 부족
# 프로세스별 페이지 테이블 크기 확인
for p in /proc/[0-9]*/status; do
name=$(grep Name "$p" 2>/dev/null | awk '{print $2}')
pte=$(grep VmPTE "$p" 2>/dev/null | awk '{print $2}')
[ -n "$pte" ] && [ "$pte" -gt 10000 ] && echo "$pte kB - $name"
done | sort -n -r | head -10
해결:
- Huge Pages 적용 (PTE 레벨 제거)
- 분산된 mmap 매핑을 연속적으로 통합
- 불필요한 매핑 해제
MMU/TLB 튜닝 체크리스트
MMU/TLB 성능은 접근 패턴, 페이지 크기, 매핑 변경 빈도에 크게 좌우됩니다. 튜닝은 "TLB miss 감소"와 "shootdown 비용 감소"를 분리해서 접근해야 효과적입니다.
| 점검 항목 | 확인 방법 | 개선 방향 | 우선순위(Priority) |
|---|---|---|---|
| TLB miss 비율 | perf stat dTLB-load-misses | HugePage/THP 적용, locality 개선 | 최고 |
| shootdown 빈도 | perf + mprotect/munmap 패턴 | 매핑 변경 배치 처리, 빈도 축소 | 높음 |
| context switch 영향 | PCID/ASID 활성 여부 | 아키텍처 최적화 옵션 확인 | 중간 |
| 페이지 테이블 크기 | grep VmPTE /proc/[pid]/status | Huge Pages로 PTE 제거 | 중간 |
| KPTI 오버헤드(Overhead) | PCID 지원 여부, 워크로드 유형 | PCID 활성화 확인 | 낮음 |
| NUMA 지역성 | numastat, remote 접근 비율 | 로컬 노드 할당 정책 | 중간 |
| 메이저 폴트 | sar -B, pgmajfault | 메모리 추가 또는 스왑(Swap) 튜닝 | 최고 |
# TLB 관측 기본 명령
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-load-misses ./workload
perf record -g -e tlb:tlb_flush -- sleep 5 2>/dev/null || true
# 종합 TLB/메모리 성능 분석 스크립트
echo "=== TLB Miss Rate ==="
perf stat -e dTLB-load-misses,dTLB-loads -a -- sleep 5 2>&1 | tail -3
echo "=== TLB Flush Stats ==="
grep tlb /proc/vmstat
echo "=== Huge Pages ==="
grep -i huge /proc/meminfo
echo "=== THP Status ==="
cat /sys/kernel/mm/transparent_hugepage/enabled
echo "=== Top VmPTE Processes ==="
for p in /proc/[0-9]*/status; do
grep -H VmPTE "$p" 2>/dev/null
done | sort -t: -k3 -n -r | head -10
TLB 튜닝 결정 트리
dTLB-load-misses > 5%?
├── Yes → working set 크기 확인
│ ├── > TLB 커버리지 → Huge Pages 적용
│ │ ├── 예측 가능한 패턴 → HugeTLB (예약)
│ │ └── 범용 워크로드 → THP (madvise 모드)
│ └── < TLB 커버리지 → 접근 패턴 분석 (locality 개선)
│
└── No → shootdown 빈도 확인
├── nr_tlb_remote_flush 높음 → mmap/munmap 빈도 감소
│ ├── 할당자 튜닝 (retain, decay 설정)
│ └── madvise(MADV_FREE) vs munmap
└── context switch 빈도 높음 → PCID 활성화 확인
5-Level Paging (LA57)
x86-64의 기존 4단계 페이징은 48비트 가상 주소 공간(256TB)을 지원합니다. 그러나 대규모 메모리를 사용하는 서버와 클라우드 환경에서는 더 넓은 가상 주소 공간이 필요합니다. Intel은 LA57 (Linear Address 57-bit) 확장을 통해 5단계 페이징을 도입했습니다.
LA57 개요
| 항목 | 4-Level Paging | 5-Level Paging (LA57) |
|---|---|---|
| 가상 주소 비트 | 48비트 | 57비트 |
| 가상 주소 공간 | 256 TB | 128 PB (페타바이트) |
| 물리 주소 비트 | 최대 52비트 | 최대 52비트 |
| 페이지 테이블 단계 | PGD → PUD → PMD → PTE | PGD → P4D → PUD → PMD → PTE |
| 워크 RAM 접근 | 4회 | 5회 |
| 커널 설정 | 기본 | CONFIG_X86_5LEVEL |
| 최초 지원 | AMD64 (2003) | Linux 4.14 (2017), Ice Lake (2019) |
| Canonical 주소 | 비트 [63:48] = 비트 47 | 비트 [63:57] = 비트 56 |
호환성: 5단계 페이징이 활성화된 시스템에서도 사용자 프로세스는 mmap() 플래그로 48비트 또는 57비트 주소 공간을 선택할 수 있습니다. 기존 프로그램은 48비트 범위 내에서 동작합니다.
57비트 가상 주소 구조
57비트 가상 주소 분할:
[56:48] PGD index (9비트) → 512 엔트리
[47:39] P4D index (9비트) → 512 엔트리
[38:30] PUD index (9비트) → 512 엔트리
[29:21] PMD index (9비트) → 512 엔트리
[20:12] PTE index (9비트) → 512 엔트리
[11:0] Offset (12비트) → 4KB 페이지
총: 9 + 9 + 9 + 9 + 9 + 12 = 57비트
커널 설정 및 감지
/* arch/x86/include/asm/pgtable_64_types.h */
#ifdef CONFIG_X86_5LEVEL
#define __ARCH_HAS_5LEVEL_HACK
#define PGDIR_SHIFT 48
#define P4D_SHIFT 39
#define MAX_PHYSMEM_BITS 52
#define PTRS_PER_P4D 512
#else
#define PGDIR_SHIFT 39
#define PTRS_PER_P4D 1 /* P4D가 폴딩됨 */
#endif
/* arch/x86/kernel/cpu/common.c - 부트 시 LA57 감지 */
static void detect_5level_paging(void)
{
if (cpuid_ecx(7) & (1 << 16)) { /* CPUID.7:ECX.LA57 bit */
setup_force_cpu_cap(X86_FEATURE_LA57);
/* CR4.LA57 비트를 설정하여 5단계 페이징 활성화 */
cr4_set_bits(X86_CR4_LA57);
}
}
사용자 공간(User Space) 주소 범위
/* arch/x86/include/asm/processor.h */
/* 4-Level: 사용자 공간 0x0000_0000_0000 ~ 0x7FFF_FFFF_FFFF (128TB) */
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
/* 5-Level: 사용자 공간 0x0000_0000_0000 ~ 0xFF_FFFF_FFFF_FFFF (64PB) */
#ifdef CONFIG_X86_5LEVEL
#define TASK_SIZE_MAX ((1UL << 56) - PAGE_SIZE)
#endif
/* 사용자 프로세스가 mmap() 시 MAP_ABOVE4G/위치 지정 가능 */
/* ADDR_LIMIT_47BIT으로 48비트 호환 모드 선택 가능 */
ARM64 페이지 테이블
ARM64(AArch64)는 x86-64와 다른 페이지 테이블 체계를 사용합니다. 가장 큰 차이점은 TTBR0/TTBR1 이중 베이스 레지스터로 사용자/커널 공간(Kernel Space)을 하드웨어 수준에서 분리하는 점입니다.
TTBR0 / TTBR1 분리
ARM64 가상 주소 공간 분할:
TTBR0 (사용자 공간):
0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF (48비트, 4KB granule)
0x0000_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF (52비트 확장 시)
TTBR1 (커널 공간):
0xFFFF_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF (48비트, 4KB granule)
주소의 최상위 비트로 자동 선택:
bit[63] = 0 → TTBR0 (사용자)
bit[63] = 1 → TTBR1 (커널)
Granule 크기
ARM64는 세 가지 granule(페이지) 크기를 지원합니다. x86이 4KB 고정인 것과 대조적입니다.
| Granule 크기 | 페이지 테이블 단계 | 가상 주소 비트 | 엔트리 수 | Block (Huge) 크기 | 사용 사례 |
|---|---|---|---|---|---|
| 4KB | 4단계 (L0~L3) | 48비트 (256TB) | 512 (9비트) | 2MB (L2), 1GB (L1) | 범용 서버, 데스크톱 |
| 16KB | 4단계 (L0~L3) | 47비트 (128TB) | 2048 (11비트) | 32MB (L2) | Apple Silicon |
| 64KB | 3단계 (L1~L3) | 48비트 (256TB) | 8192 (13비트) | 512MB (L2) | HPC, 대규모 메모리 |
ARM64 PTE 비트 구조
/* arch/arm64/include/asm/pgtable-hwdef.h */
/* Stage-1 (Normal) 디스크립터 비트 */
#define PTE_VALID (1UL << 0) /* 유효 비트 */
#define PTE_TABLE_BIT (1UL << 1) /* 테이블/블록 구분 */
#define PTE_USER (1UL << 6) /* AP[1]: EL0 접근 허용 */
#define PTE_RDONLY (1UL << 7) /* AP[2]: 읽기 전용 */
#define PTE_SHARED (3UL << 8) /* SH[1:0]: Inner Shareable */
#define PTE_AF (1UL << 10) /* Access Flag */
#define PTE_NG (1UL << 11) /* non-Global */
#define PTE_DBM (1UL << 51) /* Dirty Bit Modifier (ARMv8.1) */
#define PTE_CONT (1UL << 52) /* Contiguous hint */
#define PTE_PXN (1UL << 53) /* Privileged XN */
#define PTE_UXN (1UL << 54) /* User XN */
코드 설명
- PTE_VALID / PTE_TABLE_BITARM64 PTE의 하위 2비트입니다. bit 0(
PTE_VALID)은 엔트리 유효성, bit 1(PTE_TABLE_BIT)은 다음 레벨 테이블을 가리키는지(1) 아니면 블록 매핑인지(0)를 구분합니다. PMD 레벨에서TABLE_BIT=0이면 2MB 블록(Huge Page)으로 직접 매핑됩니다. 소스:arch/arm64/include/asm/pgtable-hwdef.h - PTE_USER / PTE_RDONLYAccess Permission 비트(AP[1:0])입니다.
PTE_USER(AP[1])이 설정되면 EL0(사용자 모드)에서 접근 가능하고,PTE_RDONLY(AP[2])가 설정되면 읽기 전용입니다. x86의 U/S, R/W 비트와 유사하지만, ARM64는 특권/사용자 접근 권한을 더 세밀하게 제어합니다. - PTE_AF / PTE_NG
PTE_AF(Access Flag)는 x86의 Accessed 비트에 해당하며, 최초 접근 시 하드웨어가 자동 설정합니다(ARMv8.1 FEAT_HAFDBS).PTE_NG(non-Global)는 ASID 태깅 대상임을 표시하며, 사용자 페이지는 항상nG=1로 설정됩니다. - PTE_DBM / PTE_CONT
PTE_DBM(Dirty Bit Modifier, ARMv8.1)은 하드웨어 Dirty 비트 자동 관리를 활성화합니다.PTE_CONT(Contiguous hint, bit 52)는 연속 16개 PTE가 물리적으로 연속임을 TLB에 알려, 하나의 TLB 엔트리로 64KB(4KB granule x 16)를 커버하게 합니다. - PTE_PXN / PTE_UXN실행 금지 비트입니다. x86이 단일
NX비트인 반면, ARM64는PXN(Privileged Execute-Never)과UXN(User Execute-Never)으로 특권/사용자 실행 권한을 독립 제어합니다. 커널 데이터 영역은PXN=1, 사용자 코드 영역은UXN=0, PXN=1로 설정합니다.
Contiguous Hint (PTE_CONT): 연속 16개(4KB granule) 또는 128개(64KB granule) PTE가 물리적으로 연속임을 TLB에 알려, 하나의 TLB 엔트리로 64KB~2MB를 커버합니다. CONFIG_ARM64_CONTPTE로 제어됩니다.
ARM64 TLB 관리 명령
/* arch/arm64/include/asm/tlbflush.h */
/* ARM64 TLB 무효화 명령 체계 (TLBI) */
/* 전체 TLB 무효화 (Inner Shareable = 모든 코어) */
static inline void flush_tlb_all(void)
{
dsb(ishst); /* 이전 스토어 완료 대기 */
__tlbi(vmalle1is); /* TLBI VMALLE1IS: VM All E1, Inner Shareable */
dsb(ish); /* TLB 무효화 완료 대기 */
isb(); /* 명령어 파이프라인 동기화 */
}
/* 단일 페이지 무효화 (특정 VA + ASID) */
static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,
unsigned long uaddr)
{
unsigned long addr = __TLBI_VADDR(uaddr, ASID(vma->vm_mm));
dsb(ishst);
__tlbi(vale1is, addr); /* TLBI VAE1IS: VA, EL1, Inner Shareable */
}
/* ASID 기반 무효화 (프로세스 전체) */
static inline void flush_tlb_mm(struct mm_struct *mm)
{
unsigned long asid = ASID(mm);
dsb(ishst);
__tlbi(aside1is, asid); /* TLBI ASIDE1IS: ASID, EL1, IS */
dsb(ish);
}
/* ARM64 TLBI vs x86 IPI 비교:
* - x86: CPU-0이 IPI를 N-1개 CPU에 전송 → 각 CPU가 INVLPG 실행
* (소프트웨어 기반, O(N) 지연)
* - ARM64: TLBI IS 명령 하나로 모든 코어에 하드웨어 브로드캐스트
* (하드웨어 기반, O(1) 명령 + 완료 대기)
*/
ARM64 Context Switch와 ASID
/* arch/arm64/mm/context.c */
/* ARM64 ASID 관리: 8비트(256) 또는 16비트(65536) */
static atomic64_t asid_generation;
static unsigned long *asid_map; /* 비트맵: 사용 중 ASID 추적 */
void check_and_switch_context(struct mm_struct *mm)
{
unsigned long flags;
u64 asid, generation;
asid = atomic64_read(&mm->context.id);
generation = atomic64_read(&asid_generation);
if ((asid ^ generation) >> asid_bits) {
/* ASID가 현재 세대와 다름 → 새 ASID 할당 필요 */
raw_spin_lock_irqsave(&cpu_asid_lock, flags);
asid = new_context(mm);
atomic64_set(&mm->context.id, asid);
raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
}
/* TTBR0 업데이트: 새 페이지 테이블 + ASID */
cpu_switch_mm(mm->pgd, mm);
}
/* ASID 롤오버: 모든 ASID가 소진되면 세대 번호 증가 + 전체 TLB 플러시 */
static u64 new_context(struct mm_struct *mm)
{
u64 asid = find_next_zero_bit(asid_map, num_asids, cur_idx);
if (asid >= num_asids) {
/* 롤오버! 새 세대 시작 */
generation = atomic64_add_return(asid_first, &asid_generation);
flush_context(); /* 전체 TLB 플러시 */
bitmap_zero(asid_map, num_asids);
asid = find_next_zero_bit(asid_map, num_asids, 1);
}
__set_bit(asid, asid_map);
return asid | generation;
}
코드 설명
- 3~4행
asid_generation은 현재 ASID 세대(generation) 번호를 전역으로 추적합니다.asid_map비트맵은 현재 세대에서 할당된 ASID를 추적하여 중복 할당을 방지합니다. 소스:arch/arm64/mm/context.c - 6~15행
check_and_switch_context()는 Context Switch 시 호출됩니다.mm->context.id에 저장된 ASID의 상위 비트(세대 번호)가 현재asid_generation과 일치하면 ASID가 유효하므로 재할당 없이 바로cpu_switch_mm()으로 TTBR0를 업데이트합니다. 세대가 다르면new_context()로 새 ASID를 할당합니다. - 18행
cpu_switch_mm()은 TTBR0_EL1 레지스터에 새 PGD 물리 주소와 ASID를 기록합니다. ASID가 TTBR에 태깅되므로, TLB에 캐시된 이전 프로세스의 엔트리를 플러시하지 않아도 ASID로 구분됩니다. - 21~29행
new_context()는asid_map에서 비어있는 ASID를find_next_zero_bit()로 탐색합니다. 8비트 ASID(256개)가 모두 소진되면 롤오버(rollover)가 발생하여 세대 번호를 증가시키고flush_context()로 전체 TLB를 플러시한 뒤 비트맵을 초기화합니다. 반환값은 ASID와 세대 번호를 OR 결합한 값입니다.
16비트 ASID: ARMv8.2의 16비트 ASID(65536개)를 사용하면 롤오버가 거의 발생하지 않아 Context Switch 시 TLB 플러시가 크게 줄어듭니다. CONFIG_ARM64_16BIT_ASID로 활성화합니다. Apple Silicon M1/M2는 16비트 ASID를 지원합니다.
PTE 비트필드 상세
PTE(Page Table Entry)의 64비트 각 비트는 권한, 캐시 정책, 보안, 상태 추적 등의 정보를 인코딩합니다. x86-64와 ARM64의 비트 배치는 크게 다릅니다.
x86-64 PTE 64비트 레이아웃
x86 vs ARM64 PTE 비트 비교
| 기능 | x86-64 | ARM64 | 비고 |
|---|---|---|---|
| 유효 여부 | bit 0 (Present) | bit 0 (Valid) | 동일 위치 |
| 읽기/쓰기 | bit 1 (R/W) | bit 7 (AP[2]) | ARM은 반전 (0=RW, 1=RO) |
| 사용자 접근 | bit 2 (U/S) | bit 6 (AP[1]) | - |
| 캐시 제어 | bit 3,4,7 (PWT/PCD/PAT) | bit 2~4 (AttrIndx) | ARM은 MAIR 레지스터 참조 |
| Accessed | bit 5 (A) | bit 10 (AF) | ARM은 하드웨어 자동 설정 옵션 |
| Dirty | bit 6 (D) | bit 51 (DBM) + AP[2] | ARMv8.1-TTHM 필요 |
| Global | bit 8 (G) | bit 11 (nG, 반전) | ARM은 0=Global, 1=non-Global |
| 실행 금지 | bit 63 (NX) | bit 53/54 (PXN/UXN) | ARM은 특권/사용자 분리 |
| Protection Keys | bit 62:59 (PKU) | 없음 (POIndex 예정) | ARMv9에서 POE 추가 |
| 물리 주소 | bit 51:12 (40비트) | bit 47:12 (36비트) | ARM은 52비트 PA 확장 가능 |
/* x86: PTE에서 Present가 0일 때 = 스왑 엔트리로 사용 */
/*
* 스왑 PTE 레이아웃 (Present=0):
* bit 0 : Present (0)
* bit 1 : 예약 (0)
* bit 2..7 : 스왑 타입 (64가지)
* bit 8..57: 스왑 오프셋 (50비트)
* bit 58..63: 소프트웨어 플래그
*/
static inline swp_entry_t pte_to_swp_entry(pte_t pte)
{
unsigned long val = pte_val(pte);
return (swp_entry_t) { .val = val >> 2 };
}
PAT/MAIR 메모리 타입과 TLB
메모리 타입(Memory Type)은 CPU가 특정 물리 주소 범위에 접근할 때 사용하는 캐싱 및 쓰기 정책을 정의합니다. x86에서는 PAT(Page Attribute Table), ARM64에서는 MAIR(Memory Attribute Indirection Register)가 이 역할을 담당합니다. 메모리 타입 정보는 TLB 엔트리에 함께 저장되어, 물리 주소 변환 시 캐시 정책도 동시에 결정됩니다.
x86 PAT (Page Attribute Table)
x86에서 각 PTE는 3개의 비트(PWT, PCD, PAT)를 통해 8개의 PAT 엔트리 중 하나를 선택합니다. PAT는 MSR IA32_PAT(주소 0x277)에 저장된 8개 슬롯으로 구성되며, 각 슬롯은 메모리 타입을 정의합니다. 커널은 부팅 시 PAT를 초기화하여 슬롯별 정책을 설정합니다.
/* arch/x86/mm/pat/memtype.c: 부팅 시 PAT 초기화 */
/* PTE 비트 조합 -> PAT 인덱스: PAT[2] PCD[1] PWT[0] */
/* 인덱스 0 (PAT=0,PCD=0,PWT=0): WB (Write-Back) -- 기본값 */
/* 인덱스 1 (PAT=0,PCD=0,PWT=1): WT (Write-Through) */
/* 인덱스 2 (PAT=0,PCD=1,PWT=0): UC- (Uncacheable Minus) */
/* 인덱스 3 (PAT=0,PCD=1,PWT=1): UC (Uncacheable) -- MMIO */
/* 인덱스 4 (PAT=1,PCD=0,PWT=0): WB (Write-Back, 재지정 가능) */
/* 인덱스 5 (PAT=1,PCD=0,PWT=1): WP (Write-Protected) */
/* 인덱스 6 (PAT=1,PCD=1,PWT=0): UC- (Uncacheable Minus) */
/* 인덱스 7 (PAT=1,PCD=1,PWT=1): WC (Write-Combining) -- 프레임버퍼 */
#define PAT_VALUE ((u64)PAT_WRITE_BACK << (0 * 8) | \
(u64)PAT_WRITE_THROUGH << (1 * 8) | \
(u64)PAT_UNCACHED_MINUS << (2 * 8) | \
(u64)PAT_UNCACHED << (3 * 8) | \
(u64)PAT_WRITE_BACK << (4 * 8) | \
(u64)PAT_WRITE_PROTECTED << (5 * 8) | \
(u64)PAT_UNCACHED_MINUS << (6 * 8) | \
(u64)PAT_WRITE_COMBINING << (7 * 8))
static void pat_init(void)
{
wrmsrl(MSR_IA32_PAT, PAT_VALUE);
}
/* PTE 보호 플래그 -- 메모리 타입 선택 비트 */
#define _PAGE_PWT ((pteval_t)1 << 3) /* PAT 인덱스 bit0 */
#define _PAGE_PCD ((pteval_t)1 << 4) /* PAT 인덱스 bit1 */
#define _PAGE_PAT ((pteval_t)1 << 7) /* PAT 인덱스 bit2 (4KB PTE) */
#define _PAGE_PAT_LARGE ((pteval_t)1 << 12) /* PAT 인덱스 bit2 (2MB PMD) */
/* 메모리 타입 선택 헬퍼 매크로 */
#define pgprot_noncached(prot) __pgprot(pgprot_val(prot) | _PAGE_PCD | _PAGE_PWT)
#define pgprot_writecombine(prot) __pgprot((pgprot_val(prot) & ~(_PAGE_PCD|_PAGE_PWT)) | _PAGE_PAT)
#define pgprot_writethrough(prot) __pgprot((pgprot_val(prot) & ~(_PAGE_PCD|_PAGE_PAT)) | _PAGE_PWT)
ARM64 MAIR (Memory Attribute Indirection Register)
ARM64에서는 PTE의 AttrIndx[2:0] 필드(3비트)가 MAIR_EL1 레지스터의 8개 슬롯 중 하나를 가리킵니다. 각 슬롯은 8비트로 내부 및 외부 캐시 정책을 독립적으로 지정합니다. x86의 PAT와 달리 MAIR는 내부 캐시(Inner Shareable, CPU 캐시)와 외부 캐시(Outer Shareable, 시스템 캐시)를 구분하여 설정합니다.
/* arch/arm64/include/asm/memory.h: MAIR 인덱스 정의 */
#define MT_DEVICE_nGnRnE 0 /* Device, non-Gathering/Reordering/Early Write */
#define MT_DEVICE_nGnRE 1 /* Device, nGnRE (일반 MMIO) */
#define MT_DEVICE_GRE 2 /* Device, Gathering+Reordering+Early Write */
#define MT_NORMAL_NC 3 /* Normal, Non-Cacheable */
#define MT_NORMAL 4 /* Normal, WB+WA (기본 메모리) */
#define MT_NORMAL_TAGGED 5 /* Normal, Tagged (MTE 메모리 태깅 확장) */
#define MT_NORMAL_WT 6 /* Normal, Write-Through (프레임버퍼 등) */
/* MAIR_EL1 인코딩: 각 슬롯 8비트 -- [7:4]=외부캐시 [3:0]=내부캐시 */
#define MAIR_ATTRIDX(attr, idx) ((attr) << ((idx) * 8))
#define MAIR_EL1_SET \
(MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \
MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) | \
MAIR_ATTRIDX(MAIR_ATTR_DEVICE_GRE, MT_DEVICE_GRE) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL_TAGGED,MT_NORMAL_TAGGED) | \
MAIR_ATTRIDX(MAIR_ATTR_NORMAL_WT, MT_NORMAL_WT))
/* 부팅 시 MAIR_EL1 설정 (arch/arm64/mm/mmu.c) */
write_sysreg(MAIR_EL1_SET, mair_el1);
/* ARM64 pgprot 헬퍼: AttrIndx 필드를 교체하여 메모리 타입 지정 */
#define pgprot_noncached(prot) \
__pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_DEVICE_nGnRnE))
#define pgprot_writecombine(prot) \
__pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_NORMAL_NC))
#define pgprot_device(prot) \
__pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_DEVICE_nGnRE))
| 메모리 타입 | x86 PAT 슬롯 | ARM64 MAIR 인덱스 | 캐시 동작 | TLB 관련 특이사항 |
|---|---|---|---|---|
| WB (Write-Back) | 0 (기본) | 4 (Normal WB) | 읽기·쓰기 모두 캐시, 성능 최고 | 일반 RAM 매핑, TLB 동작 무관 |
| WT (Write-Through) | 1 | 6 (Normal WT) | 읽기 캐시, 쓰기 즉시 반영 | 일반적, TLB 동작 무관 |
| WC (Write-Combining) | 7 | 3 (Normal NC) | 쓰기 버퍼 합산 후 전송 | GPU/프레임버퍼, TLB 히트 후 WC 버퍼 경유 |
| UC (Uncacheable) | 3 | 0 (Device nGnRnE) | 캐시 전혀 없음 | 매 접근마다 물리 메모리 직접 접근, TLB는 여전히 사용 |
| Device nGnRE | 해당없음 | 1 | 장치 메모리, 재정렬 없음 | 추측 실행(Speculative Load) 금지 |
메모리 타입 변경 시 TLB 무효화 필수: pgprot_noncached() 등으로 보호 플래그를 변경하면 반드시 TLB Shootdown을 수행해야 합니다. TLB에 이전 메모리 타입이 캐시된 상태로 접근하면 구형 메모리 타입으로 동작하여 데이터 일관성(Coherency)이 깨질 수 있습니다. 특히 WB 영역을 UC로 재매핑할 때는 캐시 플러시(clflush)도 병행해야 합니다.
TLB 내부 구조
TLB는 단순한 캐시가 아니라, 마이크로아키텍처 수준에서 정교하게 설계된 연관 메모리(Content-Addressable Memory, CAM)입니다. 프로세서마다 구조와 크기가 다르며, 성능에 직접적인 영향을 줍니다.
Set-Associative vs Fully-Associative
Split TLB (ITLB / DTLB)
최신 프로세서는 TLB를 명령어(ITLB)와 데이터(DTLB)로 분리합니다. 이는 CPU 파이프라인(Pipeline)의 명령어 페치와 데이터 접근이 동시에 발생하므로, 각각 독립적인 TLB 포트가 필요하기 때문입니다.
TLB 계층 구조 (최신 x86 프로세서):
L1 ITLB (명령어):
- 4KB: 8-way, 64 entries (Fully-Associative 인 경우도 있음)
- 2MB: 8 entries, Fully-Associative
- 1 cycle 지연
L1 DTLB (데이터):
- 4KB: 4-way, 64 entries
- 2MB: 4-way, 32 entries
- 1GB: 4-way, 4 entries
- 1 cycle 지연
L2 STLB (통합 - Second-level TLB):
- 4KB + 2MB: 12-way, 1536~2048 entries
- 1GB: 4-way, 16 entries
- 7~8 cycles 지연
Miss → 페이지 테이블 워크 (Page Walker):
- 하드웨어 워커 (x86/ARM)
- 200+ cycles 지연
최신 프로세서 TLB 스펙 비교
| 프로세서 | L1 ITLB (4KB) | L1 DTLB (4KB) | L2 STLB (4KB) | L1 DTLB (2MB) | 워커 |
|---|---|---|---|---|---|
| Intel Alder Lake (P-core) | 8-way, 128 | 4-way, 64 | 12-way, 2048 | FA, 32 | HW, 2 병렬 |
| Intel Sapphire Rapids | 8-way, 128 | 4-way, 96 | 16-way, 2048 | FA, 32 | HW, 4 병렬 |
| AMD Zen 4 | FA, 64 | FA, 72 | 8-way, 3072 | FA, 64 | HW, 2 병렬 |
| ARM Cortex-A78 | FA, 32 | FA, 40 | 5-way, 1024 | FA, 32 | HW |
| ARM Neoverse V2 | FA, 48 | FA, 48 | 8-way, 2048 | FA, 48 | HW, 2 병렬 |
| Apple M2 (Avalanche) | FA, 192 | FA, 160 | 12-way, 3072 | FA, 64 | HW |
TLB 교체 정책
TLB가 가득 찼을 때 새 엔트리를 위해 기존 엔트리를 퇴거(evict)하는 정책입니다.
| 정책 | 설명 | 사용 프로세서 |
|---|---|---|
| Pseudo-LRU | 근사 LRU. 트리 기반으로 최근 미사용 후보 선택 | Intel (L1 TLB) |
| Round-Robin | 순서대로 교체. 하드웨어 단순 | 일부 ARM 코어 |
| Not-Recently-Used | 참조 비트 기반. 최근 미사용 엔트리 교체 | Intel L2 STLB |
| Random | 무작위 교체. Worst-case 보장 | 일부 임베디드 |
TLB Coalescing / Contiguous PTE
현대 CPU는 TLB 엔트리 수가 제한적이므로, 인접하고 동일한 속성을 가진 여러 PTE를 하나의 TLB 엔트리로 합치는 TLB 합체(TLB Coalescing) 기법을 사용합니다. 이를 통해 동일한 물리 TLB 슬롯으로 더 넓은 가상 주소 범위를 커버할 수 있습니다.
Intel TLB Coalescing
Intel 프로세서는 하드웨어 레벨에서 자동으로 TLB 합체를 수행합니다. 예를 들어 물리적으로 연속된 8개의 4KB 페이지가 동일한 보호 속성(R/W, U/S, XD 등)을 가질 경우, CPU 내부 로직이 이를 단일 32KB TLB 엔트리로 병합할 수 있습니다. 소프트웨어가 별도로 처리할 필요 없이 하드웨어가 투명하게 처리합니다.
- 연속 PTE의 물리 주소가 정렬(aligned)되어야 합니다.
- 모든 PTE의 보호 비트(W, U, XD, G, PAT 등)가 동일해야 합니다.
- 합체 개수와 크기는 마이크로아키텍처마다 다릅니다 (예: 8개 연속 → 32KB, 16개 연속 → 64KB).
ARM64 Contiguous PTE (PTE_CONT)
ARM64는 소프트웨어가 명시적으로 연속 힌트를 제공합니다. PTE 비트 52에 해당하는 PTE_CONT 비트를 설정하면, 하드웨어 Table Walk Unit(TWU)이 이를 하나의 합체된 TLB 엔트리로 처리합니다.
- 4KB 그래뉼(granule) 사용 시: 16개 연속 PTE → 64KB 단일 TLB 엔트리
- 16KB 그래뉼 사용 시: 128개 연속 PTE → 2MB 단일 TLB 엔트리
- 64KB 그래뉼 사용 시: 32개 연속 PMD → 1GB 단일 TLB 엔트리
/* arch/arm64/include/asm/pgtable-hwdef.h */
#define PTE_CONT (_AT(pteval_t, 1) << 52) /* 연속 힌트 비트 */
#define CONT_PTES 16 /* 4KB 그래뉼: 16개 연속 */
#define CONT_PTE_SIZE (CONT_PTES * PAGE_SIZE) /* = 64KB */
#define CONT_PTE_MASK (~(CONT_PTE_SIZE - 1))
/* arch/arm64/mm/contpte.c (CONFIG_ARM64_CONTPTE) */
/*
* set_ptes()가 연속 힌트를 자동으로 처리합니다.
* CONT_PTES개 PTE가 정렬 조건을 만족하면 PTE_CONT를 설정합니다.
*/
void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pte, unsigned int nr)
{
unsigned long next;
unsigned long end = addr + (unsigned long)nr * PAGE_SIZE;
unsigned long pfn = pte_pfn(pte);
while (addr != end) {
next = min(ALIGN(addr + 1, CONT_PTE_SIZE), end);
if ((next - addr == CONT_PTE_SIZE) &&
IS_ALIGNED(pfn << PAGE_SHIFT, CONT_PTE_SIZE)) {
/* 조건 충족: PTE_CONT 비트 설정 */
__contpte_try_fold(mm, addr, ptep, pte);
} else {
__set_ptes(mm, addr, ptep, pte, (next - addr) / PAGE_SIZE);
}
addr = next;
pfn += (next - addr) / PAGE_SIZE;
}
}
리눅스 커널은 CONFIG_ARM64_CONTPTE 옵션(커널 6.5+)으로 이 기능을 활성화하며, set_ptes() 호출 시 자동으로 연속 힌트를 적용합니다. pte_unmap()이나 속성 변경 시에는 contpte_try_unfold()로 분해 후 처리합니다.
| 시나리오 | 일반 4KB PTE | Contiguous PTE (64KB) | 향상 |
|---|---|---|---|
| TLB 슬롯 소비 (64KB 매핑) | 16개 | 1개 | 16배 절약 |
| TLB Miss율 (순차 접근) | 기준 | ~93% 감소 | 대폭 감소 |
| TLB 무효화 비용 | TLBI x 16 | TLBI x 1 | 16배 절약 |
| 대규모 파일 mmap (GB 단위) | 높은 TLB 압력 | 낮은 TLB 압력 | 처리량 향상 |
| 적용 조건 | 제한 없음 | 정렬+연속+동일 속성 | 조건 충족 시 |
CONFIG_ARM64_CONTPTE=y를 사용하면 THP 미적용 영역에서도 TLB 효율을 크게 높일 수 있습니다. Intel에서는 별도 설정 없이 하드웨어가 자동으로 수행합니다.
mmu_gather 배칭 프레임워크
struct mmu_gather는 페이지 테이블을 해체하고 TLB를 무효화하는 작업을 배칭(batching)하여 TLB Shootdown 비용을 최소화하는 프레임워크입니다. munmap(), exit_mmap(), mremap() 등에서 핵심적으로 사용됩니다.
struct mmu_gather
/* include/asm-generic/tlb.h */
struct mmu_gather {
struct mm_struct *mm; /* 대상 mm */
/* 무효화 범위 */
unsigned long start;
unsigned long end;
/* 무효화 필요 플래그 */
unsigned int freed_tables : 1; /* 페이지 테이블 해제됨 */
unsigned int need_flush_all : 1; /* 전체 플러시 필요 */
unsigned int cleared_ptes : 1;
unsigned int cleared_pmds : 1;
unsigned int cleared_puds : 1;
unsigned int cleared_p4ds : 1;
unsigned int vma_exec : 1;
unsigned int vma_huge : 1;
/* 해제할 페이지 배치 */
struct mmu_gather_batch *active; /* 현재 배치 */
struct mmu_gather_batch local; /* 로컬 배치 (스택) */
struct page *__pages[MMU_GATHER_BUNDLE]; /* 8개 */
};
사용 패턴
/* mm/memory.c - 일반적인 mmu_gather 사용 패턴 */
void unmap_region(struct mm_struct *mm,
struct maple_tree *mt,
struct vm_area_struct *vma,
unsigned long start, unsigned long end)
{
struct mmu_gather tlb;
/* 1단계: 배칭 시작 - TLB 무효화 준비 */
tlb_gather_mmu(&tlb, mm);
/* 2단계: PTE를 0으로 설정하고, 페이지를 배치에 추가 */
unmap_vmas(&tlb, mt, vma, start, end, 0, false);
/* 3단계: 페이지 테이블 자체를 해제 */
free_pgtables(&tlb, mt, vma, start, end);
/* 4단계: 배칭된 TLB 무효화 실행 + 페이지 해제 */
tlb_finish_mmu(&tlb);
}
/* include/asm-generic/tlb.h - tlb_remove_page 내부 */
static inline bool __tlb_remove_page(struct mmu_gather *tlb,
struct page *page,
int delay_rmap)
{
struct mmu_gather_batch *batch = tlb->active;
/* 배치에 페이지 추가 */
batch->pages[batch->nr++] = page;
/* 배치가 가득 찼으면 true 반환 → 중간 플러시 트리거 */
return batch->nr == batch->max;
}
/* 중간 플러시: 배치 가득 찼을 때 */
static void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
{
if (!tlb->end)
return;
/* 아키텍처별 TLB 플러시 (범위 or 전체) */
tlb_flush(tlb);
/* 범위 리셋하여 다음 배치 준비 */
tlb->start = TASK_SIZE;
tlb->end = 0;
}
mmu_gather 사용 중에는 mmap_lock을 보유해야 합니다. 배칭 도중 다른 스레드(Thread)가 같은 VMA를 수정하면 use-after-free가 발생할 수 있습니다. 커널 6.1부터 maple tree 기반 VMA 관리로 이 문제가 완화되었습니다.
페이지 폴트 핸들링
페이지 폴트(Page Fault)는 MMU가 가상 주소를 물리 주소로 변환할 수 없을 때 발생하는 예외입니다. 리눅스 커널의 페이지 폴트 핸들러(Handler)는 demand paging, COW, 스왑 인, 파일 매핑 등 가상 메모리의 핵심 기능을 구현합니다.
페이지 폴트 분류
| 종류 | 원인 | 처리 | 비용 |
|---|---|---|---|
| 마이너 폴트 (Minor) | 페이지가 메모리에 있지만 PTE 미설정 | PTE 설정만 (디스크 I/O 없음) | ~1-10 us |
| 메이저 폴트 (Major) | 페이지가 디스크에 있음 (스왑/파일) | 디스크에서 읽기 후 PTE 설정 | ~1-10 ms |
| COW 폴트 | 읽기전용 페이지에 쓰기 시도 | 페이지 복사 후 쓰기 가능으로 설정 | ~1-5 us |
| SEGV (비정상) | 유효하지 않은 주소 접근 | SIGSEGV 시그널(Signal) 전송 | 프로세스 종료 |
| 커널 폴트 | 커널 코드의 잘못된 접근 | Oops/Panic 또는 fixup | 시스템 불안정 |
do_page_fault 처리 흐름
handle_mm_fault 핵심 경로
/* mm/memory.c */
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address,
unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
};
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
/* 1단계: PGD → P4D → PUD → PMD 테이블 탐색/할당 */
pgd = pgd_offset(mm, address);
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
pud = pud_alloc(mm, p4d, address);
if (!pud)
return VM_FAULT_OOM;
/* Huge Page (1GB) 체크 */
if (pud_trans_huge(*pud) || pud_devmap(*pud)) {
vmf.pud = pud;
return create_huge_pud(&vmf);
}
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
return VM_FAULT_OOM;
/* Huge Page (2MB) 체크 - THP */
if (pmd_trans_huge(*pmd) || pmd_devmap(*pmd)) {
vmf.pmd = pmd;
return create_huge_pmd(&vmf);
}
/* 2단계: PTE 레벨 처리 */
vmf.pmd = pmd;
return handle_pte_fault(&vmf);
}
코드 설명
- 3~8행
vm_fault구조체를 초기화합니다.address & PAGE_MASK로 페이지 경계에 정렬하고,linear_page_index()는 VMA 내 파일 오프셋(페이지 단위)을 계산합니다. 이 구조체는 이후 폴트 처리 전 과정에서 컨텍스트로 전달됩니다. 소스:mm/memory.c - 13~16행
pgd_offset(mm, address)는mm->pgd에서 가상 주소의 PGD 인덱스에 해당하는 엔트리 포인터를 반환합니다.p4d_alloc()은 P4D 테이블이 없으면 새로 할당합니다. 4단계 페이징에서는 P4D가 PGD로 폴딩되어 실제 할당이 발생하지 않습니다. - 18~20행
pud_alloc()은 PUD 테이블을 할당/반환합니다. 각*_alloc()함수는 이미 테이블이 존재하면 기존 테이블을 반환하고, 없으면__get_free_page(GFP_PGTABLE_USER)로 새 4KB 페이지를 할당합니다. 할당 실패 시VM_FAULT_OOM을 반환합니다. - 22~26행PUD 레벨에서 1GB Huge Page를 검사합니다.
pud_trans_huge()는 PUD 엔트리가 다음 테이블을 가리키는 대신 1GB 물리 블록을 직접 매핑하는지 확인합니다. 참이면create_huge_pud()로 Huge Page 폴트를 처리하고, PMD/PTE 단계를 건너뜁니다. - 32~36행PMD 레벨에서 2MB THP(Transparent Huge Page)를 검사합니다.
pmd_trans_huge()가 참이면 PMD가 512개 PTE 대신 하나의 2MB 블록을 매핑합니다. 이 경우create_huge_pmd()로 처리합니다. 둘 다 아니면handle_pte_fault()로 최종 4KB PTE 레벨 폴트를 처리합니다.
Speculative Page Fault: 커널 6.x에서 논의 중인 최적화입니다. mmap_lock을 잡지 않고 낙관적으로 폴트를 처리한 뒤, 실패하면 락을 잡고 재시도합니다. 멀티스레드 워크로드에서 mmap_lock 경합을 크게 줄여줍니다.
NUMA 밸런싱과 TLB
NUMA(Non-Uniform Memory Access) 시스템에서 프로세스가 원격 노드(remote node)의 메모리에 접근하면 레이턴시가 높아집니다. 리눅스 커널의 NUMA 밸런싱(NUMA Balancing)은 프로세스가 실제로 접근하는 메모리 패턴을 런타임에 측정하여 데이터를 가까운 노드로 자동 마이그레이션합니다.
PROT_NONE 트릭: 페이지 프로빙
NUMA 밸런싱의 핵심 기법은 PTE의 Present 비트를 일시적으로 제거(PROT_NONE 설정)하는 것입니다. Present 비트가 없으면 해당 페이지 접근 시 페이지 폴트가 발생하고, 커널은 그 폴트 핸들러에서 접근한 CPU와 페이지의 물리 위치를 비교하여 이동 여부를 결정합니다.
/* kernel/sched/fair.c — NUMA 스캐닝 주기 */
/*
* task_numa_work(): 주기적으로 호출되어 VMA의 PTE를
* PROT_NONE으로 변경합니다. 이후 접근 시 do_numa_page()가
* 호출되어 마이그레이션을 결정합니다.
*/
static void task_numa_work(struct callback_head *work)
{
struct task_struct *p = current();
struct mm_struct *mm = p->mm;
struct vm_area_struct *vma;
unsigned long start, end;
/* 스캔 범위 결정 (scan_size 제한) */
start = mm->numa_scan_offset;
end = start + NUMA_MIGRATION_RATE;
mmap_read_lock(mm);
for_each_vma_range(vmi, vma, start, end) {
if (!(vma_is_accessible(vma)))
continue;
/* PTE를 PROT_NONE으로 변경 → TLB 무효화 유발 */
change_prot_numa(vma, start, end);
}
mmap_read_unlock(mm);
/* 다음 스캔 위치 갱신 */
mm->numa_scan_offset = end;
}
/* mm/memory.c — NUMA 폴트 핸들러 */
static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page = NULL;
int page_nid = NUMA_NO_NODE;
int target_nid, last_cpupid = -1;
bool migrated = false;
/* 1단계: PTE 잠금 및 페이지 조회 */
vmf_pte_lock(vmf);
page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (!page)
goto out_unlock;
/* 2단계: 현재 페이지의 NUMA 노드 파악 */
page_nid = page_to_nid(page);
/* 3단계: 마이그레이션 대상 노드 결정 */
target_nid = numa_migrate_prep(page, vmf, vmf->address,
page_nid, &flags);
if (target_nid == NUMA_NO_NODE) {
/* 이미 최적 위치: PTE 복원만 수행 */
put_page(page);
goto out_map;
}
/* 4단계: 페이지 마이그레이션 시도 */
migrated = migrate_misplaced_page(page, vmf, target_nid);
out_map:
/* 5단계: PTE 복원 (PROT_NONE → 원래 접근 권한) */
pte_mknuma_restore(vmf);
return 0;
}
task_numa_placement()와 마이그레이션 결정
task_numa_placement()는 주기적으로 각 태스크의 NUMA 통계를 집계하여 최적의 실행 노드와 메모리 노드를 결정합니다. 페이지 폴트 통계(numa_faults[])를 분석해 CPU 친화성(CPU affinity)과 메모리 배치를 함께 조정합니다.
/* kernel/sched/fair.c */
static void task_numa_placement(struct task_struct *p)
{
int seq, nid, max_nid = NUMA_NO_NODE;
unsigned long max_faults = 0;
for_each_online_node(nid) {
unsigned long faults;
/* 노드별 폴트 수 집계: 로컬+원격 폴트 합산 */
faults = task_faults(p, nid);
if (faults > max_faults) {
max_faults = faults;
max_nid = nid; /* 가장 많이 접근한 노드 */
}
}
/* 선호 노드가 바뀌면 태스크 마이그레이션 검토 */
if (max_nid != p->numa_preferred_nid)
sched_setnuma(p, max_nid);
}
| 단계 | 동작 | TLB 영향 |
|---|---|---|
| PTE 마킹 (PROT_NONE) | change_prot_numa(): PTE Present 비트 제거 | 해당 페이지 TLB 무효화 (INVLPG / TLBI) |
| 페이지 접근 | PROT_NONE PTE → 페이지 폴트 발생 | TLB Miss (의도적 프로빙) |
| 마이그레이션 결정 | do_numa_page(): 노드 비교 후 이동 | PTE 업데이트 → TLB 갱신 |
| 마이그레이션 완료 | 새 PFN으로 PTE 재설정 | 신규 TLB 엔트리 (로컬 노드 PA) |
| 장기 효과 | 로컬 메모리 접근 증가 | TLB Hit율 향상, 레이턴시 감소 |
NUMA 밸런싱 튜닝
# NUMA 밸런싱 활성화/비활성화
echo 1 > /proc/sys/kernel/numa_balancing
# 스캔 주기 (ms 단위, 기본 1000ms)
echo 1000 > /proc/sys/kernel/numa_balancing_scan_period_min_ms
echo 60000 > /proc/sys/kernel/numa_balancing_scan_period_max_ms
# 스캔 크기 (MiB 단위, 기본 256MiB)
echo 256 > /proc/sys/kernel/numa_balancing_scan_size_mb
# NUMA 폴트 통계 확인
grep numa /proc/<PID>/sched
numa_balancing을 비활성화하거나 numactl --membind로 메모리 배치를 명시적으로 고정하는 것이 효과적입니다.
do_anonymous_page() 소스 분석
do_anonymous_page()는 익명(Anonymous) 매핑에서 최초 페이지 접근 시 호출됩니다. 커널은 메모리를 아끼기 위해 제로 페이지(Zero Page) 최적화를 사용합니다. 최초 읽기 접근이면 공유 제로 페이지를 매핑하고, 최초 쓰기 접근이면 새 물리 페이지를 할당하여 0으로 초기화합니다.
/* mm/memory.c — 익명 매핑 폴트 핸들러 */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct folio *folio;
pte_t entry;
vm_fault_t ret = 0;
/* ① 익명 VMA 역매핑 준비 */
if (vma_is_anonymous(vma) && unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
/* ② 읽기 폴트: 공유 제로 페이지 매핑 (물리 페이지 미할당) */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!pte_none(*vmf->pte)) { /* 경쟁: 이미 매핑됨 */
goto unlock;
}
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0; /* PF 해결: 물리 메모리 0 bytes 소비 */
}
/* ③ 쓰기 폴트: 새 페이지 할당 + 0 초기화 */
folio = vma_alloc_zeroed_movable_folio(vma, vmf->address);
if (!folio)
return VM_FAULT_OOM;
/* ④ 익명 역매핑(Reverse Mapping) 등록 */
folio_get(folio);
folio_add_new_anon_rmap(folio, vma, vmf->address, RMAP_EXCLUSIVE);
folio_add_lru_vma(folio, vma); /* LRU 리스트에 추가 */
/* ⑤ PTE 생성: R/W, Dirty, Young 플래그 설정 */
entry = mk_pte(&folio->page, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry), vma);
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!pte_none(*vmf->pte)) { /* 경쟁: 다른 CPU가 먼저 처리 */
folio_put(folio);
goto unlock;
}
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
}
코드 설명
- ② 읽기 경로
FAULT_FLAG_WRITE가 없으면 읽기 폴트입니다.my_zero_pfn()은 현재 NUMA 노드의 공유 제로 페이지 PFN을 반환합니다.pte_mkspecial()은 해당 PTE를 "특수 매핑"으로 표시하여 역매핑 시스템이 이 페이지를 일반 folio로 취급하지 않도록 합니다. - ③ 쓰기 경로
vma_alloc_zeroed_movable_folio()는MIGRATE_MOVABLE타입의 folio를 할당하고clear_page()로 0 초기화합니다. MOVABLE 타입은 메모리 압축(Compaction) 시 이동 가능하여 단편화를 줄입니다. 이 함수는 커널 6.1 이후alloc_page_vma()를 대체했습니다. - ④ 역매핑 등록
folio_add_new_anon_rmap()은 역매핑 자료구조(anon_vma)를 folio에 연결합니다. 이후 페이지 회수(Reclaim)나 COW 시 해당 PTE를 빠르게 찾을 수 있습니다.RMAP_EXCLUSIVE는 이 folio가 이 VMA에만 매핑됨을 표시합니다. - ⑤ PTE 설정
pte_sw_mkyoung()은 소프트웨어 Accessed 비트를 설정합니다. 하드웨어가 Accessed 비트를 자동으로 설정하지 않는 아키텍처(일부 RISC-V)를 위한 폴백입니다. 쓰기 VMA라면pte_mkwrite(pte_mkdirty())로 바로 R/W + Dirty PTE를 만들어 다음 쓰기 폴트를 방지합니다.
제로 페이지 최적화의 실용적 효과: calloc(1GB, 1) 호출 시 운영체제는 1GB 물리 메모리를 즉시 할당하지 않습니다. 모든 PTE를 공유 제로 페이지로 설정하여 실제 쓰기가 일어나는 페이지만 물리 메모리를 소비합니다. 희소 행렬(Sparse Matrix), 대형 버퍼 사전 할당, 데이터베이스 버퍼 풀 등의 패턴에서 메모리 사용량을 수십 배 줄일 수 있습니다.
do_fault() 파일 매핑 폴트 분석
파일 기반 매핑(File-backed VMA)에서 발생하는 폴트는 do_fault()가 처리합니다. do_fault()는 폴트 종류에 따라 세 가지 하위 핸들러로 분기합니다.
/* mm/memory.c — 파일 매핑 폴트 디스패처 */
static vm_fault_t do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct vm_operations_struct *vma_ops = vma->vm_ops;
vm_fault_t ret;
if (!vma_ops->fault)
return VM_FAULT_SIGBUS; /* vm_ops 없는 특수 매핑 */
if (!(vmf->flags & FAULT_FLAG_WRITE)) {
/* ① 읽기 폴트: 페이지 캐시에서 읽어 PTE 설정 */
ret = do_read_fault(vmf);
} else if (!(vma->vm_flags & VM_SHARED)) {
/* ② 쓰기 폴트 + private 매핑: COW 복사 후 새 PTE */
ret = do_cow_fault(vmf);
} else {
/* ③ 쓰기 폴트 + shared 매핑: 캐시 페이지를 Dirty로 직접 설정 */
ret = do_shared_fault(vmf);
}
return ret;
}
/* ① 읽기 폴트 경로 — do_fault_around() + filemap_fault() 위임 */
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
vm_fault_t ret = 0;
/* 선행 매핑(Fault-around): 주변 페이지 64KB 미리 PTE 설정 */
if (vmf->vma->vm_ops->map_pages &&
fault_around_bytes() != PAGE_SIZE) {
if (likely(!(vmf->flags & FAULT_FLAG_TRIED)))
ret = do_fault_around(vmf);
if (ret)
return ret;
}
/* vma->vm_ops->fault() 호출 → filemap_fault() */
ret = __do_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
finish_fault(vmf); /* set_pte_at() + update_mmu_cache_range() */
return ret;
}
/* mm/filemap.c — 페이지 캐시 조회 및 리드어헤드 */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
struct file *file = vmf->vma->vm_file;
struct address_space *mapping = file->f_mapping;
pgoff_t offset = vmf->pgoff;
struct folio *folio;
vm_fault_t ret = 0;
/* ① XArray(페이지 캐시 트리)에서 folio 조회 */
folio = filemap_get_folio(mapping, offset);
if (IS_ERR(folio)) {
/* 캐시 미스: 동기 리드어헤드로 디스크에서 읽기 */
page_cache_sync_readahead(mapping,
&file->f_ra, file, offset,
ra_pages(mapping)); /* ② 동기 리드어헤드 (블로킹) */
folio = filemap_get_folio(mapping, offset);
if (IS_ERR(folio)) {
ret = VM_FAULT_OOM;
goto out;
}
}
/* 리드어헤드 윈도우 끝 마커 감지: 다음 윈도우 비동기 로드 */
if (PageReadahead(&folio->page))
page_cache_async_readahead(mapping,
&file->f_ra, file, folio, offset,
ra_pages(mapping)); /* ③ 비동기 리드어헤드 (논블로킹) */
/* 페이지가 최신(Uptodate) 상태인지 확인 후 블록 I/O */
if (unlikely(!folio_test_uptodate(folio))) {
folio_lock(folio);
if (!folio_test_uptodate(folio))
ret = filemap_read_folio(file, file->f_op->read_iter, folio);
folio_unlock(folio);
}
vmf->page = &folio->page; /* vm_fault에 페이지 연결 후 반환 */
out:
return ret;
}
코드 설명
- do_fault() ① 읽기
do_read_fault()는 먼저do_fault_around()로 폴트 주소 주변 최대 64KB를 미리 매핑합니다(fault_around_bytes기본값). 이는 mmap된 파일을 순차 접근할 때 폴트 횟수를 크게 줄입니다. 이 값은/proc/sys/vm/fault_around_bytes로 조정 가능합니다. - do_fault() ② private 쓰기
do_cow_fault()는 페이지 캐시에서 원본 페이지를 읽은 뒤 새 익명 페이지에 복사하고 그 새 페이지를 PTE에 쓰기 가능으로 연결합니다. 수정 내용은 파일에 반영되지 않습니다. - do_fault() ③ shared 쓰기
do_shared_fault()는 페이지 캐시 folio에 직접 쓰기 권한을 부여합니다. 수정된 내용은 dirty 페이지로 표시되어 나중에 writeback 스레드가 파일시스템에 반영합니다.mmap(MAP_SHARED)쓰기의 동작입니다. - filemap_fault() ②
page_cache_sync_readahead()는 현재 스레드가 블로킹되어 최소 1개 페이지를 읽어들입니다.file_ra_state(f_ra)가 리드어헤드 윈도우 크기를 동적으로 조절합니다. 첫 접근 시 4 페이지(16KB), 순차 접근 감지 시 최대 512 페이지(2MB)까지 확장됩니다. - filemap_fault() ③
PageReadahead플래그는 리드어헤드 윈도우의 마지막 페이지에 설정됩니다. 이 페이지에 접근하면page_cache_async_readahead()로 다음 윈도우를 백그라운드에서 미리 로드합니다. 현재 폴트 처리는 블로킹되지 않습니다. - TLB 연동
finish_fault()내부에서set_pte_at()으로 PTE를 설정한 뒤update_mmu_cache_range()를 호출합니다. ARM64에서는 DSB + ISB 배리어로 TLB 일관성을 보장하고, x86에서는 새 PTE 설정 후 TLB가 자동으로 갱신됩니다.
COW (Copy-on-Write)
fork() 시 부모와 자식 프로세스의 페이지 테이블은 같은 물리 페이지를 가리키되, PTE를 읽기 전용(Read-Only)으로 표시합니다. 쓰기가 발생하면 페이지 폴트를 통해 복사가 이루어집니다. 이를 COW (Copy-on-Write)라 합니다.
COW 메커니즘 단계
do_wp_page 핵심 로직
/* mm/memory.c */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
struct vm_area_struct *vma = vmf->vma;
struct folio *folio;
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (!vmf->page) {
/* 특수 매핑 (pfn, zero page 등) - 항상 복사 */
return wp_page_copy(vmf);
}
folio = page_folio(vmf->page);
/* 핵심 최적화: 참조 카운트가 1이면 복사 불필요 */
if (folio_ref_count(folio) == 1) {
if (!folio_test_ksm(folio)) {
/* reuse: PTE를 R/W로 복원만 하면 됨 */
wp_page_reuse(vmf, folio);
return 0;
}
}
/* 복사 필요: 새 페이지 할당 → 데이터 복사 → PTE 갱신 */
return wp_page_copy(vmf);
}
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
struct page *new_page;
/* 1. 새 페이지 할당 */
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
/* 2. 데이터 복사 (4KB memcpy) */
copy_user_highpage(new_page, vmf->page, vmf->address, vma);
/* 3. PTE 갱신: 새 페이지, R/W 설정 */
entry = mk_pte(new_page, vma->vm_page_prot);
entry = pte_mkwrite(pte_mkdirty(entry));
set_pte_at(mm, vmf->address, vmf->pte, entry);
/* 4. TLB 무효화 (기존 읽기전용 매핑 제거) */
flush_tlb_page(vma, vmf->address);
return 0;
}
커널 소스 분석: 폴트 처리 콜 체인
페이지 폴트 처리의 핵심 콜 체인인 do_page_fault() → handle_mm_fault() → handle_pte_fault() → do_wp_page()를 커널 소스 레벨에서 단계별로 분석합니다. 각 함수의 역할 분담과 인자 전달 방식을 이해하면 메모리 관련 버그를 정확히 진단할 수 있습니다.
콜 체인 전체 구조
handle_pte_fault() 소스 분석
/* mm/memory.c */
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
if (unlikely(pmd_none(*vmf->pmd))) {
/* PMD가 없으면 PTE 테이블 자체가 아직 없음 */
vmf->pte = NULL;
vmf->flags &= ~FAULT_FLAG_WRITE;
} else {
/* PTElock 없이 orig_pte 스냅샷 획득 */
vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
vmf->orig_pte = ptep_get_lockless(vmf->pte);
if (pte_none(vmf->orig_pte)) {
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
if (!vmf->pte) {
/* PTE 없음: 익명 페이지 또는 파일 매핑으로 분기 */
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
else
return do_fault(vmf);
}
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf); /* 스왑 아웃된 페이지 */
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf); /* NUMA 밸런싱 폴트 */
/* PTElock 획득 후 재확인 (race 방지) */
vmf_pte_lock(vmf);
entry = vmf_orig_pte_dup(vmf);
if (unlikely(!pte_same(entry, vmf->orig_pte))) {
/* 락 사이에 다른 CPU가 PTE를 변경함 */
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
if (vmf->flags & (FAULT_FLAG_WRITE | FAULT_FLAG_UNSHARE)) {
if (!pte_write(entry))
return do_wp_page(vmf); /* COW: 쓰기 보호 해제 */
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry); /* Accessed 비트 설정 */
if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte,
entry, vmf->flags & FAULT_FLAG_WRITE)) {
update_mmu_cache_range(vmf, vmf->vma, vmf->address,
vmf->pte, 1);
}
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
코드 설명
- 3행
pmd_none()체크: PMD 엔트리가 아직 없으면 PTE 테이블 자체가 존재하지 않으므로vmf->pte = NULL로 설정하고 익명/파일 폴트 경로로 진입합니다. - 9행
ptep_get_lockless(): 락 없이 원자적으로 PTE 값을 읽습니다. 이 스냅샷은orig_pte에 저장되어 이후 경쟁 조건 검사에 사용됩니다. - 20행
vma_is_anonymous()로 분기: 익명 VMA면do_anonymous_page()(제로 페이지 또는 새 페이지 할당), 파일 매핑이면do_fault()(filemap_fault 경유 읽기)로 처리합니다. - 26행
pte_present()가 거짓이면 스왑 아웃 상태입니다.do_swap_page()가 스왑 파티션에서 페이지를 읽어 다시 매핑합니다. - 29행
pte_protnone(): NUMA 밸런싱을 위해 PTE를 의도적으로 접근 불가로 표시한 경우입니다.do_numa_page()가 더 가까운 NUMA 노드로 페이지를 마이그레이션합니다. - 33행
vmf_pte_lock()으로 PTElock(스핀락)을 획득한 뒤 PTE 값을 다시 읽어orig_pte와 비교합니다. 락을 잡는 사이 다른 CPU가 PTE를 이미 변경했다면 0을 반환하고 폴트를 재시도하게 합니다. - 41행쓰기 폴트(
FAULT_FLAG_WRITE)이고 현재 PTE가 쓰기 불가(!pte_write())이면do_wp_page()를 호출하여 COW를 처리합니다. 이것이 콜 체인의 핵심 분기점입니다. - 46행
pte_mkyoung()은 하드웨어 Accessed 비트를 소프트웨어적으로 설정합니다.ptep_set_access_flags()가 TLB를 업데이트하며, 변경이 있었으면update_mmu_cache_range()로 아키텍처별 캐시 동기화를 수행합니다.
do_wp_page() 분석
/* mm/memory.c — COW 핵심 함수 */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
struct vm_area_struct *vma = vmf->vma;
struct folio *folio = NULL;
pte_t pte;
if (userfaultfd_pte_wp(vma, vmf_orig_pte_dup(vmf))) {
/* userfaultfd WP 이벤트: 사용자 공간 폴트 핸들러에 위임 */
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_WP);
}
vmf->page = vm_normal_page(vma, vmf->address, vmf_orig_pte_dup(vmf));
if (!vmf->page) {
/* 특수 매핑 (pfnmap, zero page): 단순 복사 */
if (unshare) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
return wp_page_copy(vmf);
}
folio = page_folio(vmf->page);
if (folio_is_zone_device(folio)) {
/* DAX / HMEM 디바이스 메모리: 항상 복사 */
folio_get(folio);
pte_unmap_unlock(vmf->pte, vmf->ptl);
return wp_page_copy(vmf);
}
folio_lock(folio);
if (folio_test_anon(folio) &&
folio_test_large(folio) &&
!PageAnonExclusive(vmf->page)) {
/* 대형 익명 folio에서 단일 페이지 분리 필요 */
folio_unlock(folio);
folio_get(folio);
pte_unmap_unlock(vmf->pte, vmf->ptl);
if (folio_referenced(folio, 0, vma->vm_mm, NULL) != 0)
return VM_FAULT_RETRY;
return wp_page_copy(vmf);
}
/* 단독 참조(folio_ref_count == 1) + 익명 + 비-KSM: reuse */
if (PageAnonExclusive(vmf->page)) {
wp_page_reuse(vmf, folio);
return 0;
}
folio_unlock(folio);
return wp_page_copy(vmf);
}
코드 설명
- 9행
userfaultfd_pte_wp(): userfaultfd(UFFD) WP 모드가 활성화된 VMA이면 COW 처리를 사용자 공간의 폴트 핸들러에 위임합니다. 가상화 환경의 실시간 마이그레이션(live migration)에서 핵심적으로 활용됩니다. - 14행
vm_normal_page(): PTE에서 일반 struct page 포인터를 반환합니다. pfnmap(디바이스 메모리 직접 매핑)이나 zero page 같은 특수 매핑은 NULL을 반환하며, 이 경우 복사로 직행합니다. - 24행
page_folio(): 커널 6.x부터 도입된 folio(연속 페이지 묶음) 인터페이스로 변환합니다. folio는 THP(Transparent Huge Page) 같은 대형 페이지도 단일 단위로 관리합니다. - 27행Zone device(DAX, HMEM) 메모리는 참조 카운트 기반 최적화를 적용할 수 없으므로 항상
wp_page_copy()를 호출합니다.folio_get()으로 락을 해제하기 전에 참조를 먼저 증가시킵니다. - 34행대형(large) 익명 folio에서 특정 페이지만 COW가 발생했을 때의 처리입니다.
PageAnonExclusive가 설정되지 않은 경우 folio를 분리해야 하므로wp_page_copy()로 이동합니다. - 46행
PageAnonExclusive: 이 페이지가 현재 단일 프로세스에만 독점적으로 매핑되어 있음을 의미하는 플래그입니다. 설정되어 있으면 복사 없이wp_page_reuse()로 PTE의 쓰기 보호만 해제합니다. 이것이 가장 빠른 COW 경로입니다.
flush_tlb_mm_range() 소스 분석
COW 완료 후에는 반드시 TLB를 무효화해야 합니다. flush_tlb_page()는 내부적으로 flush_tlb_mm_range()를 호출하며, SMP 환경에서는 TLB Shootdown IPI를 발생시킵니다.
/* arch/x86/mm/tlb.c */
void flush_tlb_mm_range(struct mm_struct *mm,
unsigned long start, unsigned long end,
unsigned int stride_shift, bool freed_tables)
{
struct flush_tlb_info *info;
u64 new_tlb_gen;
int cpu = get_cpu();
/* 로컬 CPU 정보 설정 */
info = get_flush_tlb_info(mm, start, end, stride_shift,
freed_tables, TLB_GENERATION_INVALID);
/* mm이 현재 CPU에서 활성화된 경우 로컬 플러시 */
if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {
/* 다른 CPU들에 TLB Shootdown IPI 전송 */
inc_irq_stat(irq_tlb_count);
on_each_cpu_mask(mm_cpumask(mm), flush_tlb_func, info, 1);
}
if (this_cpu_read(cpu_tlbstate.loaded_mm) == mm) {
flush_tlb_func(info); /* 로컬 CPU TLB 직접 플러시 */
}
put_flush_tlb_info();
put_cpu();
}
/* 단일 페이지 flush: flush_tlb_page() 래퍼 */
void flush_tlb_page(const struct vm_area_struct *vma, unsigned long start)
{
flush_tlb_mm_range(vma->vm_mm, start, start + PAGE_SIZE,
PAGE_SHIFT, false);
}
/* TLB Shootdown IPI 실제 처리 함수 */
static void flush_tlb_func(void *info)
{
const struct flush_tlb_info *f = info;
if (f->end == TLB_FLUSH_ALL) {
local_flush_tlb_one_user(f->start); /* INVLPG 또는 invlpgb */
} else if ((f->end - f->start) <= PAGE_SIZE << f->stride_shift) {
local_flush_tlb_one_user(f->start);
} else {
/* 범위가 넓으면 전체 TLB 플러시 (CR3 재로드) */
local_flush_tlb();
}
}
코드 설명
- 1행
flush_tlb_mm_range()는 x86-64의 TLB 무효화 핵심 함수입니다.start~end범위의 가상 주소에 대한 TLB 엔트리를 무효화하며,stride_shift로 일반 4KB 페이지와 Huge Page를 구분합니다. - 10행
get_flush_tlb_info(): per-CPU 변수에서 flush 요청 정보 구조체를 가져옵니다. 이 구조체는 flush 범위와 TLB 세대(generation) 번호를 담아 불필요한 중복 flush를 방지합니다. - 14행
mm_cpumask(mm): 해당 mm을 현재 사용 중인 CPU 집합을 반환합니다. 현재 CPU 이외에 사용 중인 CPU가 있으면 IPI(프로세서 간 인터럽트)로 TLB Shootdown을 전파합니다. - 17행
on_each_cpu_mask(): 대상 CPU 집합의 모든 CPU에 IPI를 보내flush_tlb_func를 동기적으로 실행합니다. 마지막 인자1은 완료를 기다림(synchronous)을 의미합니다. - 20행로컬 CPU가 대상 mm을 현재 실행 중이면 IPI 없이 직접
flush_tlb_func()를 호출합니다. 멀티코어에서 IPI 오버헤드를 피하는 최적화입니다. - 37행x86에서 단일 페이지 무효화는
INVLPG명령어로 수행합니다(local_flush_tlb_one_user()). 무효화 범위가 넓으면INVLPG를 반복하는 것보다 CR3 재로드(전체 TLB flush)가 더 빠릅니다.
mm_struct 핵심 필드 한국어 주석
mm_struct는 프로세스의 가상 주소 공간 전체를 표현하는 최상위 자료구조입니다. task_struct의 mm 필드가 이 구조체를 가리킵니다.
/* include/linux/mm_types.h */
struct mm_struct {
struct {
struct maple_tree mm_mt; /* VMA 트리 (Maple Tree, 6.1+) */
unsigned long mmap_base; /* mmap 영역 시작 주소 */
unsigned long mmap_legacy_base; /* 레거시 mmap 배치 기준 */
unsigned long task_size; /* 사용자 공간 최대 크기 */
pgd_t *pgd; /* 프로세스 페이지 글로벌 디렉터리 */
atomic_t mm_users; /* 이 mm을 사용하는 스레드 수 */
atomic_t mm_count; /* 참조 카운트 (마지막 해제 시 파괴) */
atomic_long_t pgtables_bytes; /* 페이지 테이블이 차지하는 바이트 */
int map_count; /* VMA 개수 (RLIMIT_AS 검사용) */
spinlock_t page_table_lock; /* PMD/PUD 레벨 락 (PTElock은 별도) */
struct rw_semaphore mmap_lock; /* VMA 트리 읽기/쓰기 세마포어 */
struct list_head mmlist; /* init_mm.mmlist 연결 리스트 */
unsigned long hiwater_rss; /* RSS 최고수위 (KB 단위 통계용) */
unsigned long hiwater_vm; /* 가상 메모리 최고수위 (페이지 수) */
unsigned long total_vm; /* 전체 매핑된 가상 페이지 수 */
unsigned long locked_vm; /* mlock()으로 잠긴 페이지 수 */
atomic64_t pinned_vm; /* GUP으로 핀된 페이지 수 */
unsigned long data_vm; /* 데이터 세그먼트 페이지 수 */
unsigned long exec_vm; /* 실행 가능 페이지 수 */
unsigned long stack_vm; /* 스택 페이지 수 */
unsigned long def_flags; /* 새 VMA 기본 플래그 */
/**
* 세그먼트 경계 주소들
*/
unsigned long start_code; /* 코드 세그먼트 시작 주소 */
unsigned long end_code; /* 코드 세그먼트 끝 주소 */
unsigned long start_data; /* 데이터 세그먼트 시작 주소 */
unsigned long end_data; /* 데이터 세그먼트 끝 주소 */
unsigned long start_brk; /* 힙 시작 주소 */
unsigned long brk; /* 현재 힙 끝 주소 (brk() 시스템 콜로 확장) */
unsigned long start_stack; /* 스택 시작(최상단) 주소 */
unsigned long arg_start; /* 프로그램 인자(argv) 시작 */
unsigned long arg_end; /* 프로그램 인자 끝 */
unsigned long env_start; /* 환경변수(envp) 시작 */
unsigned long env_end; /* 환경변수 끝 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* ELF auxiliary vector */
struct mm_rss_stat rss_stat; /* RSS 통계: 익명/파일/스왑 구분 */
struct linux_binfmt *binfmt; /* 실행 파일 포맷 핸들러 */
mm_context_t context; /* 아키텍처별 문맥 (x86: PCID/LDT, ARM64: ASID) */
unsigned long flags; /* 상태 플래그 (MMF_DUMP_*, MMF_OOM_* 등) */
struct user_namespace *user_ns; /* 프로세스 소속 사용자 네임스페이스 */
struct file *exe_file; /* 실행 파일 /proc/pid/exe 참조 */
struct mmu_notifier_subscriptions *notifier_subscriptions;
/* MMU 변경 알림 구독자 목록 (KVM, HMM 등이 등록) */
} __randomize_layout; /* 구조체 레이아웃 KASLR 랜덤화 */
/* 멀티스레드 접근이 없는 필드: 별도 캐시라인 배치 */
unsigned long cpu_bitmap[]; /* 이 mm이 활성화된 CPU 비트맵 (가변 크기) */
};
코드 설명
- 4행
mm_mt(Maple Tree): 리눅스 6.1부터 기존 레드-블랙 트리를 대체한 VMA 저장소입니다. 캐시 친화적인 B-tree 계열로 범위 검색과 삽입이 빠릅니다. - 8행
pgd: 이 프로세스의 페이지 테이블 루트입니다. x86-64에서는 CR3 레지스터에 로드됩니다.context_switch()시 이 값이 CR3에 쓰여 주소 공간이 전환됩니다. - 10행
mm_users는 이 주소 공간을 공유하는 스레드 수(스레드 그룹 크기)입니다.mm_count는 모든 종류의 참조를 포함하며 0이 되면mm_struct를 해제합니다. - 15행
page_table_lock은 PUD/PMD 레벨 테이블을 보호하는 스핀락입니다. PTE 레벨은 별도의 PTElock(페이지 구조체 embedded)을 사용하여 락 경합을 줄입니다. - 16행
mmap_lock은 VMA 트리 전체를 보호하는 읽기/쓰기 세마포어입니다.mmap(),munmap(),fork()등은 쓰기 락을, 페이지 폴트는 읽기 락을 잡습니다. 멀티스레드 환경에서 경합이 심한 핫스팟입니다. - 23~28행가상 메모리 사용량 통계 필드들입니다.
total_vm은/proc/pid/status의VmSize,locked_vm은VmLck,pinned_vm은VmPin에 대응합니다. - 34~44행프로세스 메모리 맵의 세그먼트 경계 주소들입니다.
/proc/pid/maps와/proc/pid/stat에서 확인할 수 있으며,brk()시스템 콜은brk필드를 늘려 힙을 확장합니다. - 52행
context는 아키텍처별로 다릅니다. x86-64에서는 PCID(Process Context ID)와 LDT 포인터를 담고, ARM64에서는 ASID(Address Space ID)를 담아 Context Switch 시 TLB 플러시를 최소화합니다. - 57행
notifier_subscriptions: MMU가 페이지 매핑을 변경할 때 KVM, HMM(Heterogeneous Memory Management), RDMA 등의 하위 시스템에 알림을 보내는 콜백 목록입니다. - 62행
__randomize_layout: 컴파일 타임에 구조체 필드 순서를 무작위화하는 KASLR 보안 기법을 적용합니다. 구조체 오프셋을 이용한 익스플로잇을 어렵게 만듭니다.
pte_t 및 관련 헬퍼 구조체 분석
/* arch/x86/include/asm/pgtable_types.h */
typedef struct { pteval_t pte; } pte_t; /* x86-64: 64비트 PTE 래퍼 */
/* 주요 PTE 비트 상수 */
#define _PAGE_PRESENT 0x001 /* bit 0: RAM에 존재 */
#define _PAGE_RW 0x002 /* bit 1: 쓰기 가능 */
#define _PAGE_USER 0x004 /* bit 2: 사용자 접근 가능 */
#define _PAGE_PWT 0x008 /* bit 3: Page Write-Through */
#define _PAGE_PCD 0x010 /* bit 4: Page Cache Disable */
#define _PAGE_ACCESSED 0x020 /* bit 5: 하드웨어 Accessed (LRU용) */
#define _PAGE_DIRTY 0x040 /* bit 6: 하드웨어 Dirty (writeback용) */
#define _PAGE_PSE 0x080 /* bit 7: 큰 페이지 (PMD=2MB, PUD=1GB) */
#define _PAGE_GLOBAL 0x100 /* bit 8: Context Switch 후에도 TLB 유지 */
#define _PAGE_SOFTW1 0x200 /* bit 9: 소프트웨어 정의 플래그 1 */
#define _PAGE_SOFTW2 0x400 /* bit 10: 소프트웨어 정의 플래그 2 */
#define _PAGE_SOFTW3 0x800 /* bit 11: 소프트웨어 정의 플래그 3 */
#define _PAGE_NX (1ULL << 63) /* bit 63: No-eXecute (DEP/W^X) */
/* 커널이 소프트웨어 비트를 사용하는 예 */
#define _PAGE_UFFD_WP _PAGE_SOFTW2 /* userfaultfd 쓰기 보호 마커 */
#define _PAGE_SAVED_DIRTY _PAGE_SOFTW3 /* 소프트-Dirty 트래킹 (/proc/pid/pagemap) */
/* COW에서 자주 쓰이는 헬퍼 */
static inline pte_t pte_mkwrite(pte_t pte)
{
return pte_set_flags(pte, _PAGE_RW); /* R/W 비트 설정 → 쓰기 가능 */
}
static inline pte_t pte_wrprotect(pte_t pte)
{
return pte_clear_flags(pte, _PAGE_RW); /* R/W 비트 해제 → COW 트리거 준비 */
}
static inline bool pte_write(pte_t pte)
{
return pte_flags(pte) & _PAGE_RW; /* 쓰기 가능 여부 검사 */
}
코드 설명
- 2행
pte_t는 단순히 64비트 정수를 감싸는 래퍼 구조체입니다. 타입 안전성을 위해unsigned long을 직접 쓰지 않고 구조체로 감쌉니다.pte_val(pte)로 원시 값을 꺼냅니다. - 4~16행PTE 비트 상수들입니다. 하드웨어가 직접 해석하는 비트(0~8)와 소프트웨어용 예약 비트(9~11, 52~62)가 구분됩니다.
_PAGE_ACCESSED와_PAGE_DIRTY는 하드웨어가 자동으로 설정하지만 커널도 소프트웨어적으로 조작합니다. - 19~20행커널이 소프트웨어 예약 비트를 실제로 활용하는 예입니다.
_PAGE_UFFD_WP는 userfaultfd write-protect 마커이고,_PAGE_SAVED_DIRTY는/proc/pid/pagemap의 soft-dirty 트래킹에 사용됩니다. - 23~31행
pte_mkwrite()와pte_wrprotect()는 COW의 핵심 조작입니다.fork()시에는pte_wrprotect()로 부모·자식 모두의 PTE에서_PAGE_RW를 제거하고, COW 폴트 처리 완료 후pte_mkwrite()로 복원합니다. - 33~35행
pte_write()는handle_pte_fault()에서 쓰기 폴트 시do_wp_page()호출 여부를 결정하는 핵심 검사입니다.!pte_write(entry)가 참이면 COW 경로로 진입합니다.
vm_fault 구조체 — 폴트 처리 컨텍스트
/* include/linux/mm_types.h */
struct vm_fault {
const struct vm_fault_flags flags; /* 폴트 플래그 (FAULT_FLAG_WRITE 등) */
pgoff_t pgoff; /* 파일 매핑 오프셋 (페이지 단위) */
unsigned long address; /* 폴트가 발생한 가상 주소 */
unsigned long real_address; /* 정렬 전 실제 접근 주소 */
struct vm_area_struct *vma; /* 폴트 발생 VMA */
pte_t *pte; /* 폴트 PTE 포인터 (락 이후 유효) */
pte_t orig_pte; /* 락 전 스냅샷 (경합 검사용) */
struct page *page; /* 폴트 처리에 사용할 struct page */
struct folio *folio; /* page가 속한 folio (6.x+) */
pmd_t *pmd; /* 상위 PMD 포인터 */
pud_t *pud; /* 상위 PUD 포인터 */
spinlock_t *ptl; /* PTE 레벨 스핀락 포인터 */
pgtable_t prealloc_pte; /* 사전 할당 PTE 테이블 */
};
코드 설명
- 3행
flags:FAULT_FLAG_WRITE(쓰기 폴트),FAULT_FLAG_USER(사용자 공간에서),FAULT_FLAG_REMOTE(다른 프로세스 mm),FAULT_FLAG_UNSHARE(KSM 비공유화) 등의 비트 플래그입니다. - 5행
address:PAGE_MASK로 정렬된 폴트 페이지의 시작 주소입니다. 실제 접근 주소(real_address)와 구별됩니다. - 8행
orig_pte: 스핀락 없이 읽은 PTE의 스냅샷입니다. 락 획득 후 현재 PTE와 비교하여 그 사이에 다른 CPU가 변경했는지 확인합니다(ABA 문제 방지). - 14행
ptl: PTElock 스핀락 포인터입니다. PTE 페이지 프레임에 임베드된 락으로,pte_lockptr()로 획득합니다.pte_unmap_unlock(vmf->pte, vmf->ptl)로 PTE 매핑 해제와 동시에 락을 풉니다. - 15행
prealloc_pte: 슬랩에서 미리 할당해둔 PTE 테이블입니다. 재귀적 락 획득 없이 PTE 테이블을 안전하게 추가할 수 있도록 폴트 핸들러 진입 전에 미리 준비합니다.
per-VMA Lock (커널 6.1+)
커널 6.1에서 도입된 per-VMA Lock은 페이지 폴트 처리의 핵심 병목인 mmap_lock 경합을 해소하는 기법입니다. 기존에는 모든 페이지 폴트가 mmap_read_lock()으로 전체 주소 공간을 직렬화했지만, per-VMA Lock은 개별 VMA 단위의 RCU 기반 읽기 락을 사용하여 병렬 폴트 처리를 가능하게 합니다.
mmap_lock 병목 문제
멀티스레드 프로세스에서 여러 스레드가 동시에 서로 다른 VMA 영역에서 페이지 폴트를 발생시키면, 모두 같은 mmap_read_lock()을 두고 경합합니다. 스레드 수가 늘어날수록 대기 시간이 급증하여 성능이 선형적으로 저하됩니다.
lock_vma_under_rcu() 동작 원리
lock_vma_under_rcu()는 mmap_lock 없이 VMA를 안전하게 잠그는 핵심 함수입니다. RCU 읽기 락으로 VMA 포인터를 안전하게 조회한 뒤, VMA 자체의 읽기 세마포어(vm_lock)를 획득합니다.
/* mm/memory.c — per-VMA Lock 기반 폴트 처리 경로 (커널 6.1+) */
/* struct vm_area_struct 내부의 per-VMA 락 (CONFIG_PER_VMA_LOCK=y) */
struct vm_area_struct {
/* ... 기존 필드 ... */
#ifdef CONFIG_PER_VMA_LOCK
int vm_lock_seq; /* 시퀀스 번호 (mmap_lock 일관성 검증) */
struct vma_lock *vm_lock; /* per-VMA 읽기/쓰기 세마포어 */
#endif
};
/* per-VMA Lock으로 VMA를 잠그는 함수 */
static inline struct vm_area_struct *
lock_vma_under_rcu(struct mm_struct *mm, unsigned long address)
{
struct vm_area_struct *vma;
/* ① RCU 읽기 락: VMA 트리 순회를 안전하게 */
rcu_read_lock();
vma = find_vma_rcu(mm, address); /* GC 보호 하에 VMA 탐색 */
if (!vma)
goto inval;
/* ② VMA 유효성 검증: 이 VMA가 아직 유효한가? */
if (!vma_is_accessible(vma))
goto inval;
/* ③ per-VMA 읽기 락 획득 */
if (vma_read_trylock(vma)) {
rcu_read_unlock();
/* ④ 시퀀스 번호로 mmap_lock과의 일관성 검증 */
if (unlikely(vma->vm_lock_seq != READ_ONCE(mm->mm_lock_seq))) {
vma_read_unlock(vma);
return NULL; /* mmap_lock 폴백 트리거 */
}
return vma; /* 성공: per-VMA Lock 보유 중 */
}
inval:
rcu_read_unlock();
return NULL; /* 실패: 호출자가 mmap_lock으로 재시도 */
}
/* 페이지 폴트 메인 경로: per-VMA Lock 우선 시도 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
unsigned long address,
unsigned int flags,
struct pt_regs *regs)
{
struct mm_struct *mm = vma->vm_mm;
vm_fault_t ret;
#ifdef CONFIG_PER_VMA_LOCK
if (!(flags & FAULT_FLAG_VMA_LOCK)) {
vma = lock_vma_under_rcu(mm, address);
if (vma) {
/* 빠른 경로: mmap_lock 없이 폴트 처리 */
ret = __handle_mm_fault(vma, address,
flags | FAULT_FLAG_VMA_LOCK);
vma_read_unlock(vma);
if (!(ret & VM_FAULT_RETRY))
return ret;
}
}
#endif
/* 느린 경로: mmap_lock 필요 (VMA 변경, 스택 확장 등) */
mmap_read_lock(mm);
ret = __handle_mm_fault(vma, address, flags);
mmap_read_unlock(mm);
return ret;
}
코드 설명
- ① RCU 읽기 락
rcu_read_lock()은 락이 아닌 선점 비활성화입니다. RCU 보호 아래 VMA 트리를 순회하여 포인터를 얻습니다. VMA 객체는 RCU 유예 기간(Grace Period)이 지날 때까지 메모리에서 해제되지 않으므로 안전합니다. - ③ vma_read_trylock()
vma_read_trylock()은vm_lock의 읽기 세마포어를 논블로킹으로 획득합니다. 실패하면 NULL을 반환하여 호출자가 mmap_lock으로 폴백합니다. 같은 VMA에 여러 스레드가 동시에 읽기 락을 획득할 수 있습니다. - ④ 시퀀스 번호 검증
vm_lock_seq와mm->mm_lock_seq를 비교합니다.mmap_write_lock()이mm_lock_seq를 증가시키므로, VMA가 수정되었다면 두 값이 달라집니다. 이 경우 mmap_lock으로 폴백하여 최신 VMA로 재시도합니다. - FAULT_FLAG_VMA_LOCK이 플래그가 설정된 폴트는 이미 per-VMA Lock 보호 하에 있습니다. 재귀 시도를 방지하고, mmap_lock 폴백이 필요한 경우(스택 확장, VMA 병합 등)를 구분하는 데 사용됩니다.
- mmap_lock 폴백per-VMA Lock이 실패하거나
VM_FAULT_RETRY가 반환되면 전통적인mmap_read_lock()경로로 폴백합니다. VMA 수정(mremap, mprotect), 스택 확장, anon_vma 초기화 등은 여전히 mmap_lock이 필요합니다.
성능 개선 데이터
per-VMA Lock은 서로 다른 VMA를 동시에 접근하는 멀티스레드 워크로드에서 가장 큰 효과를 발휘합니다.
| 워크로드 | 스레드 수 | 기존 (mmap_lock) | per-VMA Lock | 개선율 |
|---|---|---|---|---|
| 익명 페이지 폴트 (각 스레드 별도 영역) | 16 | 기준 | +2.5x | +150% |
| 파일 매핑 폴트 (각 스레드 별도 파일) | 16 | 기준 | +1.8x | +80% |
| 단일 스레드 | 1 | 기준 | 동일 | 0% |
| 같은 VMA 경합 | 16 | 기준 | +1.05x | +5% |
CONFIG_PER_VMA_LOCK 활성화 조건: 커널 6.1에서 도입되었으며 CONFIG_PER_VMA_LOCK=y로 활성화합니다. 비용은 struct vm_area_struct당 읽기/쓰기 세마포어 1개 추가(약 32바이트)와 vm_lock_seq(4바이트)입니다. 메모리 오버헤드가 작아 대부분의 배포판에서 기본 활성화됩니다. 동일 VMA를 여러 스레드가 경합하는 패턴(단일 공유 메모리 영역)에서는 개선 효과가 제한적입니다.
KPTI (Kernel Page Table Isolation)
KPTI는 2018년 공개된 Meltdown (CVE-2017-5754) 취약점에 대한 커널 완화 기법입니다. 사용자 모드에서 커널 메모리를 읽을 수 있는 하드웨어 취약점을 소프트웨어적으로 차단합니다.
Meltdown 문제
Meltdown 공격 원리:
1. 사용자 코드에서 커널 주소 읽기 시도
2. CPU는 권한 검사 전에 투기적 실행(speculative execution)
3. 투기적으로 읽은 커널 데이터가 캐시에 남음
4. 캐시 타이밍 사이드채널로 커널 데이터 유출
문제: PTE의 U/S 비트로 보호하지만, 투기적 실행이 권한 검사를 우회
원인: Intel CPU의 투기적 실행이 PTE 권한 검사보다 먼저 데이터를 가져옴
KPTI 구현
/* arch/x86/entry/entry_64.S - syscall 진입 시 KPTI 페이지 테이블 전환 */
SYM_CODE_START(entry_SYSCALL_64)
/* 1. 사용자 스택 → 커널 스택 전환 */
swapgs /* GS base를 커널용으로 교체 */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
/* 2. KPTI: 사용자 CR3 → 커널 CR3 */
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
/* ... syscall 처리 ... */
/* 3. 복귀 시: 커널 CR3 → 사용자 CR3 */
SWITCH_TO_USER_CR3_NOSTACK scratch_reg=%rdi
swapgs
sysretq
/* arch/x86/include/asm/pgtable.h - CR3 전환 매크로 */
/* 사용자/커널 CR3는 인접 페이지에 배치 (bit 12로 구분) */
#define PTI_USER_PGTABLE_BIT 12 /* 4096 바이트 오프셋 */
#define PTI_USER_PGTABLE_MASK (1 << PTI_USER_PGTABLE_BIT)
/* 커널 CR3 → 사용자 CR3: bit 12를 OR */
#define SWITCH_TO_USER_CR3_NOSTACK \
movq %cr3, scratch_reg; \
orq $PTI_USER_PGTABLE_MASK, scratch_reg; \
movq scratch_reg, %cr3
/* 사용자 CR3 → 커널 CR3: bit 12를 AND NOT */
#define SWITCH_TO_KERNEL_CR3 \
movq %cr3, scratch_reg; \
andq $~PTI_USER_PGTABLE_MASK, scratch_reg; \
movq scratch_reg, %cr3
코드 설명
- 3~4행KPTI의 핵심 아이디어입니다. 커널용 PGD와 사용자용 PGD를 물리적으로 인접한 2개의 4KB 페이지에 배치합니다.
PTI_USER_PGTABLE_BIT=12이므로, 사용자 PGD는 커널 PGD보다 정확히 4096바이트 뒤에 위치합니다. 소스:arch/x86/include/asm/pgtable.h - 7~10행
SWITCH_TO_USER_CR3_NOSTACK매크로는 커널→사용자 전환 시 CR3의 bit 12를 OR로 설정하여 사용자 PGD 주소로 변경합니다. 사용자 PGD에는 커널 매핑이 최소한만 포함(syscall/interrupt 진입점만)되어 Meltdown 공격을 차단합니다. - 13~16행
SWITCH_TO_KERNEL_CR3매크로는 사용자→커널 전환 시 bit 12를 AND NOT으로 제거하여 커널 PGD 주소를 복원합니다. 이 전환은entry_SYSCALL_64,idtentry등 모든 커널 진입 경로에서 수행됩니다. 단 하나의 비트 연산만 필요하므로 극히 빠릅니다.
KPTI + PCID 조합
KPTI는 사용자/커널 페이지 테이블을 분리하므로, PCID를 활용하면 각각의 TLB 엔트리를 보존할 수 있습니다.
/* arch/x86/mm/tlb.c - KPTI + PCID 조합 */
/*
* PCID 할당:
* - 커널 페이지 테이블: PCID = kern_pcid (짝수)
* - 사용자 페이지 테이블: PCID = user_pcid (kern_pcid | 1, 홀수)
*
* CR3 값:
* 커널 모드: PGD_phys | kern_pcid
* 사용자 모드: PGD_phys | PTI_USER_PGTABLE_MASK | user_pcid
*
* 효과:
* syscall 진입 시 CR3 교체하면서 PCID가 바뀌므로
* 사용자 TLB 엔트리는 보존됨 (커널 TLB도 보존됨)
* → syscall 복귀 시 사용자 TLB가 여전히 유효!
*/
static inline unsigned long build_cr3(pgd_t *pgd, u16 asid)
{
unsigned long cr3;
if (static_cpu_has(X86_FEATURE_PCID)) {
/* INVPCID_CMD_NOFLUSH bit로 TLB 보존 */
cr3 = __sme_pa(pgd) | asid | CR3_NOFLUSH;
} else {
cr3 = __sme_pa(pgd);
}
return cr3;
}
/* CR3_NOFLUSH (bit 63): CR3 변경 시 현재 PCID의 TLB를 플러시하지 않음 */
#define CR3_NOFLUSH BIT_ULL(63)
코드 설명
- 3~11행KPTI + PCID 조합의 핵심 설계입니다. 각 프로세스에 짝수(커널용)와 홀수(사용자용) 두 개의 PCID를 할당합니다. syscall 진입 시 CR3에서 PGD 주소와 PCID가 동시에 바뀌므로, 사용자 TLB 엔트리와 커널 TLB 엔트리가 각각의 PCID로 보존됩니다. 소스:
arch/x86/mm/tlb.c - 13~22행
build_cr3()는 PGD 물리 주소, ASID(PCID), 그리고CR3_NOFLUSH비트를 결합하여 CR3 값을 생성합니다.static_cpu_has(X86_FEATURE_PCID)는 컴파일 타임 최적화된 CPU 기능 검사로, PCID 미지원 CPU에서는 분기 비용이 0입니다. - CR3_NOFLUSH (bit 63)
CR3_NOFLUSH는 Intel이 PCID와 함께 도입한 최적화입니다. CR3에 이 비트를 설정하면, CR3 변경 시 현재 PCID의 TLB 엔트리를 플러시하지 않습니다. KPTI에서 사용자↔커널 전환마다 CR3를 바꿔야 하므로, 이 비트 없이는 매번 전체 TLB가 플러시되어 5~30% 성능 저하가 발생합니다.
KPTI 관련 부트 파라미터
| 파라미터 | 설명 | 보안 영향 |
|---|---|---|
pti=on | KPTI 강제 활성화 | 안전 |
pti=off (nopti) | KPTI 비활성화 | Meltdown 취약 |
pti=auto | CPU에 따라 자동 결정 (기본값) | 안전 |
nopcid | PCID 비활성화 (디버깅용) | KPTI 성능 저하 증가 |
nokaslr | KASLR 비활성화 (디버깅용) | 주소 예측 가능 |
Huge Pages와 TLB
Huge Pages는 4KB보다 큰 페이지(2MB, 1GB)를 사용하여 TLB 커버리지를 극대화하는 기법입니다. 리눅스는 두 가지 방식으로 Huge Pages를 지원합니다: 명시적 HugeTLB와 투명한 THP (Transparent Huge Pages).
PMD-mapped Huge Page 구조
HugeTLB vs THP 비교
| 항목 | HugeTLB | THP (Transparent Huge Pages) |
|---|---|---|
| 설정 방식 | 명시적 (hugetlbfs 마운트(Mount), mmap 플래그) | 투명 (커널 자동 관리) |
| 예약 | 부트 시 또는 sysctl로 예약 | 동적 할당 |
| 크기 | 2MB, 1GB (x86) | 2MB (PMD 레벨) |
| 스왑 지원 | 스왑 불가 | 스왑 가능 (분할 후) |
| OOM 위험 | 예약 기반으로 안전 | 할당 실패 시 4KB 폴백 |
| 관리 데몬 | 없음 | khugepaged (백그라운드 합체) |
| 분할/합체 | 없음 (고정) | split_huge_page / collapse |
| 사용 사례 | DB (Oracle, PostgreSQL), DPDK | 범용 워크로드 |
| 설정 | /proc/sys/vm/nr_hugepages | /sys/kernel/mm/transparent_hugepage/enabled |
| 메모리 낭비 | 내부 단편화(Fragmentation) 가능 | 적음 (필요 시 분할) |
THP 생명 주기
THP (Transparent Huge Page) 생명 주기:
1. 할당 (Allocation):
- 페이지 폴트 시 2MB compound page 할당 시도
- 실패하면 4KB 폴백 (defrag 설정에 따라 compaction 시도)
- defrag=always → 동기 compaction (지연 증가)
- defrag=defer+madvise → 비동기 compaction (권장)
2. 합체 (Collapse) - khugepaged:
- 백그라운드 스캔 (5초 간격, /sys/kernel/mm/.../scan_sleep_millisecs)
- 512개 연속 4KB PTE → 1개 2MB PMD 전환
- 물리 메모리 compaction 필요할 수 있음
3. 분할 (Split):
- mprotect()로 부분 권한 변경 시
- swap out 시 (개별 4KB 단위로 스왑)
- madvise(MADV_DONTNEED) 부분 적용 시
- 분할은 비용이 큼 (512개 PTE 설정 + TLB 무효화)
4. 해제 (Free):
- 일반 페이지와 동일하게 참조 카운트로 관리
- compound page 전체가 한번에 해제됨
khugepaged 동작
/* mm/khugepaged.c */
/* khugepaged: 백그라운드에서 4KB 페이지들을 2MB로 합체 */
static void khugepaged_scan_mm_slot(struct mm_slot *mm_slot)
{
struct mm_struct *mm = mm_slot->mm;
struct vm_area_struct *vma;
/* VMA를 순회하며 합체 후보 탐색 */
for (vma = mm->mmap; vma; vma = vma->vm_next) {
if (!hugepage_vma_check(vma))
continue;
/* 2MB 정렬된 범위 내의 512개 PTE가 모두 채워져 있는지 검사 */
if (khugepaged_scan_pmd(mm, vma, address, &result) == SCAN_SUCCEED) {
/* 합체: 512개 4KB 페이지 → 1개 2MB 페이지 */
collapse_huge_page(mm, address, &result);
}
}
}
/* 합체 조건:
* 1. 512개 연속 PTE가 모두 present
* 2. 모든 페이지가 동일 VMA에 속함
* 3. VMA 플래그가 huge page 허용
* 4. 2MB 물리 메모리 연속 할당 가능
* 5. refcount 조건 충족 (GUP pin 없음) */
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled로 설정하면 madvise(MADV_HUGEPAGE)를 호출한 영역만 THP를 적용합니다. 이는 예측 불가능한 지연(compaction)을 피하면서 선택적으로 THP를 활용하는 운영 환경에서 권장됩니다.
folio와 대형 페이지 TLB
커널 5.16부터 도입된 folio(폴리오)는 복합 페이지(compound page)를 추상화하는 새로운 자료 구조입니다. 기존 struct page의 head/tail 구조를 대체하여, 2MB THP와 같은 대형 페이지를 단일 개체로 표현합니다. 이는 TLB 관리와 페이지 테이블 처리의 효율을 크게 향상시킵니다.
folio vs 기존 compound page
기존 struct page 기반의 복합 페이지 구조에서는 2MB THP가 512개의 struct page로 구성되었습니다. head 페이지 하나와 511개의 tail 페이지로 이루어지며, tail 페이지의 refcount를 별도 추적해야 했습니다. folio는 이를 단일 개체로 통합합니다.
/* include/linux/mm_types.h — folio 핵심 API */
/* folio 크기 관련 */
static inline unsigned int folio_order(struct folio *folio);
static inline unsigned long folio_nr_pages(struct folio *folio);
static inline size_t folio_size(struct folio *folio);
/* folio 상태 확인 */
static inline bool folio_test_large(struct folio *folio);
static inline bool folio_test_hugetlb(struct folio *folio);
static inline bool folio_test_anon(struct folio *folio);
/* 사용 예: THP folio 처리 */
struct folio *folio = page_folio(page);
if (folio_test_large(folio)) {
unsigned int order = folio_order(folio);
unsigned long nr = folio_nr_pages(folio);
/* order=9 이면 2MB THP: PMD 단일 엔트리로 매핑 가능 */
if (order == PMD_ORDER)
set_pmd_at(mm, addr, pmdp, mk_large_pmd(folio));
}
mTHP: 멀티사이즈 THP (커널 6.8+)
커널 6.8에서 도입된 mTHP(multi-size THP)는 2MB 고정 크기 THP 외에도 16KB, 32KB, 64KB, 128KB, 256KB, 512KB 등 다양한 크기의 익명 folio를 지원합니다. TLB 관점에서는 ARM64의 Contiguous PTE와 결합되어 더욱 효율적인 TLB 활용이 가능합니다.
# mTHP 지원 크기 확인 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*kB/
# 64KB mTHP 활성화
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
# folio 통계 확인
grep -i thp /proc/vmstat | grep -i folio
| 구분 | 기존 compound page | folio (커널 5.16+) | mTHP folio (커널 6.8+) |
|---|---|---|---|
| 자료 구조 | 512개 struct page (head+tail) | 단일 struct folio | 단일 struct folio (가변 order) |
| refcount 관리 | head page만 유효 | folio 단위 단일 카운터 | folio 단위 단일 카운터 |
| TLB 엔트리 수 (2MB) | PMD 1개 또는 PTE 512개 | PMD 1개 (명시적) | 크기별 최적 엔트리 |
| TLB 무효화 범위 | folio 경계 불명확 | folio 크기 = 무효화 범위 | order 기반 정확한 범위 |
| ARM64 PTE_CONT 결합 | 제한적 | 지원 | 64KB folio + PTE_CONT 최적화 |
folio와 TLB 무효화: folio 기반 코드에서 TLB 무효화는 folio_size()로 정확한 범위를 계산하여 수행합니다. 기존 compound page에서는 hpage_nr_pages()로 유사하게 처리했지만, folio API가 더 명시적이고 안전합니다. folio_remove_rmap_range()와 같은 함수들이 folio 단위로 rmap과 TLB를 함께 처리합니다.
아키텍처별 TLB 비교
x86, ARM64, RISC-V, MIPS는 각각 다른 TLB 관리 방식을 사용합니다. 가장 큰 차이는 하드웨어 TLB refill과 소프트웨어 TLB refill입니다.
| 특성 | x86-64 | ARM64 | RISC-V | MIPS |
|---|---|---|---|---|
| TLB Refill | 하드웨어 (Page Walker) | 하드웨어 (Table Walk Unit) | 구현 의존 (보통 HW) | 소프트웨어 (TLB Miss 핸들러) |
| 페이지 테이블 포맷 | 4/5 단계 고정 | 4 단계 (granule 가변) | Sv39/Sv48/Sv57 | 2~4 단계 |
| ASID/PCID | PCID (12비트, 4096) | ASID (8/16비트) | ASID (9~16비트) | ASID (8비트) |
| TLB 무효화 | INVLPG, INVPCID, MOV CR3 | TLBI 명령 (IS/OS 범위) | SFENCE.VMA | TLBWI, TLBWR |
| 브로드캐스트 무효화 | IPI 기반 (소프트웨어) | TLBI IS (하드웨어 브로드캐스트) | 아직 미표준 | IPI 기반 |
| Huge Page 크기 | 2MB, 1GB | 64KB~1GB (granule 의존) | 2MB, 1GB (Sv39) | 구현 의존 |
| Global 비트 | PTE bit 8 (G) | PTE bit 11 (nG, 반전) | PTE bit 5 (G) | PTE Global 비트 |
| Dirty 비트 관리 | 하드웨어 자동 | HW (ARMv8.1) / SW (이전) | 하드웨어 (Svadu) | 소프트웨어 |
| 커널 주소 분리 | KPTI (소프트웨어) | TTBR0/TTBR1 (하드웨어) | 소프트웨어 | 소프트웨어 |
ARM64 TLBI IS (Inner Shareable): ARM64에서 TLB 무효화는 TLBI VAE1IS 같은 명령으로 하드웨어 수준에서 모든 코어에 브로드캐스트됩니다. x86의 IPI 기반 TLB Shootdown보다 훨씬 효율적입니다. 이것이 대규모 ARM 서버에서 TLB Shootdown 비용이 낮은 이유입니다.
RISC-V TLB 관리
/* arch/riscv/include/asm/tlbflush.h */
/* RISC-V TLB 무효화: SFENCE.VMA 명령 */
/* 전체 TLB 플러시 */
static inline void local_flush_tlb_all(void)
{
__asm__ __volatile__("sfence.vma" ::: "memory");
}
/* 특정 VA 무효화 */
static inline void local_flush_tlb_page(unsigned long addr)
{
__asm__ __volatile__("sfence.vma %0" : : "r" (addr) : "memory");
}
/* 특정 ASID 무효화 */
static inline void local_flush_tlb_all_asid(unsigned long asid)
{
__asm__ __volatile__("sfence.vma x0, %0"
: : "r" (asid) : "memory");
}
/* RISC-V 페이지 테이블 모드:
* Sv39: 39비트 VA, 3단계 (512GB)
* Sv48: 48비트 VA, 4단계 (256TB)
* Sv57: 57비트 VA, 5단계 (128PB)
*
* SATP 레지스터 (= x86 CR3):
* bits [63:60]: MODE (0=Bare, 8=Sv39, 9=Sv48, 10=Sv57)
* bits [59:44]: ASID (16비트)
* bits [43:0] : PPN (물리 페이지 번호)
*/
MIPS 소프트웨어 TLB refill
/* arch/mips/kernel/genex.S - MIPS TLB Miss 핸들러 */
/* TLB Miss 시 CPU가 예외를 발생시키고, 소프트웨어가 직접 TLB 채움 */
NESTED(except_vec0, 0, sp)
.set noat
/* BadVAddr에서 폴트 주소 읽기 */
mfc0 k0, CP0_BADVADDR
/* 페이지 테이블에서 PTE 조회 */
lw k1, pgd_current
srl k0, k0, 22 /* PGD 인덱스 */
sll k0, k0, 2
addu k1, k1, k0
lw k1, 0(k1) /* PGD 엔트리 */
/* ... PTE 조회 ... */
/* TLB에 직접 기록 */
mtc0 k0, CP0_ENTRYHI
mtc0 k1, CP0_ENTRYLO0
tlbwr /* TLB Write Random */
eret /* 예외 복귀 */
END(except_vec0)
AMD invlpgb (하드웨어 브로드캐스트 TLB 무효화)
기존 x86 아키텍처에서 멀티코어 TLB 무효화는 소프트웨어 IPI(Inter-Processor Interrupt) 기반의 TLB Shootdown에 의존했습니다. CPU 수가 증가할수록 IPI 오버헤드가 선형으로 증가하는 구조적 한계가 있었습니다. AMD Zen 4(Genoa, 2022)부터 도입된 INVLPGB(INValidate TLB Pages Broadcast) 명령어는 하드웨어가 직접 모든 CPU에 TLB 무효화를 브로드캐스트하여 이 문제를 해결합니다.
INVLPGB vs IPI 기반 TLB Shootdown 비교
INVLPGB 명령어 상세
INVLPGB는 기존 INVLPG의 브로드캐스트 버전입니다. 단일 명령으로 시스템 내 모든 논리 CPU의 TLB에서 지정된 범위를 무효화합니다. CPU 인터럽트를 발생시키지 않으므로 다른 CPU의 실행 흐름을 방해하지 않습니다.
/* arch/x86/include/asm/tlb.h (CONFIG_X86_INVLPGB) */
/*
* INVLPGB: 브로드캐스트 TLB 무효화
* - rax: 가상 주소 + 속성 비트
* - edx: PCID (상위 16비트) + 범위 길이 (하위 16비트)
* - ecx: 추가 제어 플래그
*/
static inline void invlpgb_flush_user_nr(unsigned long pcid,
unsigned long addr,
long nr, bool pmd_stride)
{
u64 rax = addr | INVLPGB_VA_VALID;
u32 edx = (pcid << 16) | (nr - 1);
u32 ecx = pmd_stride ? INVLPGB_STRIDE_2MB : 0;
asm volatile(
".byte 0x0f, 0x01, 0xfe\n\t" /* INVLPGB 인코딩 */
:
: "a"(rax), "c"(ecx), "d"(edx)
: "memory"
);
}
/* TLBSYNC: INVLPGB 완료 대기 배리어 */
static inline void tlbsync(void)
{
asm volatile(
".byte 0x0f, 0x01, 0xff\n\t" /* TLBSYNC 인코딩 */
::: "memory"
);
}
/* 커널 6.x 통합: arch_tlbbatch_flush()에서 INVLPGB 사용 */
void native_flush_tlb_multi(const struct cpumask *cpumask,
const struct flush_tlb_info *info)
{
if (cpu_feature_enabled(X86_FEATURE_INVLPGB) &&
info->end - info->start <= INVLPGB_MAX_RANGE) {
/* INVLPGB 사용: IPI 불필요 */
invlpgb_flush_user_nr(info->pcid,
info->start,
(info->end - info->start) >> PAGE_SHIFT,
false);
tlbsync();
} else {
/* 폴백: 기존 IPI 방식 */
smp_call_function_many(cpumask, flush_tlb_func,
(void *)info, 1);
}
}
| 항목 | IPI 기반 (기존 x86) | INVLPGB (AMD Zen 4+) | ARM64 TLBI IS |
|---|---|---|---|
| 무효화 방식 | 소프트웨어 IPI + INVLPG | 하드웨어 브로드캐스트 | 하드웨어 브로드캐스트 |
| CPU 인터럽트 발생 | O (각 CPU마다 인터럽트) | X (하드웨어 직접 처리) | X (하드웨어 직접 처리) |
| 레이턴시 (128코어) | ~수 마이크로초 | ~수십 나노초 | ~수십 나노초 |
| 확장성 | O(N) — 코어 수 비례 | O(1) — 코어 수 무관 | O(1) — 코어 수 무관 |
| 완료 배리어 | IPI ACK 대기 | TLBSYNC 명령 | DSB ISH |
| 커널 지원 | 항상 | CONFIG_X86_INVLPGB (6.x+) | 항상 (ARM64) |
| 성능 향상 (많은 코어) | 기준 | 최대 10배 향상 | 최대 10배 향상 |
CONFIG_X86_INVLPGB=y로 빌드된 커널이 필요합니다. CPUID 확인: CPUID[8000_0008h].EBX[bit3]. 현재 커널은 PCID와 함께 사용하는 경우에만 INVLPGB를 활성화합니다. /proc/cpuinfo에서 invlpgb 플래그를 확인할 수 있습니다.
PCID/ASID 구현
PCID(x86)와 ASID(ARM)는 Context Switch 시 TLB 플러시를 피하기 위한 핵심 기법입니다. 하지만 ID 공간이 제한적이므로(PCID 12비트=4096, ASID 8/16비트), 할당과 롤오버 관리가 중요합니다.
x86 PCID 할당 알고리즘
/* arch/x86/mm/tlb.c */
/*
* PCID 할당 전략:
* - 최대 6개 PCID만 사용 (TLB_NR_DYN_ASIDS)
* (4096개 전부 사용하면 관리 비용이 커짐)
* - per-CPU 단위로 PCID ↔ mm 매핑 관리
* - 세대(generation) 번호로 유효성 추적
*/
#define TLB_NR_DYN_ASIDS 6
/* per-CPU PCID 상태 */
DEFINE_PER_CPU(struct tlb_state, cpu_tlbstate) = {
.loaded_mm = &init_mm,
.next_asid = 1,
.cr4 = ~0UL,
};
struct tlb_state {
struct mm_struct *loaded_mm;
u16 loaded_mm_asid;
u16 next_asid;
u64 ctxs[TLB_NR_DYN_ASIDS]; /* 세대 번호 */
};
static void choose_new_asid(struct mm_struct *next,
u64 next_tlb_gen,
u16 *new_asid, bool *need_flush)
{
u16 asid;
/* 이미 할당된 ASID가 유효한지 검사 */
for (asid = 0; asid < TLB_NR_DYN_ASIDS; asid++) {
if (this_cpu_read(cpu_tlbstate.ctxs[asid]) ==
next->context.ctx_id) {
/* 기존 ASID 재사용 - TLB 플러시 불필요! */
*new_asid = asid;
*need_flush = false;
return;
}
}
/* 새 ASID 할당 (Round-Robin) */
asid = this_cpu_read(cpu_tlbstate.next_asid);
if (asid >= TLB_NR_DYN_ASIDS)
asid = 0;
*new_asid = asid;
*need_flush = true; /* 이전 mm의 TLB 엔트리 무효화 필요 */
this_cpu_write(cpu_tlbstate.ctxs[asid], next->context.ctx_id);
this_cpu_write(cpu_tlbstate.next_asid, asid + 1);
}
Lazy TLB Mode
/* kernel/sched/core.c - Context Switch에서의 Lazy TLB */
static inline void context_switch(struct rq *rq,
struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (!mm) {
/* 커널 스레드: mm이 없음 → Lazy TLB Mode */
/* 이전 프로세스의 mm을 빌려 사용 (active_mm) */
next->active_mm = oldmm;
mmgrab(oldmm); /* mm_count 증가 */
enter_lazy_tlb(oldmm, next);
/* CR3 변경하지 않음 = TLB 플러시 없음 */
} else {
/* 사용자 프로세스: 실제 mm 전환 */
switch_mm_irqs_off(oldmm, mm, next);
}
}
커널 주소 공간 레이아웃
x86-64에서 가상 주소 공간은 48비트(또는 57비트) 중 상위 반은 커널이, 하위 반은 사용자가 사용합니다. 커널 주소 공간의 각 영역은 특정 목적에 할당되어 있으며, MMU 설정에 직접 영향을 줍니다.
x86-64 가상 주소 맵 (48비트)
주소 범위별 용도
| 시작 주소 | 끝 주소 | 크기 | 용도 | 관련 함수/매크로 |
|---|---|---|---|---|
0x0000_0000_0000_0000 | 0x0000_7FFF_FFFF_FFFF | 128TB | 사용자 공간 | TASK_SIZE |
0xFFFF_8880_0000_0000 | 0xFFFF_C87F_FFFF_FFFF | 64TB | Direct Map (물리 메모리) | __va()/__pa(), PAGE_OFFSET |
0xFFFF_C900_0000_0000 | 0xFFFF_E8FF_FFFF_FFFF | 32TB | vmalloc/ioremap | vmalloc(), VMALLOC_START |
0xFFFF_EA00_0000_0000 | 0xFFFF_EAFF_FFFF_FFFF | 1TB | vmemmap (struct page) | VMEMMAP_START |
0xFFFF_FFFF_0000_0000 | 0xFFFF_FFFF_7FFF_FFFF | 2GB | 커널 모듈(Kernel Module) | MODULES_VADDR |
0xFFFF_FFFF_8000_0000 | 0xFFFF_FFFF_9FFF_FFFF | 512MB | 커널 텍스트 | __START_KERNEL_map |
0xFFFF_FFFF_FFE0_0000 | 0xFFFF_FFFF_FFFF_FFFF | 2MB | fixmap (고정 매핑) | FIXADDR_START |
/* arch/x86/include/asm/page_64.h */
/* Direct Map: 물리 주소 ↔ 가상 주소 변환 */
#define __PAGE_OFFSET_BASE_L4 (0xffff888000000000UL)
#define __PAGE_OFFSET_BASE_L5 (0xff11000000000000UL)
static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
unsigned long y = x - __PAGE_OFFSET;
/* KASLR offset 고려 */
return y;
}
#define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
#define __pa(x) __phys_addr((unsigned long)(x))
/* KASLR: 커널 텍스트 랜덤 오프셋 */
/* kaslr_offset = 0 ~ 1GB 범위의 랜덤 값 (2MB 정렬) */
KASLR (Kernel Address Space Layout Randomization)
KASLR은 부트마다 커널의 가상 주소 배치를 랜덤화하여, 공격자가 커널 함수/데이터의 주소를 예측하기 어렵게 만듭니다.
/* arch/x86/boot/compressed/kaslr.c */
/* KASLR 랜덤화 대상 3가지: */
/* 1. 커널 텍스트 (512MB 영역 내 2MB 정렬 랜덤 위치) */
/* 2. Direct Map (PAGE_OFFSET 랜덤 오프셋) */
/* 3. vmalloc/vmemmap (시작 주소 랜덤) */
unsigned long get_random_long(void)
{
/* 엔트로피 소스: RDRAND/RDSEED, 타임스탬프, UEFI 등 */
if (has_cpuflag(X86_FEATURE_RDRAND))
return rdrand_long();
return get_random_boot();
}
# KASLR 오프셋 확인 (디버깅용)
# /proc/kallsyms가 0 주소를 보여주면 권한 부족
sudo cat /proc/kallsyms | head -3
# ffffffff9a000000 T startup_64
# ffffffff9a000040 T secondary_startup_64
# 부트마다 주소가 달라짐 (KASLR 활성 시)
# KASLR 비활성화: 커널 파라미터 nokaslr
# Direct Map 오프셋 확인
sudo cat /proc/iomem | grep Kernel
# dmesg에서 KASLR 오프셋 확인
dmesg | grep -i kaslr
/proc/kallsyms, dmesg, 커널 포인터 출력 등을 통해 주소가 유출되면 KASLR의 보안 효과가 무력화됩니다. kptr_restrict=2, dmesg_restrict=1 설정으로 강화하세요.
TLB 관련 보안
2018년 이후 발견된 마이크로아키텍처 보안 취약점들은 TLB와 페이지 테이블 메커니즘에 직접적으로 관련됩니다. 커널은 다양한 완화 기법을 통해 이러한 하드웨어 취약점에 대응합니다.
취약점별 완화 기법
| 취약점 | CVE | 원인 | 커널 완화 | 성능 영향 |
|---|---|---|---|---|
| Meltdown | CVE-2017-5754 | 투기적 실행(Speculative Execution)이 U/S 비트 우회 | KPTI (페이지 테이블 격리(Isolation)) | 1~5% (PCID), 5~30% (no PCID) |
| Spectre v1 | CVE-2017-5753 | 경계 검사 우회 투기적 실행 | array_index_nospec, lfence | 미미 |
| Spectre v2 | CVE-2017-5715 | 간접 분기 주입 | retpoline, IBRS, eIBRS | 2~10% |
| L1TF | CVE-2018-3646 | L1D 캐시에서 비Present PTE의 PFN 투기적 접근 | PTE 반전, L1D 플러시 | KVM에서 10~30% |
| MDS | CVE-2018-12130 | 마이크로아키텍처 버퍼(Buffer) 데이터 유출 | VERW 명령, HT 비활성화 | 3~10% |
| iTLB Multihit | CVE-2018-12207 | ITLB에 2MB/4KB 동시 매핑 시 MCE | Huge Page 실행 제한 | 미미 |
| CET 우회 | 다수 | ROP/JOP 공격 | CET Shadow Stack, IBT | 미미 |
L1TF (L1 Terminal Fault) 상세
/* L1TF 완화: PTE의 PFN 비트를 반전하여 투기적 접근 방지 */
/* arch/x86/include/asm/pgtable.h */
static inline pte_t pte_set_flags(pte_t pte, pteval_t set)
{
pteval_t v = native_pte_val(pte);
return native_make_pte(v | set);
}
/*
* L1TF 문제: PTE의 Present 비트가 0이어도, CPU가 PFN 필드를
* 투기적으로 L1D 캐시에서 참조할 수 있음.
*
* 완화: Present=0인 PTE의 물리 주소 비트를 반전시켜
* 유효하지 않은 물리 주소를 가리키게 함.
*/
#define __pte_needs_invert(val) \
((val) && !((val) & _PAGE_PRESENT))
static inline u64 protnone_mask(u64 val)
{
return __pte_needs_invert(val) ? ~0ull : 0;
}
kvm-intel.vmentry_l1d_flush=always로 설정합니다.
Spectre v2와 BTB/RSB
Spectre v2는 분기 예측(Branch Prediction)기(Branch Target Buffer)를 오염시켜 투기적으로 공격자가 원하는 코드를 실행시키는 취약점입니다. TLB와 직접 관련은 없지만, 완화 기법이 성능에 큰 영향을 미칩니다.
주요 Spectre v2 완화 기법들
- Retpoline: 간접 분기를 ret 명령으로 대체: JMP *rax → CALL retpoline_rax_trampoline 컴파일러가 자동 적용 (CONFIG_RETPOLINE)
- IBRS (Indirect Branch Restricted Speculation): MSR IA32_SPEC_CTRL에 IBRS 비트 설정 커널 진입 시 활성화, 사용자 복귀 시 비활성화
- eIBRS (Enhanced IBRS) - 최신 CPU: 한 번 설정하면 커널/사용자 전환 시 자동 적용 Retpoline보다 성능 우수
- IBPB (Indirect Branch Predictor Barrier): Context Switch 시 분기 예측기 초기화 프로세스 간 BTB 오염 방지
- RSB (Return Stack Buffer) 채우기: Context Switch 시 RSB에 무해한 주소 채움 RSB underflow 공격 방지
SMEP / SMAP
SMEP(Supervisor Mode Execution Prevention)과 SMAP(Supervisor Mode Access Prevention)은 커널이 사용자 공간 코드를 실행하거나 데이터에 접근하는 것을 하드웨어적으로 차단합니다.
/* SMEP: 커널 모드에서 U/S=1인 페이지의 코드 실행 금지 */
/* CR4.SMEP=1이면, 커널이 사용자 코드 실행 시 #PF 발생 */
/* 효과: ret2usr 공격 방지 */
/* SMAP: 커널 모드에서 U/S=1인 페이지의 데이터 접근 금지 */
/* CR4.SMAP=1이면, copy_from_user() 등에서만 임시 해제 */
/* STAC (Set AC flag) / CLAC (Clear AC flag)으로 제어 */
static inline void stac(void)
{
/* AC=1: SMAP 임시 비활성화 (사용자 메모리 접근 허용) */
alternative("", __stringify(__ASM_STAC), X86_FEATURE_SMAP);
}
static inline void clac(void)
{
/* AC=0: SMAP 재활성화 */
alternative("", __stringify(__ASM_CLAC), X86_FEATURE_SMAP);
}
현재 시스템 완화 상태 확인
# 모든 취약점 완화 상태 확인
for f in /sys/devices/system/cpu/vulnerabilities/*; do
echo "$(basename $f): $(cat $f)"
done
# 출력 예:
# itlb_multihit: KVM: Mitigation: VMX disabled
# l1tf: Mitigation: PTE Inversion; VMX: flush not necessary
# mds: Mitigation: Clear CPU buffers; SMT vulnerable
# meltdown: Mitigation: PTI
# spec_store_bypass: Mitigation: Speculative Store Bypass disabled
# spectre_v1: Mitigation: usercopy/swapgs barriers
# spectre_v2: Mitigation: Retpolines, IBPB: conditional, STIBP
# KPTI 상태 확인
dmesg | grep -i "page table isolation"
# Kernel/User page tables isolation: enabled
# PCID 지원 확인
grep pcid /proc/cpuinfo | head -1
# flags : ... pcid ...
성능 분석 사례
실제 운영 환경에서 TLB 관련 성능 병목을 진단하고 해결하는 과정을 단계별로 살펴봅니다.
시나리오: 데이터베이스 지연 증가
문제 상황:
- PostgreSQL 서버에서 쿼리 지연이 불규칙적으로 2~3배 증가
- CPU 사용률은 정상 범위
- 메모리 여유 충분 (128GB 중 40GB 사용)
- iostat/iotop에서 디스크 I/O 문제 없음
가설: TLB 관련 성능 병목 의심
1단계: perf로 TLB 이벤트 측정
# TLB 관련 PMU 이벤트 측정 (30초)
perf stat -e dTLB-loads,dTLB-load-misses,\
dTLB-stores,dTLB-store-misses,\
iTLB-loads,iTLB-load-misses,\
L1-dcache-loads,L1-dcache-load-misses \
-p $(pgrep -x postgres | head -1) -- sleep 30
# 결과:
# 12,345,678,901 dTLB-loads
# 987,654,321 dTLB-load-misses # 8.0% of dTLB-loads ← 높음!
# 2,345,678,901 dTLB-stores
# 234,567,890 dTLB-store-misses # 10.0% ← 매우 높음!
# 123,456,789 iTLB-load-misses # 1.0% ← 정상
#
# 진단: dTLB miss rate 8~10%는 심각한 수준
# (정상: 1% 미만, 주의: 2~5%, 심각: 5%+)
2단계: 핫스팟 프로파일링(Profiling)
# dTLB miss가 가장 많이 발생하는 함수 찾기
perf record -e dTLB-load-misses -g \
-p $(pgrep -x postgres | head -1) -- sleep 10
perf report --stdio --sort=dso,symbol | head -30
# 출력 예:
# 42.3% postgres [kernel] [k] clear_page_rep
# 18.7% postgres postgres [.] hash_search_with_hash
# 12.1% postgres postgres [.] ExecScanFetch
# 8.4% postgres libc-2.31.so [.] __memmove_avx_unaligned
#
# 분석: clear_page_rep에서 42% → 새 페이지 할당/초기화가 빈번
# hash_search에서 18% → 해시 테이블이 큰 메모리 영역에 분산
3단계: BPF 기반 TLB 분석
# bpftrace로 TLB flush 이벤트 추적
bpftrace -e '
tracepoint:tlb:tlb_flush {
@flush_reason[args->reason] = count();
@flush_pages = hist(args->pages);
}
interval:s:10 {
print(@flush_reason);
print(@flush_pages);
clear(@flush_reason);
clear(@flush_pages);
}
'
# 출력 예:
# @flush_reason[3]: 15234 ← TLB_REMOTE_SHOOTDOWN (가장 많음!)
# @flush_reason[0]: 2341 ← TLB_FLUSH_ON_TASK_SWITCH
# @flush_reason[2]: 891 ← TLB_LOCAL_MM_SHOOTDOWN
#
# @flush_pages:
# [1] |@@@@@@@@@@@@@@ | 3456
# [2, 4) |@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | 7890
# [4, 8) |@@@@@@@@@@@@@@@@@@ | 4567
# [8, 16) |@@@@@@ | 1234
#
# 진단: Remote Shootdown이 지배적 → 다수 CPU에서 mmap 관련 변경이 빈번
4단계: 근본 원인 파악
# mmap/munmap 시스템 콜 빈도 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_mmap { @mmap = count(); }
tracepoint:syscalls:sys_enter_munmap { @munmap = count(); }
tracepoint:syscalls:sys_enter_mprotect { @mprotect = count(); }
tracepoint:syscalls:sys_enter_madvise { @madvise = count(); }
interval:s:5 {
print(@mmap); print(@munmap);
print(@mprotect); print(@madvise);
clear(@mmap); clear(@munmap);
clear(@mprotect); clear(@madvise);
}
'
# 결과: mmap/munmap이 5초당 수천 회 → jemalloc의 공격적 매핑/해제
# PostgreSQL의 work_mem 사용 시 임시 메모리 할당/해제가 빈번
5단계: 튜닝 적용
# 해결 방법 1: Transparent Huge Pages 활성화
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# 해결 방법 2: PostgreSQL huge_pages 설정
# postgresql.conf:
# huge_pages = try
# shared_buffers = 32GB
# 해결 방법 3: HugeTLB 예약
echo 16384 > /proc/sys/vm/nr_hugepages # 32GB를 2MB hugepages로
# 해결 방법 4: jemalloc retain 설정 (mmap/munmap 감소)
export MALLOC_CONF="retain:true,dirty_decay_ms:10000,muzzy_decay_ms:30000"
# 튜닝 결과 확인
perf stat -e dTLB-loads,dTLB-load-misses \
-p $(pgrep -x postgres | head -1) -- sleep 30
# 결과:
# 12,345,678,901 dTLB-loads
# 98,765,432 dTLB-load-misses # 0.8% ← 8% → 0.8% (10배 개선!)
튜닝 전후 비교
| 지표 | 튜닝 전 | 튜닝 후 | 개선율 |
|---|---|---|---|
| dTLB miss rate | 8.0% | 0.8% | 10배 감소 |
| TLB Shootdown/sec | ~3,000회 | ~200회 | 15배 감소 |
| P99 쿼리 지연 | 15ms | 5ms | 3배 개선 |
| TLB 커버리지 | 256KB (64 x 4KB) | 64MB (32 x 2MB) | 256배 증가 |
| Page Walk 비율 | 높음 | 매우 낮음 | - |
perf stat -e dTLB-load-misses로 반드시 TLB miss rate를 확인하세요. 2% 이상이면 Huge Pages 도입을 검토해야 합니다.
perf c2c로 False Sharing + TLB 복합 분석
# TLB miss와 캐시라인 False Sharing이 동시에 발생하면
# 성능이 급격히 저하됩니다.
# c2c (cache-to-cache) 프로파일링
perf c2c record -g -- ./workload
perf c2c report --stats
# TLB + 캐시 복합 이벤트
perf stat -e dTLB-load-misses,\
L1-dcache-load-misses,\
LLC-load-misses,\
cache-misses \
-- ./workload
# 조합 해석:
# TLB miss + L1 miss → 데이터가 핫하지만 분산되어 있음
# TLB miss + LLC miss → 심각한 메모리 접근 비효율
# TLB hit + LLC miss → NUMA remote 접근 의심
워크로드별 TLB 튜닝 요약
| 워크로드 유형 | 주요 TLB 병목 | 권장 설정 |
|---|---|---|
| OLTP 데이터베이스 | 대규모 shared_buffers의 TLB miss | HugeTLB 예약, shared_buffers에 2MB 페이지 |
| 인메모리 캐시 (Redis) | 해시 테이블(Hash Table) 랜덤 접근 | THP madvise, jemalloc retain |
| JVM 애플리케이션 | 힙이 큰 경우 TLB 부족 | -XX:+UseHugeTLBFS, THP |
| HPC / 과학 계산 | 대규모 배열 순차 접근 | 1GB HugeTLB 페이지 |
| 웹 서버 (많은 프로세스) | Context Switch 시 TLB 플러시 | PCID 확인, 프로세스 수 제한 |
| 컨테이너 (K8s) | cgroup 내 mmap 경합 | THP per-cgroup 설정, shootdown 모니터링 |
| DPDK / 네트워크 | 패킷(Packet) 버퍼 메모리 | 1GB HugeTLB, IOMMU hugepage |
고급 BPF 분석 스크립트
/* tlb_analysis.bpf.c - TLB 성능 분석용 BPF 프로그램 */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32); /* pid */
__type(value, u64); /* shootdown count */
} shootdown_count SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HISTOGRAM);
__uint(max_entries, 64);
__type(key, u64);
} shootdown_latency SEC(".maps");
SEC("tp/tlb/tlb_flush")
int trace_tlb_flush(struct trace_event_raw_tlb_flush *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count = bpf_map_lookup_elem(&shootdown_count, &pid);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
u64 init_val = 1;
bpf_map_update_elem(&shootdown_count, &pid, &init_val, BPF_ANY);
}
/* 플러시된 페이지 수 히스토그램 */
u64 pages = ctx->pages;
bpf_map_update_elem(&shootdown_latency, &pages, &pages, BPF_ANY);
return 0;
}
참고자료
커널 문서
- Page Tables — 커널 페이지 테이블 구조 문서입니다
- x86 TLB Flushing — x86 아키텍처 TLB 플러시 동작을 설명합니다
- Split Page Table Lock — 페이지 테이블 잠금 분할 기법을 다룹니다
LWN 기사
- Five-level page tables — 5단계 페이지 테이블 도입 배경과 구현을 설명합니다 (2017)
- Page table isolation / KPTI — 페이지 테이블 격리 기법의 초기 논의입니다 (2007)
- Reworking page-table traversal — 페이지 테이블 탐색 방식 개선을 다룹니다 (2019)
- Memory management notifiers — MMU 노티파이어 메커니즘을 설명합니다 (2010)
커널 소스 코드
- arch/x86/mm/tlb.c — x86 TLB 플러시 핵심 구현입니다
- mm/pgtable-generic.c — 아키텍처 독립적인 범용 페이지 테이블 코드입니다
- arch/x86/include/asm/pgtable_types.h — x86 페이지 테이블 타입 및 플래그 정의입니다