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 관련 보안 취약점과 완화 기법, 실전 성능 분석 사례까지 종합적으로 다룹니다.

일상 비유: TLB는 전화번호 즐겨찾기와 비슷합니다. 모든 번호를 전화번호부(페이지 테이블)에서 찾으면 느리지만, 자주 쓰는 번호는 즐겨찾기(TLB)에 저장하여 빠르게 찾습니다.

핵심 요약

  • MMU — 가상 주소를 물리 주소로 변환하는 하드웨어 유닛입니다.
  • TLB — 최근 주소 변환을 캐시하여 페이지 테이블 워크를 생략합니다.
  • 페이지 테이블 워크 — TLB Miss 시 4~5단계 메모리 접근이 필요합니다 (느림).
  • TLB Shootdown — 페이지 테이블 변경 시 모든 CPU의 TLB를 무효화합니다 (비용 큼).
  • ASID/PCID — Context Switch 시 TLB 플러시를 피하는 최적화 기법입니다.

단계별 이해

  1. 핵심 요소 확인
    이 문서에서 다루는 자료구조/API를 먼저 정리합니다.
  2. 처리 흐름 추적
    요청 시작부터 완료까지 실행 경로를 순서대로 확인합니다.
  3. 문제 지점 점검
    실패 경로, 경합 구간, 성능 병목을 체크합니다.

개요 (Overview)

MMU (Memory Management Unit)는 CPU 코어 내부에 위치하며, 모든 메모리 접근을 가로채어 가상 주소를 물리 주소로 변환합니다. 이 과정은 완전히 하드웨어에서 수행되며, 커널은 페이지 테이블을 설정하고 TLB를 관리하는 역할을 합니다.

MMU 핵심 구성 요소

구성 요소역할위치관리 주체
TLB최근 변환 캐시CPU 내부 (L1/L2)하드웨어 + 커널
Page WalkerTLB 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% 이상 개선될 수 있습니다.

CPU 가상 주소 접근 예: 0x7fff1234abcd MMU: TLB 조회 가상 주소로 TLB 탐색 Hit Miss 물리 주소 즉시 반환 TLB 캐시에서 직접 조회 약 1 cycle RAM 접근 완료 물리 주소로 데이터 읽기 페이지 테이블 워크 CR3→PGD→PUD→PMD→PTE 4회 RAM 접근 (200+ cycles) TLB 갱신 변환 결과를 TLB에 캐시 RAM 접근 완료 물리 주소로 데이터 읽기
그림 2: TLB Hit/Miss 처리 흐름 — Hit는 1 cycle, Miss는 200+ cycles

페이지 테이블 워크 (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 → 물리 주소
가상 주소 (48비트) PGD [47:39] PUD [38:30] PMD [29:21] PTE [20:12] Offset [11:0] 9비트 → 512 엔트리 9비트 → 512 엔트리 9비트 → 512 엔트리 9비트 → 512 엔트리 12비트 → 4KB 페이지 index index index index 결합 CR3 PGD 베이스 PGD → PUD 주소 pgd_t[512] PUD → PMD 주소 pud_t[512] PMD → PTE 주소 pmd_t[512] PTE Frame + Flags pte_t[512] 물리 페이지 Frame 주소 + Offset = 최종 물리 주소 struct page ① RAM 접근 ② RAM 접근 ③ RAM 접근 ④ RAM 접근 ⑤ 최종 접근 TLB Miss: 4회 RAM 접근 (①~④) + 최종 데이터 접근 (⑤) = 200+ cycles TLB Hit: TLB 캐시에서 직접 물리 주소 반환 → 1 cycle (①~④ 생략)
그림 1: x86-64 4단계 페이지 테이블 워크 (TLB Miss 경우)

페이지 테이블 엔트리 (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%)
 */
페이지 테이블 메모리 비용: 프로세스가 1TB 가상 메모리를 분산적으로 매핑하면 페이지 테이블만 2GB+ 메모리를 소비할 수 있습니다. /proc/[pid]/statusVmPTE 필드로 프로세스별 페이지 테이블 크기를 확인하세요. 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);
}
mprotect() / munmap() 페이지 테이블 변경 요청 CPU-0 (Initiator) ① 페이지 테이블 수정 ② 로컬 TLB 무효화 ③ IPI 전송 (전체 CPU) smp_call_function_many() flush_tlb_func_remote ④ 모든 응답 대기 (blocking) IPI 브로드캐스트 (Inter-Processor Interrupt) CPU-1 TLB 무효화 (INVLPG) CPU-2 TLB 무효화 (INVLPG) CPU-3 TLB 무효화 (INVLPG) ... CPU-N TLB 무효화 (INVLPG) 비용: 4 CPU ≈ 1 µs / 64 CPU ≈ 10 µs / 256 CPU ≈ 100 µs — CPU 수에 비례하여 증가
그림 3: TLB Shootdown — CPU-0이 IPI를 브로드캐스트하여 전체 CPU의 TLB를 동기화

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_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_allocTHP 폴트로 할당된 huge page 수-
thp_collapse_allockhugepaged가 합체한 횟수-
thp_split_pagehuge 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)

