MMU & TLB
MMU (Memory Management Unit)와 TLB (Translation Lookaside Buffer)는 가상 메모리의 하드웨어 핵심입니다.
x86-64/ARM64 주소 변환, 4/5단계 페이지 테이블 워크, PTE 비트필드, TLB 내부 구조, mmu_gather 배칭,
페이지 폴트 핸들링, COW(Copy-on-Write), KPTI(Kernel Page Table Isolation),
Huge Pages/THP TLB 최적화, PCID/ASID 심화, 커널 주소 공간 레이아웃,
TLB 관련 보안 취약점과 완화 기법, 실전 성능 분석 사례까지 종합적으로 다룹니다.
핵심 요약
- MMU — 가상 주소를 물리 주소로 변환하는 하드웨어 유닛입니다.
- TLB — 최근 주소 변환을 캐시하여 페이지 테이블 워크를 생략합니다.
- 페이지 테이블 워크 — TLB Miss 시 4~5단계 메모리 접근이 필요합니다 (느림).
- TLB Shootdown — 페이지 테이블 변경 시 모든 CPU의 TLB를 무효화합니다 (비용 큼).
- ASID/PCID — Context Switch 시 TLB 플러시를 피하는 최적화 기법입니다.
단계별 이해
- 핵심 요소 확인
이 문서에서 다루는 자료구조/API를 먼저 정리합니다. - 처리 흐름 추적
요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다. - 문제 지점 점검
실패 경로, 경합 구간, 성능 병목을 체크합니다.
개요 (Overview)
MMU (Memory Management Unit)는 CPU 코어 내부에 위치하며, 모든 메모리 접근을 가로채어 가상 주소를 물리 주소로 변환합니다. 이 과정은 완전히 하드웨어에서 수행되며, 커널은 페이지 테이블을 설정하고 TLB를 관리하는 역할을 합니다.
MMU 핵심 구성 요소
| 구성 요소 | 역할 | 위치 | 관리 주체 |
|---|---|---|---|
| TLB | 최근 변환 캐시 | CPU 내부 (L1/L2) | 하드웨어 + 커널 |
| Page Walker | TLB Miss 시 페이지 테이블 탐색 | CPU 내부 (전용 회로) | 하드웨어 |
| 페이지 테이블 | VA→PA 매핑 정의 | RAM (per-process) | 커널 소프트웨어 |
| CR3/TTBR | 현재 페이지 테이블 베이스 주소 | CPU 레지스터 | 커널 (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라 합니다.
주요 MMU 관련 레지스터 (x86-64)
/* x86-64 MMU 관련 제어 레지스터 */
/* CR0: 페이징 활성화 */
/* bit 31 (PG): Paging Enable - 이 비트가 1이면 MMU 활성 */
/* bit 16 (WP): Write Protect - 커널도 RO 페이지 보호 (COW 필수) */
/* CR3: 페이지 테이블 베이스 */
/* bits [51:12]: PGD 물리 주소 (4KB 정렬) */
/* bits [11:0] : PCID (Process-Context Identifier) */
/* bit 63 : PCID 무효화 방지 (INVPCID_NOFLUSH) */
/* CR4: 페이징 확장 기능 */
/* bit 4 (PSE) : Page Size Extension (4MB pages) */
/* bit 5 (PAE) : Physical Address Extension */
/* bit 7 (PGE) : Page Global Enable */
/* bit 12 (LA57) : 5-Level Paging */
/* bit 17 (PCIDE): PCID Enable */
/* bit 20 (SMEP) : Supervisor Mode Execution Prevention */
/* bit 21 (SMAP) : Supervisor Mode Access Prevention */
/* bit 22 (PKE) : Protection Keys Enable */
/* bit 24 (PKS) : Protection Keys for Supervisor */
주소 변환 과정
가상 주소: 0x7ffff7a0e000
"num">1. CPU가 가상 주소로 메모리 접근 요청
"num">2. MMU가 TLB 확인
- TLB Hit → 즉시 물리 주소 반환 ("num">1 cycle)
- TLB Miss → 페이지 테이블 워크 (수십~수백 cycles)
"num">3. 물리 주소로 RAM 접근
물리 주소: 0x12345000
성능 차이: TLB Hit는 1 cycle, TLB Miss는 페이지 테이블 워크로 인해 200+ cycles 소요됩니다. TLB 적중률이 99%에서 99.9%로 향상되면 성능이 10% 이상 개선될 수 있습니다.
페이지 테이블 워크 (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 |
페이지가 RAM에 있음 |
| 1 | Read/Write |
쓰기 가능 여부 |
| 2 | User/Supervisor |
유저 모드 접근 가능 |
| 5 | Accessed |
읽기/쓰기 발생 |
| 6 | Dirty |
쓰기 발생 |
| 63 | NX (No Execute) |
실행 금지 |
커널 PTE 조작 매크로
/* 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);
페이지 테이블 메모리 관리
/* 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) | 지연시간 | 커버 범위 (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) — 이보다 크면 전체 플러시가 유리 */
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 최소화 — 시스템 콜 비용 + TLB Shootdown 비용 감소
- PCID 활성화 — Context Switch 시 TLB 플러시 방지
- 메모리 할당자 튜닝 — 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() 해제 */
플러시 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 활성화 (가장 효과적)
- 메모리 접근 패턴 개선 (배열 순차 접근, 구조체 패딩)
- 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 이상, 물리 메모리 부족
# 프로세스별 페이지 테이블 크기 확인
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 비용 감소"를 분리해서 접근해야 효과적입니다.
| 점검 항목 | 확인 방법 | 개선 방향 | 우선순위 |
|---|---|---|---|
| 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 오버헤드 | PCID 지원 여부, 워크로드 유형 | PCID 활성화 확인 | 낮음 |
| NUMA 지역성 | numastat, remote 접근 비율 | 로컬 노드 할당 정책 | 중간 |
| 메이저 폴트 | sar -B, pgmajfault | 메모리 추가 또는 스왑 튜닝 | 최고 |
# 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);
}
}
사용자 공간 주소 범위
/* 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 이중 베이스 레지스터로 사용자/커널 공간을 하드웨어 수준에서 분리한다는 점입니다.
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 */
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;
}
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 };
}
TLB 내부 구조 심화
TLB는 단순한 캐시가 아니라, 마이크로아키텍처 수준에서 정교하게 설계된 연관 메모리(Content-Addressable Memory, CAM)입니다. 프로세서마다 구조와 크기가 다르며, 성능에 직접적인 영향을 줍니다.
Set-Associative vs Fully-Associative
Split TLB (ITLB / DTLB)
최신 프로세서는 TLB를 명령어(ITLB)와 데이터(DTLB)로 분리합니다. 이는 CPU 파이프라인의 명령어 페치와 데이터 접근이 동시에 발생하므로, 각각 독립적인 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 보장 | 일부 임베디드 |
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을 보유해야 합니다. 배칭 도중 다른 스레드가 같은 VMA를 수정하면 use-after-free가 발생할 수 있습니다. 커널 6.1부터 maple tree 기반 VMA 관리로 이 문제가 완화되었습니다.
페이지 폴트 핸들링
페이지 폴트(Page Fault)는 MMU가 가상 주소를 물리 주소로 변환할 수 없을 때 발생하는 예외입니다. 리눅스 커널의 페이지 폴트 핸들러는 demand paging, COW, 스왑 인, 파일 매핑 등 가상 메모리의 핵심 기능을 구현합니다.
페이지 폴트 분류
| 종류 | 원인 | 처리 | 비용 |
|---|---|---|---|
| 마이너 폴트 (Minor) | 페이지가 메모리에 있지만 PTE 미설정 | PTE 설정만 (디스크 I/O 없음) | ~1-10 us |
| 메이저 폴트 (Major) | 페이지가 디스크에 있음 (스왑/파일) | 디스크에서 읽기 후 PTE 설정 | ~1-10 ms |
| COW 폴트 | 읽기전용 페이지에 쓰기 시도 | 페이지 복사 후 쓰기 가능으로 설정 | ~1-5 us |
| SEGV (비정상) | 유효하지 않은 주소 접근 | SIGSEGV 시그널 전송 | 프로세스 종료 |
| 커널 폴트 | 커널 코드의 잘못된 접근 | 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);
}
Speculative Page Fault: 커널 6.x에서 논의 중인 최적화입니다. mmap_lock을 잡지 않고 낙관적으로 폴트를 처리한 뒤, 실패하면 락을 잡고 재시도합니다. 멀티스레드 워크로드에서 mmap_lock 경합을 크게 줄여줍니다.
COW (Copy-on-Write)
fork() 시 부모와 자식 프로세스의 페이지 테이블은 같은 물리 페이지를 가리키되, PTE를 읽기 전용으로 표시합니다. 쓰기가 발생하면 페이지 폴트를 통해 복사가 이루어집니다. 이를 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;
}
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
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)
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 마운트, 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 |
| 메모리 낭비 | 내부 단편화 가능 | 적음 (필요 시 분할) |
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를 활용하는 운영 환경에서 권장됩니다.
아키텍처별 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)
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 | 커널 모듈 | 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 | 투기적 실행이 U/S 비트 우회 | KPTI (페이지 테이블 격리) | 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 | 마이크로아키텍처 버퍼 데이터 유출 | 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 Target Buffer)를 오염시켜 투기적으로 공격자가 원하는 코드를 실행시키는 취약점입니다. TLB와 직접 관련은 없지만, 완화 기법이 성능에 큰 영향을 미칩니다.
/* 주요 Spectre v2 완화 기법들 */
/* 1. Retpoline: 간접 분기를 ret 명령으로 대체 */
/* JMP *rax → CALL retpoline_rax_trampoline */
/* 컴파일러가 자동 적용 (CONFIG_RETPOLINE) */
/* 2. IBRS (Indirect Branch Restricted Speculation) */
/* MSR IA32_SPEC_CTRL에 IBRS 비트 설정 */
/* 커널 진입 시 활성화, 사용자 복귀 시 비활성화 */
/* 3. eIBRS (Enhanced IBRS) - 최신 CPU */
/* 한 번 설정하면 커널/사용자 전환 시 자동 적용 */
/* Retpoline보다 성능 우수 */
/* 4. IBPB (Indirect Branch Predictor Barrier) */
/* Context Switch 시 분기 예측기 초기화 */
/* 프로세스 간 BTB 오염 방지 */
/* 5. 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단계: 핫스팟 프로파일링
# 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) | 해시 테이블 랜덤 접근 | THP madvise, jemalloc retain |
| JVM 애플리케이션 | 힙이 큰 경우 TLB 부족 | -XX:+UseHugeTLBFS, THP |
| HPC / 과학 계산 | 대규모 배열 순차 접근 | 1GB HugeTLB 페이지 |
| 웹 서버 (많은 프로세스) | Context Switch 시 TLB 플러시 | PCID 확인, 프로세스 수 제한 |
| 컨테이너 (K8s) | cgroup 내 mmap 경합 | THP per-cgroup 설정, shootdown 모니터링 |
| DPDK / 네트워크 | 패킷 버퍼 메모리 | 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;
}