모범 사례

  1. Huge Pages 사용 — TLB 커버리지 512배 증가, 워크 비용 25% 감소
  2. 메모리 국소성 (Locality) — working set을 TLB 커버리지 이내로 유지
  3. mprotect/munmap 최소화시스템 콜 비용 + TLB Shootdown 비용 감소
  4. PCID 활성화Context Switch 시 TLB 플러시 방지
  5. 메모리 할당자 튜닝 — jemalloc/tcmalloc의 retain 옵션으로 mmap/munmap 빈도 감소
  6. MADV_HUGEPAGE — 선택적 THP 활성화로 핵심 데이터 구조에 Huge Pages 적용
  7. NUMA-aware 할당 — 로컬 노드 메모리 사용으로 TLB miss 후 워크 지연 감소
  8. 페이지 테이블 크기 모니터링/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 필수

해결:

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(@); }'

해결:

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

해결:

페이지 테이블 비대화

증상: 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

해결:

MMU/TLB 튜닝 체크리스트

MMU/TLB 성능은 접근 패턴, 페이지 크기, 매핑 변경 빈도에 크게 좌우됩니다. 튜닝은 "TLB miss 감소"와 "shootdown 비용 감소"를 분리해서 접근해야 효과적입니다.

점검 항목확인 방법개선 방향우선순위
TLB miss 비율perf stat dTLB-load-missesHugePage/THP 적용, locality 개선최고
shootdown 빈도perf + mprotect/munmap 패턴매핑 변경 배치 처리, 빈도 축소높음
context switch 영향PCID/ASID 활성 여부아키텍처 최적화 옵션 확인중간
페이지 테이블 크기grep VmPTE /proc/[pid]/statusHuge 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 Paging5-Level Paging (LA57)
가상 주소 비트48비트57비트
가상 주소 공간256 TB128 PB (페타바이트)
물리 주소 비트최대 52비트최대 52비트
페이지 테이블 단계PGD → PUD → PMD → PTEPGD → 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비트
57비트 가상 주소 (LA57) PGD [56:48] P4D [47:39] PUD [38:30] PMD [29:21] PTE [20:12] Offset [11:0] 9비트 9비트 (신규) 9비트 9비트 9비트 12비트 CR3 PGD base PGD → P4D pgd_t[512] P4D → PUD p4d_t[512] PUD → PMD pud_t[512] PMD → PTE pmd_t[512] PTE Frame+Flags pte_t[512] 물리 페이지 Frame + Offset = 최종 물리 주소 ① RAM ② RAM (신규) ③ RAM ④ RAM ⑤ RAM ⑥ 최종 5-Level: 5회 RAM 접근 (①~⑤) + 최종 데이터 접근 (⑥) = 4-Level 대비 ~25% 추가 지연 TLB Hit 시에는 단계 수와 무관하게 1 cycle로 변환 완료 P4D: 5단계 전용 4단계에서는 폴딩됨
그림 4: x86-64 5단계 페이지 테이블 워크 (LA57) - P4D 단계가 추가됨

커널 설정 및 감지

/* 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비트 호환 모드 선택 가능 */
주의: 5단계 페이징은 TLB Miss 시 워크 비용이 20~25% 증가합니다. 실제로 128PB 주소 공간이 필요하지 않다면, 4단계 페이징을 유지하는 것이 성능에 유리합니다. AWS의 대규모 인스턴스(12TB+ RAM)에서 주로 활용됩니다.

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 (커널)
x86와의 차이: x86-64는 CR3 하나로 사용자/커널을 모두 참조하므로 KPTI가 필요합니다. ARM64는 TTBR0/TTBR1이 하드웨어적으로 분리되어 있어 Meltdown 취약점에 영향을 받지 않습니다 (일부 Cortex-A75 등 예외 존재).

Granule 크기

ARM64는 세 가지 granule(페이지) 크기를 지원합니다. x86이 4KB 고정인 것과 대조적입니다.

Granule 크기페이지 테이블 단계가상 주소 비트엔트리 수Block (Huge) 크기사용 사례
4KB4단계 (L0~L3)48비트 (256TB)512 (9비트)2MB (L2), 1GB (L1)범용 서버, 데스크톱
16KB4단계 (L0~L3)47비트 (128TB)2048 (11비트)32MB (L2)Apple Silicon
64KB3단계 (L1~L3)48비트 (256TB)8192 (13비트)512MB (L2)HPC, 대규모 메모리
ARM64 주소 변환 (4KB Granule, 48-bit VA) bit[63] L0 [47:39] L1 [38:30] L2 [29:21] L3 [20:12] Offset [11:0] bit[63]=0? User Kernel TTBR0_EL1 User 페이지 테이블 TTBR1_EL1 Kernel 페이지 테이블 L0 Table → L1 base 512 entries L1 Table → L2 base 1GB Block L2 Table → L3 base 2MB Block L3 Table Page desc 4KB Page 물리 메모리 PA = Output + Offset = 최종 물리 주소 ASID (Address Space ID) TTBR0 상위 비트에 포함 (8/16비트) Context Switch 시: TTBR0만 교체 (커널 TTBR1은 유지) + ASID로 TLB 플러시 회피
그림 5: ARM64 주소 변환 흐름 - TTBR0/TTBR1 이중 베이스와 4단계 워크

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-64 PTE 64비트 레이아웃 NX bit 63 PKU [62:59] Prot. Keys SW [58:52] 소프트웨어용 물리 프레임 번호 (PFN) [51:12] 40비트 = 최대 4PB 물리 메모리 주소 지정 AVL [11:9] G 8 PAT 7 D 6 A 5 PCD 4 PWT 3 U/S 2 R/W 1 bit 0: Present (P) -- 페이지가 RAM에 존재하면 1, 스왑 아웃이면 0 (Page Fault 발생) 주요 비트 설명 NX (63): 실행 금지 (DEP/W^X 구현). 데이터 페이지에 설정하여 코드 실행 방지 PKU (62:59): Protection Keys. PKRU 레지스터로 사용자 공간에서 권한 제어 (4비트 = 16 그룹) G (8): Global. CR3 변경(Context Switch) 시에도 TLB에서 무효화되지 않음 (커널 매핑에 사용) PAT (7): Page Attribute Table. PWT/PCD와 조합하여 8가지 캐시 타입 선택 D (6), A (5): Dirty/Accessed. 하드웨어가 자동 설정. 페이지 회수(LRU)와 더티 페이지 기록에 사용 U/S (2): User/Supervisor. 0=커널 전용, 1=사용자 접근 가능. KPTI는 이 비트로 커널 페이지 격리 R/W (1): Read/Write. 0=읽기 전용, 1=쓰기 가능. COW는 이 비트를 0으로 설정하여 트리거 P (0): Present. 0이면 MMU가 Page Fault 생성. 나머지 63비트를 소프트웨어가 자유롭게 사용 (스왑 정보)
그림 6: x86-64 PTE 64비트 레이아웃 - 각 비트의 역할

x86 vs ARM64 PTE 비트 비교

기능x86-64ARM64비고
유효 여부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 레지스터 참조
Accessedbit 5 (A)bit 10 (AF)ARM은 하드웨어 자동 설정 옵션
Dirtybit 6 (D)bit 51 (DBM) + AP[2]ARMv8.1-TTHM 필요
Globalbit 8 (G)bit 11 (nG, 반전)ARM은 0=Global, 1=non-Global
실행 금지bit 63 (NX)bit 53/54 (PXN/UXN)ARM은 특권/사용자 분리
Protection Keysbit 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

4-Way Set-Associative TLB 구조 가상 주소 (VPN) Virtual Page Number Set Index 추출 (하위 비트) TLB Sets (예: 16 Sets) Set 0: Way 0 [VPN|PFN] Way 1 [VPN|PFN] Way 2 [VPN|PFN] Way 3 [VPN|PFN] Set 1: Way 0 [VPN|PFN] Way 1 [VPN|PFN] Way 2 [VPN|PFN] Way 3 [VPN|PFN] Set 15: Way 0 [VPN|PFN] Way 1 [VPN|PFN] Way 2 [VPN|PFN] Way 3 [VPN|PFN] 4-Way 병렬 태그 비교 (CAM) 선택된 Set의 모든 Way에서 VPN 태그를 동시에 비교 → Hit/Miss 판정 (1 cycle) VPN 하위 4비트 → Set 선택 각 엔트리 구성: VPN Tag + PFN + ASID + 권한 비트 + Valid 총 엔트리: 16 x 4 = 64
그림 7: 4-Way Set-Associative TLB - VPN으로 Set 선택 후 4개 Way를 병렬 비교

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, 1284-way, 6412-way, 2048FA, 32HW, 2 병렬
Intel Sapphire Rapids8-way, 1284-way, 9616-way, 2048FA, 32HW, 4 병렬
AMD Zen 4FA, 64FA, 728-way, 3072FA, 64HW, 2 병렬
ARM Cortex-A78FA, 32FA, 405-way, 1024FA, 32HW
ARM Neoverse V2FA, 48FA, 488-way, 2048FA, 48HW, 2 병렬
Apple M2 (Avalanche)FA, 192FA, 16012-way, 3072FA, 64HW
FA = Fully-Associative: 모든 엔트리에서 병렬 검색하므로 충돌 미스가 없습니다. 크기가 작은 L1 TLB에서 주로 사용합니다. N-way Set-Associative는 크기가 큰 L2 STLB에서 사용하여 면적/전력 효율을 높입니다.

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);
}
mmu_gather 배칭 흐름 tlb_gather_mmu() mmu_gather 초기화 범위/플래그 리셋 unmap_vmas() 루프 PTE 해제 + 페이지를 배치에 추가 tlb_remove_page() 반복 호출 배치 버퍼 pages[0..N] 축적 가득 차면 중간 플러시 배치 가득 차면 중간 플러시 tlb_flush_mmu_tlbonly() TLB Shootdown + 페이지 해제 free_pgtables() 페이지 테이블 구조체 해제 freed_tables = 1 설정 PGD/PUD/PMD/PTE 테이블 해제 tlb_finish_mmu() 최종 TLB Shootdown (IPI) + 배칭된 모든 페이지 free_pages_and_swap_cache() 배칭 효과 TLB Shootdown 횟수: 배칭 없이: N회 (페이지 수) 배칭 사용: 1~2회 = 수백~수천 배 절감 IPI 비용이 지배적
그림 8: mmu_gather 배칭 흐름 - 다수 페이지 해제를 하나의 TLB Shootdown으로 처리
/* 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 처리 흐름

페이지 폴트 처리 흐름 (x86-64) Page Fault 예외 (#PF) CR2 = 폴트 주소, error_code 전달 exc_page_fault() 커널 모드 폴트? Yes kernelmode_fixup fixup_exception() or Oops No find_vma() - VMA 탐색 VMA 찾음? No SIGSEGV bad_area() Yes access_error() - 권한 검사 handle_mm_fault() 실제 페이지 폴트 처리 진입점 __handle_mm_fault() - 테이블 워크 + PTE 할당 handle_pte_fault() - PTE 레벨 처리 Demand Paging do_anonymous_page() zero 페이지 또는 새 페이지 File Mapping do_fault() filemap_fault() COW Fault do_wp_page() write-protect 해제 Swap In do_swap_page() 스왑에서 읽어오기
그림 9: 페이지 폴트 처리 흐름 - do_page_fault에서 PTE 레벨까지의 경로

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 메커니즘 단계

1. fork() 전 부모 PTE R/W = 1 물리 페이지 refcount=1 2. fork() 직후 부모 PTE R/W = 0 자식 PTE R/W = 0 물리 페이지 refcount=2 3. 자식 쓰기 시도 자식 PTE R/W = 0 (쓰기 시도!) Page Fault! (#PF, write) do_wp_page() 호출 4. COW 복사 완료 부모 PTE R/W = 1 (복원) 원본 페이지 refcount=1 자식 PTE R/W = 1 (새 페이지) 복사본 refcount=1 COW 최적화 핵심 1. refcount == 1이면 복사 없이 바로 R/W 복원 (wp_page_reuse) - "reuse optimization" 2. KSM (Kernel Same-page Merging)으로 중복 페이지를 COW 공유하여 메모리 절약 3. fork() + exec() 패턴에서 실제 복사되는 페이지는 스택/데이터 극소수뿐 (나머지는 exec에서 해제) 4. GUP (get_user_pages)로 핀된 페이지는 COW 시 즉시 복사 필요 (CVE-2020-29661 취약점 관련) 5. Dirty COW (CVE-2016-5195): 경합 조건으로 읽기전용 매핑에 쓰기 가능한 커널 보안 취약점
그림 10: COW 메커니즘 - fork() 후 쓰기 시 페이지 폴트를 통한 복사

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 전 (취약) 단일 페이지 테이블 (CR3) 커널 주소 공간 커널 텍스트, 모듈, vmalloc direct map (전체 물리 메모리) U/S=0 (커널 전용) Meltdown: 투기적 실행으로 읽기 가능! 0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF 사용자 주소 공간 코드, 데이터, 스택, mmap U/S=1 (사용자 접근 가능) 0x0000_0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF KPTI 후 (안전) 사용자 모드 페이지 테이블 커널: 최소 매핑만 (entry/exit 트램폴린, IDT, TSS) 사용자 공간 전체 코드, 데이터, 스택, mmap 커널 모드 페이지 테이블 커널 전체 매핑 텍스트, 모듈, vmalloc, direct map 사용자 공간 전체 커널이 사용자 데이터 접근 시 필요 syscall/interrupt 진입/탈출 시 CR3 교체 (+ PCID로 TLB 유지)
그림 11: KPTI 전후 주소 공간 비교 - 사용자 모드에서 커널 매핑을 최소화

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 성능 비용: CR3 교체는 모든 syscall/interrupt에서 발생합니다. PCID가 없으면 매번 전체 TLB 플러시가 필요하여 5~30% 성능 저하가 발생합니다. PCID를 사용하면 TLB 플러시 없이 CR3만 교체하므로 성능 저하가 1~5%로 줄어듭니다.

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=onKPTI 강제 활성화안전
pti=off (nopti)KPTI 비활성화Meltdown 취약
pti=autoCPU에 따라 자동 결정 (기본값)안전
nopcidPCID 비활성화 (디버깅용)KPTI 성능 저하 증가
nokaslrKASLR 비활성화 (디버깅용)주소 예측 가능

Huge Pages와 TLB 심화

Huge Pages는 4KB보다 큰 페이지(2MB, 1GB)를 사용하여 TLB 커버리지를 극대화하는 기법입니다. 리눅스는 두 가지 방식으로 Huge Pages를 지원합니다: 명시적 HugeTLB와 투명한 THP (Transparent Huge Pages).

PMD-mapped Huge Page 구조

4KB 페이지 vs 2MB Huge Page (PMD-mapped) 4KB 페이지 경로 PGD PUD PMD PTE 4KB Page 512개 PTE 필요 4회 RAM 접근 2MB Huge Page 경로 PGD PUD PMD PTE 생략! 2MB Huge Page PMD 엔트리가 직접 지정 3회 RAM 접근 (1회 절약) TLB 커버리지 비교 4KB 페이지: TLB 64 엔트리 x 4KB = 256KB 커버 (L1 DTLB 기준) 2MB 페이지: TLB 32 엔트리 x 2MB = 64MB 커버 (256배 향상) 1GB 페이지: TLB 4 엔트리 x 1GB = 4GB 커버 (16384배 향상) 워크 비용: 4KB=4회, 2MB=3회, 1GB=2회 RAM 접근 (TLB Miss 시)
그림 12: PMD-mapped 2MB Huge Page - PTE 레벨을 건너뛰어 TLB 효율 극대화

HugeTLB vs THP 비교

항목HugeTLBTHP (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 없음) */
THP 튜닝: 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-64ARM64RISC-VMIPS
TLB Refill하드웨어 (Page Walker)하드웨어 (Table Walk Unit)구현 의존 (보통 HW)소프트웨어 (TLB Miss 핸들러)
페이지 테이블 포맷4/5 단계 고정4 단계 (granule 가변)Sv39/Sv48/Sv572~4 단계
ASID/PCIDPCID (12비트, 4096)ASID (8/16비트)ASID (9~16비트)ASID (8비트)
TLB 무효화INVLPG, INVPCID, MOV CR3TLBI 명령 (IS/OS 범위)SFENCE.VMATLBWI, TLBWR
브로드캐스트 무효화IPI 기반 (소프트웨어)TLBI IS (하드웨어 브로드캐스트)아직 미표준IPI 기반
Huge Page 크기2MB, 1GB64KB~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 (물리 페이지 번호)
 */
RISC-V 원격 TLB 무효화: 현재 RISC-V에는 ARM64의 TLBI IS와 같은 하드웨어 브로드캐스트가 표준화되어 있지 않습니다. 대부분의 구현에서 x86과 유사하게 IPI 기반 소프트웨어 shootdown을 사용합니다. 그러나 SiFive의 U740 등 일부 구현에서는 SFENCE.VMA가 코어간 동기화를 포함합니다.

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);
}
Per-CPU PCID 슬롯 관리 (6 슬롯) CPU-0 PCID 슬롯 PCID 0 → mm_A (gen=42) Active PCID 1 → mm_B (gen=40) Cached PCID 2 → mm_C (gen=38) Cached ... PCID 5 → mm_F (gen=30) Oldest 새 mm_G 진입 Context Switch 기존 PCID 있음? Yes 재사용 (flush 불필요) TLB 유지 = 성능 이득 No Oldest 슬롯 교체 PCID 5의 TLB 엔트리 무효화 mm_G → PCID 5 할당 Lazy TLB Mode 커널 스레드 실행 시 mm 전환 생략 → 불필요한 flush 방지
그림 13: Per-CPU PCID 슬롯 관리 - 6개 슬롯을 Round-Robin으로 재활용

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);
    }
}
Lazy TLB 최적화: 커널 스레드(kworker, kswapd 등)는 사용자 주소 공간을 사용하지 않으므로, Context Switch 시 CR3을 변경할 필요가 없습니다. 이전 프로세스의 페이지 테이블을 그대로 유지하여 불필요한 TLB 플러시를 방지합니다.

커널 주소 공간 레이아웃

x86-64에서 가상 주소 공간은 48비트(또는 57비트) 중 상위 반은 커널이, 하위 반은 사용자가 사용합니다. 커널 주소 공간의 각 영역은 특정 목적에 할당되어 있으며, MMU 설정에 직접 영향을 줍니다.

x86-64 가상 주소 맵 (48비트)

x86-64 가상 주소 공간 레이아웃 (48비트) 0x0000_0000_0000_0000 사용자 공간 (128 TB) 코드, 데이터, 힙, 스택, mmap, vDSO 0x0000_7FFF_FFFF_FFFF 비 Canonical 주소 영역 (접근 시 #GP) 0xFFFF_FFFF_FFFF_FFFF fixmap, vsyscall 0xFFFF_FFFF_8000_0000 커널 텍스트 매핑 (512MB) _text ~ _end, 커널 코드/데이터 0xFFFF_FFFF_0000_0000 모듈 영역 (2GB) 로드된 커널 모듈 (.ko) 0xFFFF_C900_0000_0000 KASAN shadow (디버그용) 0xFFFF_C900_0000_0000 vmalloc 영역 (32TB) vmalloc(), ioremap(), vmap() 0xFFFF_EA00_0000_0000 vmemmap (struct page 배열, 1TB) 0xFFFF_8880_0000_0000 Direct Map (64TB) 전체 물리 메모리의 선형 매핑 PAGE_OFFSET, __va()/__pa() 변환 guard holes + 예약 영역 KASLR: 부트마다 커널 텍스트/모듈/Direct Map 베이스 주소 랜덤화 User Kernel
그림 14: x86-64 가상 주소 공간 레이아웃 - 커널/사용자 영역 분할

주소 범위별 용도

시작 주소끝 주소크기용도관련 함수/매크로
0x0000_0000_0000_00000x0000_7FFF_FFFF_FFFF128TB사용자 공간TASK_SIZE
0xFFFF_8880_0000_00000xFFFF_C87F_FFFF_FFFF64TBDirect Map (물리 메모리)__va()/__pa(), PAGE_OFFSET
0xFFFF_C900_0000_00000xFFFF_E8FF_FFFF_FFFF32TBvmalloc/ioremapvmalloc(), VMALLOC_START
0xFFFF_EA00_0000_00000xFFFF_EAFF_FFFF_FFFF1TBvmemmap (struct page)VMEMMAP_START
0xFFFF_FFFF_0000_00000xFFFF_FFFF_7FFF_FFFF2GB커널 모듈MODULES_VADDR
0xFFFF_FFFF_8000_00000xFFFF_FFFF_9FFF_FFFF512MB커널 텍스트__START_KERNEL_map
0xFFFF_FFFF_FFE0_00000xFFFF_FFFF_FFFF_FFFF2MBfixmap (고정 매핑)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
보안 주의: KASLR은 커널 주소 유출(information leak)에 취약합니다. /proc/kallsyms, dmesg, 커널 포인터 출력 등을 통해 주소가 유출되면 KASLR의 보안 효과가 무력화됩니다. kptr_restrict=2, dmesg_restrict=1 설정으로 강화하세요.

TLB 관련 보안

2018년 이후 발견된 마이크로아키텍처 보안 취약점들은 TLB와 페이지 테이블 메커니즘에 직접적으로 관련됩니다. 커널은 다양한 완화 기법을 통해 이러한 하드웨어 취약점에 대응합니다.

취약점별 완화 기법

취약점CVE원인커널 완화성능 영향
MeltdownCVE-2017-5754투기적 실행이 U/S 비트 우회KPTI (페이지 테이블 격리)1~5% (PCID), 5~30% (no PCID)
Spectre v1CVE-2017-5753경계 검사 우회 투기적 실행array_index_nospec, lfence미미
Spectre v2CVE-2017-5715간접 분기 주입retpoline, IBRS, eIBRS2~10%
L1TFCVE-2018-3646L1D 캐시에서 비Present PTE의 PFN 투기적 접근PTE 반전, L1D 플러시KVM에서 10~30%
MDSCVE-2018-12130마이크로아키텍처 버퍼 데이터 유출VERW 명령, HT 비활성화3~10%
iTLB MultihitCVE-2018-12207ITLB에 2MB/4KB 동시 매핑 시 MCEHuge 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과 L1TF: 가상화 환경에서 L1TF는 게스트 VM이 호스트 또는 다른 VM의 L1D 캐시 데이터를 읽을 수 있는 심각한 취약점입니다. 완화를 위해 VM 전환 시 L1D 캐시를 플러시하며, 이로 인해 VM Exit 비용이 크게 증가합니다. 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 rate8.0%0.8%10배 감소
TLB Shootdown/sec~3,000회~200회15배 감소
P99 쿼리 지연15ms5ms3배 개선
TLB 커버리지256KB (64 x 4KB)64MB (32 x 2MB)256배 증가
Page Walk 비율높음매우 낮음-
실전 팁: TLB 문제는 CPU 사용률이나 I/O 지표에서는 보이지 않습니다. 성능이 불규칙적으로 저하되면 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 missHugeTLB 예약, 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;
}