MMU & TLB

MMU (Memory Management Unit)와 TLB (Translation Lookaside Buffer)는 가상 메모리(Virtual Memory)의 하드웨어 핵심입니다. x86-64/ARM64 주소 변환(Address Translation), 4/5단계 페이지 테이블(Page Table) 워크, PTE 비트필드, TLB 내부 구조, mmu_gather 배칭, 페이지 폴트(Page Fault) 핸들링, COW(Copy-on-Write), KPTI(Kernel Page Table Isolation), Huge Pages/THP TLB 최적화, PCID/ASID, 커널 주소 공간(Address Space) 레이아웃, TLB 관련 보안 취약점(Vulnerability)과 완화 기법, 실전 성능 분석 사례까지 종합적으로 다룹니다.

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

핵심 요약

  • MMU — 가상 주소(Virtual Address)를 물리 주소(Physical Address)로 변환하는 하드웨어 유닛입니다. x86-64에서는 CR3 레지스터가 페이지 테이블 베이스를 가리킵니다.
  • TLB — 최근 주소 변환을 캐시(Cache)하여 페이지 테이블 워크를 생략합니다. 일반적으로 L1 ITLB/DTLB + L2 STLB 계층으로 구성됩니다.
  • 페이지 테이블 워크 — TLB Miss 시 4~5단계 메모리 접근이 필요합니다. 커널은 pgd_offset()p4d_offset()pud_offset()pmd_offset()pte_offset_map() 순으로 워크합니다.
  • TLB Shootdown — 페이지 테이블 변경 시 flush_tlb_mm()/flush_tlb_range()로 모든 CPU의 TLB를 무효화합니다. IPI(Inter-Processor Interrupt) 기반으로 비용이 큽니다.
  • ASID/PCID — Context Switch 시 TLB 전체 플러시(Flush)를 피하는 최적화 기법입니다. x86-64는 PCID(12비트), ARM64는 ASID(8/16비트)를 사용합니다.
  • KPTI — Meltdown 대응으로 유저/커널 페이지 테이블을 분리하여 커널 매핑 노출을 차단합니다.
  • Huge Pages — 2MB/1GB 페이지로 TLB 커버리지를 극대화하여 TLB Miss를 줄입니다.
# MMU/TLB 상태 확인
cat /proc/cpuinfo | grep -E 'pse|pge|pcid|la57'  # MMU 관련 CPU 플래그
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses -- sleep 1  # TLB Miss 카운터
cat /proc/vmstat | grep -E 'nr_tlb|pgfault|thp'   # TLB/폴트 통계
getconf PAGE_SIZE                                   # 페이지 크기 (보통 4096)
설명

perf stat의 TLB Miss 카운터는 워크로드의 메모리 접근 패턴을 진단하는 핵심 지표입니다. dTLB-load-misses가 높으면 데이터 접근 지역성(Data Locality)이 낮거나 워킹셋이 TLB 용량을 초과하는 상태입니다. Huge Pages 적용이나 데이터 구조 최적화를 검토해야 합니다.

단계별 이해

  1. 핵심 요소 확인
    자료구조(pgd_t/p4d_t/pud_t/pmd_t/pte_t)와 하드웨어 레지스터(CR3/TTBR0/TTBR1)를 먼저 정리합니다.
  2. 주소 변환 흐름 추적
    CPU 메모리 접근 → TLB Lookup → (Hit) 물리 주소 반환 / (Miss) Page Walker가 CR3 기반으로 PGD → P4D → PUD → PMD → PTE 순서로 메모리를 읽어 변환을 완료합니다.
  3. 페이지 폴트 처리
    PTE가 absent이거나 권한 위반이면 #PF 예외 → do_page_fault()handle_mm_fault()__handle_mm_fault()에서 VMA를 찾아 적절한 핸들러(do_anonymous_page(), do_fault(), do_wp_page())를 호출합니다.
  4. TLB 관리
    페이지 테이블 변경 후 flush_tlb_range()/flush_tlb_page()로 stale 엔트리를 무효화합니다. mmu_gather 프레임워크가 배칭하여 IPI 횟수를 줄입니다.
  5. 성능 병목 점검
    perf stat의 dTLB/iTLB Miss 카운터, /proc/vmstatpgfault/pgmajfault, nr_tlb_remote_flush로 TLB Shootdown 빈도를 확인합니다.

개요 (Overview)

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

MMU 핵심 구성 요소

구성 요소역할위치관리 주체
TLB최근 변환 캐시CPU 내부 (L1/L2)하드웨어 + 커널
Page WalkerTLB Miss 시 페이지 테이블 탐색CPU 내부 (전용 회로)하드웨어
페이지 테이블VA→PA 매핑(Mapping) 정의RAM (per-process)커널 소프트웨어
CR3/TTBR현재 페이지 테이블 베이스 주소CPU 레지스터(Register)커널 (Context Switch)
PWC (Page Walk Cache)중간 테이블 엔트리 캐시CPU 내부하드웨어
ℹ️

Page Walk Cache (PWC): 최신 프로세서는 TLB 외에도 PGD/PUD/PMD 수준의 중간 테이블 엔트리를 캐시합니다. TLB Miss가 발생해도 PWC Hit가 되면 전체 4단계를 모두 걸을 필요 없이 2~3단계만 걸으면 됩니다. Intel에서는 이를 Paging Structure Cache라 합니다.

Page Walk Cache (PWC) 상세

Page Walk Cache(PWC)는 페이지 테이블 계층의 중간 단계 엔트리를 전용으로 캐시합니다. TLB가 최종 가상→물리 주소 변환 쌍 전체를 저장하는 반면, PWC는 PGD/PUD/PMD 각 단계의 테이블 포인터(물리 주소)를 독립적으로 캐시합니다. 이를 통해 TLB Miss 발생 시에도 페이지 테이블 워크(Page Table Walk)의 깊이를 줄여 메모리 접근 횟수를 감소시킵니다.

Intel 공식 명칭은 Paging Structure Cache이며, 세 가지 하위 캐시로 구성됩니다.

TLB 계층 구조와 PWC (Page Walk Cache) 히트 시나리오 가상 주소 접근 요청 VPN[4] VPN[3] VPN[2] VPN[1] + Offset L1 TLB (ITLB / DTLB) 완전 연관, 64~128 엔트리, VA→PA 전체 캐시 Hit (1 cycle) 물리 주소 반환 L1 캐시로 직행 Miss L2 TLB (Unified STLB) 4-way 연관, 1536~2048 엔트리, 8~10 cycles Hit (~8 cycles) 물리 주소 반환 L1 TLB 갱신 Miss Page Walk Cache (Paging Structure Cache) PMD Cache (PDE Cache) PTE 테이블 주소 캐시 Hit: PTE 1회 접근 (~40 cycles) PUD Cache (PDPTE Cache) PMD 테이블 주소 캐시 Hit: PMD+PTE 2회 접근 (~80 cycles) PGD Cache (PML4E Cache) PUD 테이블 주소 캐시 Hit: PUD+PMD+PTE 3회 (~120 cycles) Full Page Table Walk CR3→PGD→PUD→PMD→PTE (4회 RAM, 200+ cycles) 전체 Miss TLB 갱신 후 물리 주소 반환 L1/L2 TLB + PWC 모두 갱신 RAM 접근 완료
그림: TLB 계층(L1 DTLB → L2 STLB)과 PWC 히트 시나리오별 워크 깊이 비교
시나리오캐시 히트 위치남은 RAM 접근 횟수대략적 지연
TLB L1 HitL1 ITLB/DTLB0회~1 cycle
TLB L2 HitL2 STLB0회~8 cycles
PWC PMD HitPMD(PDE) 캐시1회 (PTE만)~40 cycles
PWC PUD HitPUD(PDPTE) 캐시2회 (PMD+PTE)~80 cycles
PWC PGD HitPGD(PML4E) 캐시3회 (PUD+PMD+PTE)~120 cycles
Full Miss없음 (CR3부터 워크)4회 (전 단계)200+ cycles

PWC는 같은 PGD/PUD/PMD를 공유하는 인접한 가상 주소에 매우 효과적입니다. 예를 들어 같은 2MB 영역 내에서 여러 4KB 페이지에 접근하면 PMD Cache 히트로 PTE 한 단계만 워크하게 됩니다. 반면 munmap() 또는 CR3 재로드(Context Switch) 시 PWC도 무효화됩니다. PCID 기능 사용 시에는 프로세스별로 PWC 엔트리가 태깅되어 Context Switch에서도 일부 PWC 엔트리를 보존할 수 있습니다.

/* 커널이 PWC와 상호작용하는 주요 시점 */

/* 1. Context Switch: CR3 재로드 → TLB + PWC 모두 무효화 (PCID 없는 경우) */
static inline void load_new_mm_cr3(pgd_t *pgdir, u16 new_asid, bool noflush)
{
    unsigned long new_cr3 = build_cr3(pgdir, new_asid);
    /* noflush=true(PCID 활성): TLB/PWC 엔트리 유지, 새 PCID 태깅 */
    /* noflush=false: CR3 로드 시 TLB + PWC 전체 플러시 */
    if (noflush)
        new_cr3 |= X86_CR3_PCID_NOFLUSH;
    write_cr3(new_cr3);
}

/* 2. 페이지 테이블 수정 후 TLB/PWC 무효화 */
/* pmd_clear() 등으로 중간 테이블을 변경하면 PWC도 stale 상태가 됨 */
/* invlpg 명령은 해당 VA의 TLB 엔트리만 제거하지만,            */
/* 상위 레벨 PWC 엔트리는 CPU가 자체적으로 무효화하거나         */
/* CR3 재로드로 완전 정리해야 함 (아키텍처 의존적)              */
flush_tlb_mm_range(mm, addr, end, PAGE_SHIFT, false);

/* 3. INVPCID로 특정 PCID의 TLB+PWC 선택적 무효화 (x86) */
static inline void invpcid_flush_one(unsigned long pcid, unsigned long addr)
{
    struct { u64 pcid, addr; } operand = { pcid, addr };
    asm("invpcid %0, %1" : : "m"(operand), "r"((unsigned long)0));
}

주요 MMU 관련 레지스터 (x86-64)

x86-64 MMU 관련 제어 레지스터

CR0: 페이징 활성화

비트필드설명
31PGPaging Enable - 이 비트가 1이면 MMU 활성
16WPWrite Protect - 커널도 RO 페이지 보호 (COW 필수)

CR3: 페이지 테이블 베이스

비트필드설명
[51:12]PGDPGD 물리 주소 (4KB 정렬)
[11:0]PCIDProcess-Context Identifier
63NOFLUSHPCID 무효화 방지 (INVPCID_NOFLUSH)

CR4: 페이징 확장 기능

비트필드설명
4PSEPage Size Extension (4MB pages)
5PAEPhysical Address Extension
7PGEPage Global Enable
12LA575-Level Paging
17PCIDEPCID Enable
20SMEPSupervisor Mode Execution Prevention
21SMAPSupervisor Mode Access Prevention
22PKEProtection Keys Enable
24PKSProtection Keys for Supervisor

주소 변환 과정

가상 주소: 0x7ffff7a0e000

1. CPU가 가상 주소로 메모리 접근 요청
2. MMU가 TLB 확인
   - TLB Hit → 즉시 물리 주소 반환 (1 cycle)
   - TLB Miss → 페이지 테이블 워크 (수십~수백 cycles)
3. 물리 주소로 RAM 접근

물리 주소: 0x12345000
ℹ️

성능 차이: TLB Hit는 1 cycle, TLB Miss는 페이지 테이블 워크로 인해 200+ cycles 소요됩니다. TLB 적중률이 99%에서 99.9%로 향상되면 성능이 10% 이상 개선될 수 있습니다.

CPU 파이프라인과 MMU 상호작용

현대 CPU는 명령어를 단계별로 처리하는 파이프라인(Pipeline) 구조를 사용합니다. MMU는 이 파이프라인의 메모리(Memory) 단계에 위치하여, 실행(Execute) 단계에서 계산된 유효 주소(Effective Address)를 물리 주소로 변환한 뒤 캐시 혹은 메모리에 접근합니다. TLB Hit 여부는 파이프라인 전체 처리량에 직접적인 영향을 줍니다.

비순서 실행(Out-of-Order Execution) 프로세서에서는 투기적 로드(Speculative Load)가 TLB에 접근하기도 합니다. 투기적으로 발행된 로드 명령이 TLB Miss를 유발하면 페이지 워커(Page Walker)가 활성화되고, 결과가 커밋(Commit)되지 않더라도 TLB에 변환 결과가 채워집니다. 이 동작은 Spectre 계열 사이드채널 공격의 원인이 되기도 합니다.

CPU 파이프라인 5단계와 MMU/TLB 위치 Fetch 명령어 인출 Decode 명령어 해석 Execute 유효 주소 계산 Memory MMU/TLB 변환 VA → PA Writeback 결과 레지스터 기록 TLB Miss: 파이프라인 스톨 Memory 단계 내부: MMU 처리 흐름 유효 주소 (EA) Execute 단계 출력 TLB 조회 VPN 태그 비교 (CAM) Hit 물리 주소 확보 (1 cycle) L1 D-Cache 접근 캐시/RAM 데이터 Writeback으로 전달 Miss 하드웨어 Page Walker 활성화 CR3→PGD→PUD→PMD→PTE (4회 RAM 접근, 파이프라인 지연) 비순서 실행(Out-of-Order)과 투기적 로드(Speculative Load) 투기적 로드 발행 ROB(재정렬 버퍼)에서 조기 실행 분기 예측 결과 미확정 상태 TLB Miss 발생 시 Page Walker 동작 → TLB 채움 커밋 안돼도 TLB는 갱신됨 Spectre 사이드채널 위험 투기적 접근 → 캐시 상태 변화 → 타이밍 측정으로 정보 유출 완화: IBRS, Retpoline, LFENCE 배리어
그림: CPU 파이프라인 5단계 중 Memory 단계에서 MMU/TLB가 동작하며, TLB Miss 시 파이프라인이 지연되고 투기적 로드는 Spectre 위험을 내포함

파이프라인 스톨(Pipeline Stall)을 최소화하기 위해 하드웨어는 메모리 접근 순서를 재정렬하거나, TLB Miss 중에도 독립적인 다른 명령어를 계속 실행합니다. 그러나 페이지 폴트(Page Fault)처럼 OS 개입이 필요한 예외는 파이프라인을 완전히 드레인(Drain)한 뒤 처리됩니다.

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 페이지(Page)가 RAM에 있음
1 Read/Write 쓰기 가능 여부
2 User/Supervisor 유저 모드 접근 가능
5 Accessed 읽기/쓰기 발생
6 Dirty 쓰기 발생
63 NX (No Execute) 실행 금지

커널 PTE 조작 매크로(Macro)

/* include/linux/pgtable.h - PTE 조작 API */

/* PTE 생성/읽기 */
pte_t mk_pte(struct page *page, pgprot_t pgprot);
pte_t pfn_pte(unsigned long pfn, pgprot_t pgprot);
unsigned long pte_pfn(pte_t pte);

/* PTE 플래그 검사 */
int pte_present(pte_t pte);    /* Present 비트 */
int pte_write(pte_t pte);      /* Read/Write 비트 */
int pte_dirty(pte_t pte);      /* Dirty 비트 */
int pte_young(pte_t pte);      /* Accessed 비트 */
int pte_exec(pte_t pte);       /* NX 비트 반전 */
int pte_huge(pte_t pte);       /* Huge Page 비트 */

/* PTE 플래그 설정 */
pte_t pte_mkwrite(pte_t pte);    /* R/W = 1 */
pte_t pte_mkdirty(pte_t pte);    /* Dirty = 1 */
pte_t pte_mkyoung(pte_t pte);    /* Accessed = 1 */
pte_t pte_wrprotect(pte_t pte);  /* R/W = 0 (COW에 사용) */
pte_t pte_mkold(pte_t pte);      /* Accessed = 0 (LRU에 사용) */
pte_t pte_mkclean(pte_t pte);    /* Dirty = 0 */
pte_t pte_mkexec(pte_t pte);     /* NX = 0 (실행 허용) */

/* PTE를 테이블에 기록 (배리어 포함) */
void set_pte_at(struct mm_struct *mm, unsigned long addr,
                pte_t *ptep, pte_t pte);

/* Atomic PTE 교체 (COW, 페이지 마이그레이션) */
pte_t ptep_get_and_clear(struct mm_struct *mm,
                          unsigned long addr, pte_t *ptep);
코드 설명
  • 3~5행mk_pte()struct page와 보호 플래그(pgprot_t)를 결합하여 PTE 값을 생성합니다. pfn_pte()는 PFN(Page Frame Number)에서 직접 PTE를 만들며, pte_pfn()은 역으로 PTE에서 물리 프레임 번호를 추출합니다. 소스: include/linux/pgtable.h
  • 8~13행PTE 플래그 검사 함수들입니다. pte_present()는 페이지가 RAM에 존재하는지, pte_young()은 최근 접근 여부(LRU 페이지 회수에 핵심), pte_dirty()는 수정 여부(writeback 판단)를 확인합니다. 이들은 하드웨어 비트를 직접 검사하는 인라인 함수입니다.
  • 16~22행PTE 플래그 설정 함수들입니다. pte_wrprotect()fork() 시 COW를 준비하기 위해 쓰기 권한을 제거하고, pte_mkold()는 LRU 에이징(aging)에서 Accessed 비트를 초기화하여 활성 페이지를 판별합니다. 각 함수는 원본 PTE를 수정하지 않고 새 PTE 값을 반환합니다.
  • 25~26행set_pte_at()은 PTE를 페이지 테이블에 기록하며, 아키텍처별 메모리 배리어(barrier)를 포함합니다. ARM64에서는 dsb(ishst) 배리어가 필요하고, x86에서는 store ordering이 보장되므로 단순 대입입니다.
  • 29~30행ptep_get_and_clear()는 PTE를 원자적으로 읽고 0으로 클리어합니다. COW 폴트 처리(do_wp_page())와 페이지 마이그레이션에서 기존 PTE를 안전하게 교체할 때 사용됩니다. x86에서는 xchg 명령어로 구현됩니다.

페이지 테이블 메모리 관리(Memory Management)

/* mm/memory.c - 페이지 테이블 할당 */

/* 각 테이블 레벨은 4KB 페이지 하나를 차지 (512 x 8바이트 = 4096) */

/* PGD: mm_struct 생성 시 할당 */
pgd_t *pgd_alloc(struct mm_struct *mm)
{
    pgd_t *pgd = (pgd_t *)__get_free_page(GFP_PGTABLE_USER);
    if (pgd)
        pgd_ctor(mm, pgd);  /* 커널 매핑 복사 */
    return pgd;
}

/* PTE 테이블: page fault 시 lazy 할당 */
pte_t *pte_alloc_one_kernel(struct mm_struct *mm)
{
    return (pte_t *)__get_free_page(GFP_PGTABLE_KERNEL | __GFP_ZERO);
}

/* 페이지 테이블 메모리 오버헤드:
 * 프로세스당 1GB 매핑 시 (4KB 페이지):
 *   PGD: 1 페이지 (4KB)
 *   PUD: 1 페이지 (4KB)     — 1GB는 PUD 1개
 *   PMD: 1 페이지 (4KB)     — 512 PMD 엔트리
 *   PTE: 512 페이지 (2MB)   — 256K PTE 엔트리
 *   총: ~2MB (매핑 메모리의 0.2%)
 */
페이지 테이블 메모리 비용: 프로세스(Process)가 1TB 가상 메모리를 분산적으로 매핑하면 페이지 테이블만 2GB+ 메모리를 소비할 수 있습니다. /proc/[pid]/statusVmPTE 필드로 프로세스별 페이지 테이블 크기를 확인하세요. Huge Pages는 PTE 레벨을 제거하여 이 비용도 절감합니다.

TLB (Translation Lookaside Buffer)

TLB 아키텍처

레벨 크기 (x86) 지연(Latency)시간 커버 범위 (4KB)
L1 DTLB 64 엔트리 1 cycle 256KB
L1 ITLB 128 엔트리 1 cycle 512KB
L2 STLB 1536 엔트리 7 cycles 6MB
💡

Huge Pages 효과: 2MB Huge Page 사용 시 하나의 TLB 엔트리가 512배 많은 메모리를 커버합니다. 64 엔트리 TLB로 128MB → 64GB를 커버할 수 있습니다!

TLB 관리 명령 (x86)

/* arch/x86/include/asm/tlbflush.h */

/* 전체 TLB 플러시 */
static inline void __native_flush_tlb(void)
{
    unsigned long cr3;
    cr3 = __read_cr3();
    __write_cr3(cr3);  /* CR3 재설정 = 전체 플러시 */
}

/* 단일 페이지 무효화 */
static inline void __flush_tlb_one(unsigned long addr)
{
    asm volatile("invlpg (%0)" :: "r" (addr) : "memory");
}

/* PCID 사용 시 선택적 플러시 */
static inline void __flush_tlb_one_user(unsigned long addr)
{
    asm volatile("invpcid (%0), %1" :: "r"(&desc), "r"(type));
}

TLB Shootdown

문제: 페이지 테이블 변경 시 모든 CPU의 TLB를 무효화해야 함

/* mm/memory.c - mprotect 등에서 호출 */
void flush_tlb_mm_range(struct mm_struct *mm,
                         unsigned long start,
                         unsigned long end,
                         unsigned int stride_shift)
{
    /* 1. 로컬 CPU TLB 플러시 */
    __flush_tlb_mm_range(mm, start, end, stride_shift);

    /* 2. 다른 CPU들에게 IPI 전송 */
    smp_call_function_many(mm_cpumask(mm),
                             flush_tlb_func_remote, &info, 1);
}
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) — 이보다 크면 전체 플러시가 유리 */
코드 설명
  • 3행flush_tlb_func_remote()는 IPI를 수신한 원격 CPU에서 실행되는 콜백 함수입니다. struct flush_tlb_info에는 대상 mm, 시작/끝 주소, stride 정보가 담겨 있습니다. 소스: arch/x86/mm/tlb.c
  • 6~7행첫 번째 최적화로, IPI를 받은 CPU가 실제로 해당 mm을 사용 중인지 cpu_tlbstate.loaded_mm과 비교합니다. Lazy TLB 모드의 CPU(커널 스레드 실행 중)는 이 검사에서 조기 반환하여 불필요한 플러시를 피합니다.
  • 10~14행두 번째 최적화로, 무효화 범위가 TLB_FLUSH_THRESHOLD(약 33페이지, 132KB)보다 작으면 INVLPG를 페이지 단위로 반복 실행합니다. INVLPG 한 번의 비용은 약 100ns이므로, 33번 이상이면 CR3 재로드(전체 플러시)가 더 효율적입니다.
  • 15~17행범위가 임계치를 초과하면 __flush_tlb_global()로 전체 TLB를 플러시합니다. PCID가 활성화된 경우 현재 PCID의 엔트리만 무효화하므로, 다른 프로세스의 TLB 엔트리는 보존됩니다.
mm_cpumask 최적화: mm_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 최소화시스템 콜(System Call) 비용 + TLB Shootdown 비용 감소
  4. PCID 활성화Context Switch 시 TLB 플러시 방지
  5. 메모리 할당자(Memory Allocator) 튜닝 — 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() 해제 */
코드 설명
  • flush_tlb_all()모든 CPU의 모든 TLB 엔트리를 무효화합니다. vmalloc()/vfree() 등 커널 전역 매핑 변경 시 사용되며, 비용이 가장 높으므로 최후의 수단입니다. x86에서는 모든 CPU에 IPI를 전송하고, ARM64에서는 TLBI VMALLE1IS 하나로 브로드캐스트합니다. 소스: include/asm-generic/tlbflush.h
  • flush_tlb_mm()특정 mm_struct를 사용하는 CPU에만 TLB 플러시를 수행합니다. exec()에서 프로세스의 전체 주소 공간을 교체할 때 적합합니다. mm_cpumask(mm)로 IPI 대상 CPU를 제한하므로 flush_tlb_all()보다 효율적입니다.
  • flush_tlb_range()특정 VMA의 주소 범위만 무효화합니다. mprotect()mremap()에서 권한/위치가 변경된 범위에 사용됩니다. 내부적으로 범위 크기에 따라 INVLPG 반복 또는 전체 플러시를 선택합니다.
  • flush_tlb_page()단일 페이지의 TLB 엔트리만 무효화하며, 비용이 가장 낮습니다. COW 폴트(do_wp_page())나 개별 PTE 변경 시 사용됩니다. x86에서는 INVLPG 1회로 구현됩니다.
  • flush_tlb_kernel_range()커널 가상 주소(vmalloc 영역 등) 범위를 플러시합니다. 커널 매핑은 모든 프로세스가 공유하므로, 모든 CPU 대상으로 실행됩니다. vunmap(), ioremap() 해제 시 호출됩니다.

플러시 API 선택 가이드

시나리오적합한 API비고
단일 PTE 변경 (COW, page fault)flush_tlb_page()INVLPG 1회
VMA 권한 변경 (mprotect)flush_tlb_range()범위 크기에 따라 INVLPG 또는 전체 플러시
프로세스 전체 매핑 교체 (exec)flush_tlb_mm()CR3 재로드
커널 매핑 변경 (vmalloc/vfree)flush_tlb_kernel_range()모든 CPU 대상
대규모 해제 (munmap + free_pgtables)mmu_gather 배칭자동 최적화
커널 초기화, 드라이버 매핑flush_tlb_all()최후의 수단

일반적인 문제

높은 TLB Miss Rate

증상: perf에서 dTLB-load-misses > 5%

진단:

# TLB miss rate 측정
perf stat -e dTLB-loads,dTLB-load-misses -p $(pgrep myapp) -- sleep 10

# miss가 발생하는 함수 확인
perf record -e dTLB-load-misses -g -p $(pgrep myapp) -- sleep 10
perf report --sort=symbol

# working set 크기 추정
perf stat -e dTLB-load-misses,dtlb_load_misses.stlb_hit \
    -p $(pgrep myapp) -- sleep 10
# STLB hit가 높으면 L1 TLB만 부족 → 적당한 Huge Pages로 해결
# STLB miss도 높으면 working set이 매우 큼 → HugeTLB 필수

해결:

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 이상, 물리 메모리(Physical Memory) 부족

# 프로세스별 페이지 테이블 크기 확인
for p in /proc/[0-9]*/status; do
    name=$(grep Name "$p" 2>/dev/null | awk '{print $2}')
    pte=$(grep VmPTE "$p" 2>/dev/null | awk '{print $2}')
    [ -n "$pte" ] && [ "$pte" -gt 10000 ] && echo "$pte kB - $name"
done | sort -n -r | head -10

해결:

MMU/TLB 튜닝 체크리스트

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

점검 항목확인 방법개선 방향우선순위(Priority)
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 오버헤드(Overhead)PCID 지원 여부, 워크로드 유형PCID 활성화 확인낮음
NUMA 지역성numastat, remote 접근 비율로컬 노드 할당 정책중간
메이저 폴트sar -B, pgmajfault메모리 추가 또는 스왑(Swap) 튜닝최고
# TLB 관측 기본 명령
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-load-misses ./workload
perf record -g -e tlb:tlb_flush -- sleep 5 2>/dev/null || true

# 종합 TLB/메모리 성능 분석 스크립트
echo "=== TLB Miss Rate ==="
perf stat -e dTLB-load-misses,dTLB-loads -a -- sleep 5 2>&1 | tail -3
echo "=== TLB Flush Stats ==="
grep tlb /proc/vmstat
echo "=== Huge Pages ==="
grep -i huge /proc/meminfo
echo "=== THP Status ==="
cat /sys/kernel/mm/transparent_hugepage/enabled
echo "=== Top VmPTE Processes ==="
for p in /proc/[0-9]*/status; do
    grep -H VmPTE "$p" 2>/dev/null
done | sort -t: -k3 -n -r | head -10

TLB 튜닝 결정 트리

dTLB-load-misses > 5%?
├── Yes → working set 크기 확인
│   ├── > TLB 커버리지 → Huge Pages 적용
│   │   ├── 예측 가능한 패턴 → HugeTLB (예약)
│   │   └── 범용 워크로드 → THP (madvise 모드)
│   └── < TLB 커버리지 → 접근 패턴 분석 (locality 개선)

└── No → shootdown 빈도 확인
    ├── nr_tlb_remote_flush 높음 → mmap/munmap 빈도 감소
    │   ├── 할당자 튜닝 (retain, decay 설정)
    │   └── madvise(MADV_FREE) vs munmap
    └── context switch 빈도 높음 → PCID 활성화 확인

5-Level Paging (LA57)

x86-64의 기존 4단계 페이징은 48비트 가상 주소 공간(256TB)을 지원합니다. 그러나 대규모 메모리를 사용하는 서버와 클라우드 환경에서는 더 넓은 가상 주소 공간이 필요합니다. Intel은 LA57 (Linear Address 57-bit) 확장을 통해 5단계 페이징을 도입했습니다.

LA57 개요

항목4-Level 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);
    }
}

사용자 공간(User Space) 주소 범위

/* arch/x86/include/asm/processor.h */

/* 4-Level: 사용자 공간 0x0000_0000_0000 ~ 0x7FFF_FFFF_FFFF (128TB) */
#define TASK_SIZE_MAX  ((1UL << 47) - PAGE_SIZE)

/* 5-Level: 사용자 공간 0x0000_0000_0000 ~ 0xFF_FFFF_FFFF_FFFF (64PB) */
#ifdef CONFIG_X86_5LEVEL
#define TASK_SIZE_MAX  ((1UL << 56) - PAGE_SIZE)
#endif

/* 사용자 프로세스가 mmap() 시 MAP_ABOVE4G/위치 지정 가능 */
/* ADDR_LIMIT_47BIT으로 48비트 호환 모드 선택 가능 */
주의: 5단계 페이징은 TLB Miss 시 워크 비용이 20~25% 증가합니다. 실제로 128PB 주소 공간이 필요하지 않다면, 4단계 페이징을 유지하는 것이 성능에 유리합니다. AWS의 대규모 인스턴스(12TB+ RAM)에서 주로 활용됩니다.

ARM64 페이지 테이블

ARM64(AArch64)는 x86-64와 다른 페이지 테이블 체계를 사용합니다. 가장 큰 차이점은 TTBR0/TTBR1 이중 베이스 레지스터로 사용자/커널 공간(Kernel Space)을 하드웨어 수준에서 분리하는 점입니다.

TTBR0 / TTBR1 분리

ARM64 가상 주소 공간 분할:

TTBR0 (사용자 공간):
  0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF  (48비트, 4KB granule)
  0x0000_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF        (52비트 확장 시)

TTBR1 (커널 공간):
  0xFFFF_0000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF  (48비트, 4KB granule)

주소의 최상위 비트로 자동 선택:
  bit[63] = 0 → TTBR0 (사용자)
  bit[63] = 1 → TTBR1 (커널)
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 */
코드 설명
  • PTE_VALID / PTE_TABLE_BITARM64 PTE의 하위 2비트입니다. bit 0(PTE_VALID)은 엔트리 유효성, bit 1(PTE_TABLE_BIT)은 다음 레벨 테이블을 가리키는지(1) 아니면 블록 매핑인지(0)를 구분합니다. PMD 레벨에서 TABLE_BIT=0이면 2MB 블록(Huge Page)으로 직접 매핑됩니다. 소스: arch/arm64/include/asm/pgtable-hwdef.h
  • PTE_USER / PTE_RDONLYAccess Permission 비트(AP[1:0])입니다. PTE_USER(AP[1])이 설정되면 EL0(사용자 모드)에서 접근 가능하고, PTE_RDONLY(AP[2])가 설정되면 읽기 전용입니다. x86의 U/S, R/W 비트와 유사하지만, ARM64는 특권/사용자 접근 권한을 더 세밀하게 제어합니다.
  • PTE_AF / PTE_NGPTE_AF(Access Flag)는 x86의 Accessed 비트에 해당하며, 최초 접근 시 하드웨어가 자동 설정합니다(ARMv8.1 FEAT_HAFDBS). PTE_NG(non-Global)는 ASID 태깅 대상임을 표시하며, 사용자 페이지는 항상 nG=1로 설정됩니다.
  • PTE_DBM / PTE_CONTPTE_DBM(Dirty Bit Modifier, ARMv8.1)은 하드웨어 Dirty 비트 자동 관리를 활성화합니다. PTE_CONT(Contiguous hint, bit 52)는 연속 16개 PTE가 물리적으로 연속임을 TLB에 알려, 하나의 TLB 엔트리로 64KB(4KB granule x 16)를 커버하게 합니다.
  • PTE_PXN / PTE_UXN실행 금지 비트입니다. x86이 단일 NX 비트인 반면, ARM64는 PXN(Privileged Execute-Never)과 UXN(User Execute-Never)으로 특권/사용자 실행 권한을 독립 제어합니다. 커널 데이터 영역은 PXN=1, 사용자 코드 영역은 UXN=0, PXN=1로 설정합니다.
ℹ️

Contiguous Hint (PTE_CONT): 연속 16개(4KB granule) 또는 128개(64KB granule) PTE가 물리적으로 연속임을 TLB에 알려, 하나의 TLB 엔트리로 64KB~2MB를 커버합니다. CONFIG_ARM64_CONTPTE로 제어됩니다.

ARM64 TLB 관리 명령

/* arch/arm64/include/asm/tlbflush.h */

/* ARM64 TLB 무효화 명령 체계 (TLBI) */

/* 전체 TLB 무효화 (Inner Shareable = 모든 코어) */
static inline void flush_tlb_all(void)
{
    dsb(ishst);     /* 이전 스토어 완료 대기 */
    __tlbi(vmalle1is); /* TLBI VMALLE1IS: VM All E1, Inner Shareable */
    dsb(ish);       /* TLB 무효화 완료 대기 */
    isb();          /* 명령어 파이프라인 동기화 */
}

/* 단일 페이지 무효화 (특정 VA + ASID) */
static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,
                                           unsigned long uaddr)
{
    unsigned long addr = __TLBI_VADDR(uaddr, ASID(vma->vm_mm));
    dsb(ishst);
    __tlbi(vale1is, addr); /* TLBI VAE1IS: VA, EL1, Inner Shareable */
}

/* ASID 기반 무효화 (프로세스 전체) */
static inline void flush_tlb_mm(struct mm_struct *mm)
{
    unsigned long asid = ASID(mm);
    dsb(ishst);
    __tlbi(aside1is, asid); /* TLBI ASIDE1IS: ASID, EL1, IS */
    dsb(ish);
}

/* ARM64 TLBI vs x86 IPI 비교:
 * - x86: CPU-0이 IPI를 N-1개 CPU에 전송 → 각 CPU가 INVLPG 실행
 *   (소프트웨어 기반, O(N) 지연)
 * - ARM64: TLBI IS 명령 하나로 모든 코어에 하드웨어 브로드캐스트
 *   (하드웨어 기반, O(1) 명령 + 완료 대기)
 */

ARM64 Context Switch와 ASID

/* arch/arm64/mm/context.c */

/* ARM64 ASID 관리: 8비트(256) 또는 16비트(65536) */
static atomic64_t asid_generation;
static unsigned long *asid_map;   /* 비트맵: 사용 중 ASID 추적 */

void check_and_switch_context(struct mm_struct *mm)
{
    unsigned long flags;
    u64 asid, generation;

    asid = atomic64_read(&mm->context.id);
    generation = atomic64_read(&asid_generation);

    if ((asid ^ generation) >> asid_bits) {
        /* ASID가 현재 세대와 다름 → 새 ASID 할당 필요 */
        raw_spin_lock_irqsave(&cpu_asid_lock, flags);
        asid = new_context(mm);
        atomic64_set(&mm->context.id, asid);
        raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
    }

    /* TTBR0 업데이트: 새 페이지 테이블 + ASID */
    cpu_switch_mm(mm->pgd, mm);
}

/* ASID 롤오버: 모든 ASID가 소진되면 세대 번호 증가 + 전체 TLB 플러시 */
static u64 new_context(struct mm_struct *mm)
{
    u64 asid = find_next_zero_bit(asid_map, num_asids, cur_idx);
    if (asid >= num_asids) {
        /* 롤오버! 새 세대 시작 */
        generation = atomic64_add_return(asid_first, &asid_generation);
        flush_context();         /* 전체 TLB 플러시 */
        bitmap_zero(asid_map, num_asids);
        asid = find_next_zero_bit(asid_map, num_asids, 1);
    }
    __set_bit(asid, asid_map);
    return asid | generation;
}
코드 설명
  • 3~4행asid_generation은 현재 ASID 세대(generation) 번호를 전역으로 추적합니다. asid_map 비트맵은 현재 세대에서 할당된 ASID를 추적하여 중복 할당을 방지합니다. 소스: arch/arm64/mm/context.c
  • 6~15행check_and_switch_context()는 Context Switch 시 호출됩니다. mm->context.id에 저장된 ASID의 상위 비트(세대 번호)가 현재 asid_generation과 일치하면 ASID가 유효하므로 재할당 없이 바로 cpu_switch_mm()으로 TTBR0를 업데이트합니다. 세대가 다르면 new_context()로 새 ASID를 할당합니다.
  • 18행cpu_switch_mm()은 TTBR0_EL1 레지스터에 새 PGD 물리 주소와 ASID를 기록합니다. ASID가 TTBR에 태깅되므로, TLB에 캐시된 이전 프로세스의 엔트리를 플러시하지 않아도 ASID로 구분됩니다.
  • 21~29행new_context()asid_map에서 비어있는 ASID를 find_next_zero_bit()로 탐색합니다. 8비트 ASID(256개)가 모두 소진되면 롤오버(rollover)가 발생하여 세대 번호를 증가시키고 flush_context()로 전체 TLB를 플러시한 뒤 비트맵을 초기화합니다. 반환값은 ASID와 세대 번호를 OR 결합한 값입니다.
ℹ️

16비트 ASID: ARMv8.2의 16비트 ASID(65536개)를 사용하면 롤오버가 거의 발생하지 않아 Context Switch 시 TLB 플러시가 크게 줄어듭니다. CONFIG_ARM64_16BIT_ASID로 활성화합니다. Apple Silicon M1/M2는 16비트 ASID를 지원합니다.

PTE 비트필드 상세

PTE(Page Table Entry)의 64비트 각 비트는 권한, 캐시 정책, 보안, 상태 추적 등의 정보를 인코딩합니다. x86-64와 ARM64의 비트 배치는 크게 다릅니다.

x86-64 PTE 64비트 레이아웃

x86-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=bit 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 };
}

PAT/MAIR 메모리 타입과 TLB

메모리 타입(Memory Type)은 CPU가 특정 물리 주소 범위에 접근할 때 사용하는 캐싱 및 쓰기 정책을 정의합니다. x86에서는 PAT(Page Attribute Table), ARM64에서는 MAIR(Memory Attribute Indirection Register)가 이 역할을 담당합니다. 메모리 타입 정보는 TLB 엔트리에 함께 저장되어, 물리 주소 변환 시 캐시 정책도 동시에 결정됩니다.

x86 PAT (Page Attribute Table)

x86에서 각 PTE는 3개의 비트(PWT, PCD, PAT)를 통해 8개의 PAT 엔트리 중 하나를 선택합니다. PAT는 MSR IA32_PAT(주소 0x277)에 저장된 8개 슬롯으로 구성되며, 각 슬롯은 메모리 타입을 정의합니다. 커널은 부팅 시 PAT를 초기화하여 슬롯별 정책을 설정합니다.

/* arch/x86/mm/pat/memtype.c: 부팅 시 PAT 초기화 */
/* PTE 비트 조합 -> PAT 인덱스: PAT[2] PCD[1] PWT[0] */
/*  인덱스 0 (PAT=0,PCD=0,PWT=0): WB  (Write-Back)       -- 기본값      */
/*  인덱스 1 (PAT=0,PCD=0,PWT=1): WT  (Write-Through)                   */
/*  인덱스 2 (PAT=0,PCD=1,PWT=0): UC- (Uncacheable Minus)               */
/*  인덱스 3 (PAT=0,PCD=1,PWT=1): UC  (Uncacheable)       -- MMIO      */
/*  인덱스 4 (PAT=1,PCD=0,PWT=0): WB  (Write-Back, 재지정 가능)         */
/*  인덱스 5 (PAT=1,PCD=0,PWT=1): WP  (Write-Protected)                 */
/*  인덱스 6 (PAT=1,PCD=1,PWT=0): UC- (Uncacheable Minus)               */
/*  인덱스 7 (PAT=1,PCD=1,PWT=1): WC  (Write-Combining)   -- 프레임버퍼 */

#define PAT_VALUE ((u64)PAT_WRITE_BACK      << (0 * 8) | \
                   (u64)PAT_WRITE_THROUGH    << (1 * 8) | \
                   (u64)PAT_UNCACHED_MINUS   << (2 * 8) | \
                   (u64)PAT_UNCACHED         << (3 * 8) | \
                   (u64)PAT_WRITE_BACK      << (4 * 8) | \
                   (u64)PAT_WRITE_PROTECTED  << (5 * 8) | \
                   (u64)PAT_UNCACHED_MINUS   << (6 * 8) | \
                   (u64)PAT_WRITE_COMBINING  << (7 * 8))

static void pat_init(void)
{
    wrmsrl(MSR_IA32_PAT, PAT_VALUE);
}

/* PTE 보호 플래그 -- 메모리 타입 선택 비트 */
#define _PAGE_PWT       ((pteval_t)1 << 3)   /* PAT 인덱스 bit0 */
#define _PAGE_PCD       ((pteval_t)1 << 4)   /* PAT 인덱스 bit1 */
#define _PAGE_PAT       ((pteval_t)1 << 7)   /* PAT 인덱스 bit2 (4KB PTE) */
#define _PAGE_PAT_LARGE ((pteval_t)1 << 12)  /* PAT 인덱스 bit2 (2MB PMD) */

/* 메모리 타입 선택 헬퍼 매크로 */
#define pgprot_noncached(prot)    __pgprot(pgprot_val(prot) | _PAGE_PCD | _PAGE_PWT)
#define pgprot_writecombine(prot) __pgprot((pgprot_val(prot) & ~(_PAGE_PCD|_PAGE_PWT)) | _PAGE_PAT)
#define pgprot_writethrough(prot) __pgprot((pgprot_val(prot) & ~(_PAGE_PCD|_PAGE_PAT)) | _PAGE_PWT)

ARM64 MAIR (Memory Attribute Indirection Register)

ARM64에서는 PTE의 AttrIndx[2:0] 필드(3비트)가 MAIR_EL1 레지스터의 8개 슬롯 중 하나를 가리킵니다. 각 슬롯은 8비트로 내부 및 외부 캐시 정책을 독립적으로 지정합니다. x86의 PAT와 달리 MAIR는 내부 캐시(Inner Shareable, CPU 캐시)와 외부 캐시(Outer Shareable, 시스템 캐시)를 구분하여 설정합니다.

/* arch/arm64/include/asm/memory.h: MAIR 인덱스 정의 */
#define MT_DEVICE_nGnRnE  0  /* Device, non-Gathering/Reordering/Early Write */
#define MT_DEVICE_nGnRE   1  /* Device, nGnRE (일반 MMIO)                   */
#define MT_DEVICE_GRE     2  /* Device, Gathering+Reordering+Early Write    */
#define MT_NORMAL_NC      3  /* Normal, Non-Cacheable                       */
#define MT_NORMAL         4  /* Normal, WB+WA (기본 메모리)                  */
#define MT_NORMAL_TAGGED  5  /* Normal, Tagged (MTE 메모리 태깅 확장)        */
#define MT_NORMAL_WT      6  /* Normal, Write-Through (프레임버퍼 등)        */

/* MAIR_EL1 인코딩: 각 슬롯 8비트 -- [7:4]=외부캐시 [3:0]=내부캐시 */
#define MAIR_ATTRIDX(attr, idx)  ((attr) << ((idx) * 8))

#define MAIR_EL1_SET \
    (MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \
     MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE,  MT_DEVICE_nGnRE)  | \
     MAIR_ATTRIDX(MAIR_ATTR_DEVICE_GRE,   MT_DEVICE_GRE)    | \
     MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC,    MT_NORMAL_NC)     | \
     MAIR_ATTRIDX(MAIR_ATTR_NORMAL,       MT_NORMAL)        | \
     MAIR_ATTRIDX(MAIR_ATTR_NORMAL_TAGGED,MT_NORMAL_TAGGED) | \
     MAIR_ATTRIDX(MAIR_ATTR_NORMAL_WT,    MT_NORMAL_WT))

/* 부팅 시 MAIR_EL1 설정 (arch/arm64/mm/mmu.c) */
write_sysreg(MAIR_EL1_SET, mair_el1);

/* ARM64 pgprot 헬퍼: AttrIndx 필드를 교체하여 메모리 타입 지정 */
#define pgprot_noncached(prot) \
    __pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_DEVICE_nGnRnE))
#define pgprot_writecombine(prot) \
    __pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_NORMAL_NC))
#define pgprot_device(prot) \
    __pgprot((pgprot_val(prot) & ~PTE_ATTRINDX_MASK) | PTE_ATTRINDX(MT_DEVICE_nGnRE))
x86 PAT vs ARM64 MAIR: 메모리 타입 선택 구조 x86 PAT (IA32_PAT MSR) PTE 비트: PAT PCD PWT 인덱스 선택 0 (000): WB (Write-Back) 일반 RAM 기본값 1 (001): WT (Write-Through) 읽기 캐시O, 쓰기 즉시 2 (010): UC- (Uncacheable-) MTRR 오버라이드 가능 3 (011): UC (Uncacheable) MMIO, 캐시 완전 차단 4 (100): WB (재지정) OS 용도 재정의 5 (101): WP (Write-Protected) 쓰기 차단, 읽기 캐시 6 (110): UC- (재지정) UC- 두 번째 슬롯 7 (111): WC (Write-Combining) 프레임버퍼, GPU VRAM ARM64 MAIR_EL1 PTE 필드: AttrIndx[2:0] 인덱스 선택 0: Device nGnRnE 엄격한 장치 메모리 1: Device nGnRE 일반 MMIO 2: Device GRE 재정렬 허용 장치 3: Normal NC 비캐시 일반 메모리 4: Normal WB+WA 기본 RAM (최고 성능) 5: Normal Tagged (MTE) 메모리 태깅 활성화 6: Normal WT Write-Through 캐시 TLB 엔트리에 메모리 타입 정보가 함께 저장됨 x86: TLB = VPN + PFN + 보호비트 + PAT 인덱스 ARM64: TLB = VPN + PFN + ASID + AttrIndx + SH[1:0] 메모리 타입 불일치로 pgprot 변경 시 반드시 TLB 무효화 필요 (TLB shootdown)
그림: x86 PAT 8슬롯(PWT/PCD/PAT 비트 선택)과 ARM64 MAIR 슬롯(AttrIndx 필드 선택) 비교 -- 메모리 타입 정보는 TLB 엔트리에 포함됨
메모리 타입x86 PAT 슬롯ARM64 MAIR 인덱스캐시 동작TLB 관련 특이사항
WB (Write-Back)0 (기본)4 (Normal WB)읽기·쓰기 모두 캐시, 성능 최고일반 RAM 매핑, TLB 동작 무관
WT (Write-Through)16 (Normal WT)읽기 캐시, 쓰기 즉시 반영일반적, TLB 동작 무관
WC (Write-Combining)73 (Normal NC)쓰기 버퍼 합산 후 전송GPU/프레임버퍼, TLB 히트 후 WC 버퍼 경유
UC (Uncacheable)30 (Device nGnRnE)캐시 전혀 없음매 접근마다 물리 메모리 직접 접근, TLB는 여전히 사용
Device nGnRE해당없음1장치 메모리, 재정렬 없음추측 실행(Speculative Load) 금지
⚠️

메모리 타입 변경 시 TLB 무효화 필수: pgprot_noncached() 등으로 보호 플래그를 변경하면 반드시 TLB Shootdown을 수행해야 합니다. TLB에 이전 메모리 타입이 캐시된 상태로 접근하면 구형 메모리 타입으로 동작하여 데이터 일관성(Coherency)이 깨질 수 있습니다. 특히 WB 영역을 UC로 재매핑할 때는 캐시 플러시(clflush)도 병행해야 합니다.

TLB 내부 구조

TLB는 단순한 캐시가 아니라, 마이크로아키텍처 수준에서 정교하게 설계된 연관 메모리(Content-Addressable Memory, CAM)입니다. 프로세서마다 구조와 크기가 다르며, 성능에 직접적인 영향을 줍니다.

Set-Associative vs Fully-Associative

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 파이프라인(Pipeline)의 명령어 페치와 데이터 접근이 동시에 발생하므로, 각각 독립적인 TLB 포트가 필요하기 때문입니다.

TLB 계층 구조 (최신 x86 프로세서):

L1 ITLB (명령어):
  - 4KB: 8-way, 64 entries (Fully-Associative 인 경우도 있음)
  - 2MB: 8 entries, Fully-Associative
  - 1 cycle 지연

L1 DTLB (데이터):
  - 4KB: 4-way, 64 entries
  - 2MB: 4-way, 32 entries
  - 1GB: 4-way, 4 entries
  - 1 cycle 지연

L2 STLB (통합 - Second-level TLB):
  - 4KB + 2MB: 12-way, 1536~2048 entries
  - 1GB: 4-way, 16 entries
  - 7~8 cycles 지연

Miss → 페이지 테이블 워크 (Page Walker):
  - 하드웨어 워커 (x86/ARM)
  - 200+ cycles 지연

최신 프로세서 TLB 스펙 비교

프로세서L1 ITLB (4KB)L1 DTLB (4KB)L2 STLB (4KB)L1 DTLB (2MB)워커
Intel Alder Lake (P-core)8-way, 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 보장일부 임베디드

TLB Coalescing / Contiguous PTE

현대 CPU는 TLB 엔트리 수가 제한적이므로, 인접하고 동일한 속성을 가진 여러 PTE를 하나의 TLB 엔트리로 합치는 TLB 합체(TLB Coalescing) 기법을 사용합니다. 이를 통해 동일한 물리 TLB 슬롯으로 더 넓은 가상 주소 범위를 커버할 수 있습니다.

Intel TLB Coalescing

Intel 프로세서는 하드웨어 레벨에서 자동으로 TLB 합체를 수행합니다. 예를 들어 물리적으로 연속된 8개의 4KB 페이지가 동일한 보호 속성(R/W, U/S, XD 등)을 가질 경우, CPU 내부 로직이 이를 단일 32KB TLB 엔트리로 병합할 수 있습니다. 소프트웨어가 별도로 처리할 필요 없이 하드웨어가 투명하게 처리합니다.

ARM64 Contiguous PTE (PTE_CONT)

ARM64는 소프트웨어가 명시적으로 연속 힌트를 제공합니다. PTE 비트 52에 해당하는 PTE_CONT 비트를 설정하면, 하드웨어 Table Walk Unit(TWU)이 이를 하나의 합체된 TLB 엔트리로 처리합니다.

/* arch/arm64/include/asm/pgtable-hwdef.h */

#define PTE_CONT        (_AT(pteval_t, 1) << 52)  /* 연속 힌트 비트 */
#define CONT_PTES       16                           /* 4KB 그래뉼: 16개 연속 */
#define CONT_PTE_SIZE   (CONT_PTES * PAGE_SIZE)       /* = 64KB */
#define CONT_PTE_MASK   (~(CONT_PTE_SIZE - 1))

/* arch/arm64/mm/contpte.c (CONFIG_ARM64_CONTPTE) */

/*
 * set_ptes()가 연속 힌트를 자동으로 처리합니다.
 * CONT_PTES개 PTE가 정렬 조건을 만족하면 PTE_CONT를 설정합니다.
 */
void contpte_set_ptes(struct mm_struct *mm, unsigned long addr,
                     pte_t *ptep, pte_t pte, unsigned int nr)
{
    unsigned long next;
    unsigned long end = addr + (unsigned long)nr * PAGE_SIZE;
    unsigned long pfn = pte_pfn(pte);

    while (addr != end) {
        next = min(ALIGN(addr + 1, CONT_PTE_SIZE), end);
        if ((next - addr == CONT_PTE_SIZE) &&
            IS_ALIGNED(pfn << PAGE_SHIFT, CONT_PTE_SIZE)) {
            /* 조건 충족: PTE_CONT 비트 설정 */
            __contpte_try_fold(mm, addr, ptep, pte);
        } else {
            __set_ptes(mm, addr, ptep, pte, (next - addr) / PAGE_SIZE);
        }
        addr = next;
        pfn += (next - addr) / PAGE_SIZE;
    }
}
ARM64 Contiguous PTE: 16 x 4KB → 단일 64KB TLB 엔트리 페이지 테이블 (PTE) PTE[0] | PTE_CONT | PFN+0 PTE[1] | PTE_CONT | PFN+1 PTE[2] | PTE_CONT | PFN+2 ... (총 16개) ... PTE[15]| PTE_CONT | PFN+15 TWU 합체 PTE_CONT 감지 후 병합 TLB (합체 후) 단일 합체 TLB 엔트리 VA: [base] ~ [base + 0xFFFF] PA: PFN_BASE ~ PFN_BASE+15 크기: 64KB (4KB x 16) PTE_CONT 미사용 (일반 4KB PTE) TLB 엔트리 16개 소비 TLB 무효화: TLBI IS x 16회 필요 TLB Miss율 상대적으로 높음 PTE_CONT 사용 (64KB 합체) TLB 엔트리 1개 소비 (16배 절약) TLB 무효화: TLBI IS x 1회 TLB Miss율 대폭 감소

리눅스 커널은 CONFIG_ARM64_CONTPTE 옵션(커널 6.5+)으로 이 기능을 활성화하며, set_ptes() 호출 시 자동으로 연속 힌트를 적용합니다. pte_unmap()이나 속성 변경 시에는 contpte_try_unfold()로 분해 후 처리합니다.

TLB Coalescing 성능 효과 요약
시나리오일반 4KB PTEContiguous PTE (64KB)향상
TLB 슬롯 소비 (64KB 매핑)16개1개16배 절약
TLB Miss율 (순차 접근)기준~93% 감소대폭 감소
TLB 무효화 비용TLBI x 16TLBI x 116배 절약
대규모 파일 mmap (GB 단위)높은 TLB 압력낮은 TLB 압력처리량 향상
적용 조건제한 없음정렬+연속+동일 속성조건 충족 시
TLB Coalescing 활용: ARM64 시스템에서 대용량 파일 mmap이나 익명 대형 매핑 시 CONFIG_ARM64_CONTPTE=y를 사용하면 THP 미적용 영역에서도 TLB 효율을 크게 높일 수 있습니다. Intel에서는 별도 설정 없이 하드웨어가 자동으로 수행합니다.

mmu_gather 배칭 프레임워크

struct mmu_gather는 페이지 테이블을 해체하고 TLB를 무효화하는 작업을 배칭(batching)하여 TLB Shootdown 비용을 최소화하는 프레임워크입니다. munmap(), exit_mmap(), mremap() 등에서 핵심적으로 사용됩니다.

struct mmu_gather

/* include/asm-generic/tlb.h */

struct mmu_gather {
    struct mm_struct        *mm;           /* 대상 mm */

    /* 무효화 범위 */
    unsigned long            start;
    unsigned long            end;

    /* 무효화 필요 플래그 */
    unsigned int             freed_tables : 1;  /* 페이지 테이블 해제됨 */
    unsigned int             need_flush_all : 1; /* 전체 플러시 필요 */
    unsigned int             cleared_ptes : 1;
    unsigned int             cleared_pmds : 1;
    unsigned int             cleared_puds : 1;
    unsigned int             cleared_p4ds : 1;
    unsigned int             vma_exec : 1;
    unsigned int             vma_huge : 1;

    /* 해제할 페이지 배치 */
    struct mmu_gather_batch  *active;       /* 현재 배치 */
    struct mmu_gather_batch  local;         /* 로컬 배치 (스택) */
    struct page             *__pages[MMU_GATHER_BUNDLE]; /* 8개 */
};

사용 패턴

/* mm/memory.c - 일반적인 mmu_gather 사용 패턴 */

void unmap_region(struct mm_struct *mm,
                  struct maple_tree *mt,
                  struct vm_area_struct *vma,
                  unsigned long start, unsigned long end)
{
    struct mmu_gather tlb;

    /* 1단계: 배칭 시작 - TLB 무효화 준비 */
    tlb_gather_mmu(&tlb, mm);

    /* 2단계: PTE를 0으로 설정하고, 페이지를 배치에 추가 */
    unmap_vmas(&tlb, mt, vma, start, end, 0, false);

    /* 3단계: 페이지 테이블 자체를 해제 */
    free_pgtables(&tlb, mt, vma, start, end);

    /* 4단계: 배칭된 TLB 무효화 실행 + 페이지 해제 */
    tlb_finish_mmu(&tlb);
}
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을 보유해야 합니다. 배칭 도중 다른 스레드(Thread)가 같은 VMA를 수정하면 use-after-free가 발생할 수 있습니다. 커널 6.1부터 maple tree 기반 VMA 관리로 이 문제가 완화되었습니다.

페이지 폴트 핸들링

페이지 폴트(Page Fault)는 MMU가 가상 주소를 물리 주소로 변환할 수 없을 때 발생하는 예외입니다. 리눅스 커널의 페이지 폴트 핸들러(Handler)는 demand paging, COW, 스왑 인, 파일 매핑 등 가상 메모리의 핵심 기능을 구현합니다.

페이지 폴트 분류

종류원인처리비용
마이너 폴트 (Minor)페이지가 메모리에 있지만 PTE 미설정PTE 설정만 (디스크 I/O 없음)~1-10 us
메이저 폴트 (Major)페이지가 디스크에 있음 (스왑/파일)디스크에서 읽기 후 PTE 설정~1-10 ms
COW 폴트읽기전용 페이지에 쓰기 시도페이지 복사 후 쓰기 가능으로 설정~1-5 us
SEGV (비정상)유효하지 않은 주소 접근SIGSEGV 시그널(Signal) 전송프로세스 종료
커널 폴트커널 코드의 잘못된 접근Oops/Panic 또는 fixup시스템 불안정

do_page_fault 처리 흐름

페이지 폴트 처리 흐름 (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);
}
코드 설명
  • 3~8행vm_fault 구조체를 초기화합니다. address & PAGE_MASK로 페이지 경계에 정렬하고, linear_page_index()는 VMA 내 파일 오프셋(페이지 단위)을 계산합니다. 이 구조체는 이후 폴트 처리 전 과정에서 컨텍스트로 전달됩니다. 소스: mm/memory.c
  • 13~16행pgd_offset(mm, address)mm->pgd에서 가상 주소의 PGD 인덱스에 해당하는 엔트리 포인터를 반환합니다. p4d_alloc()은 P4D 테이블이 없으면 새로 할당합니다. 4단계 페이징에서는 P4D가 PGD로 폴딩되어 실제 할당이 발생하지 않습니다.
  • 18~20행pud_alloc()은 PUD 테이블을 할당/반환합니다. 각 *_alloc() 함수는 이미 테이블이 존재하면 기존 테이블을 반환하고, 없으면 __get_free_page(GFP_PGTABLE_USER)로 새 4KB 페이지를 할당합니다. 할당 실패 시 VM_FAULT_OOM을 반환합니다.
  • 22~26행PUD 레벨에서 1GB Huge Page를 검사합니다. pud_trans_huge()는 PUD 엔트리가 다음 테이블을 가리키는 대신 1GB 물리 블록을 직접 매핑하는지 확인합니다. 참이면 create_huge_pud()로 Huge Page 폴트를 처리하고, PMD/PTE 단계를 건너뜁니다.
  • 32~36행PMD 레벨에서 2MB THP(Transparent Huge Page)를 검사합니다. pmd_trans_huge()가 참이면 PMD가 512개 PTE 대신 하나의 2MB 블록을 매핑합니다. 이 경우 create_huge_pmd()로 처리합니다. 둘 다 아니면 handle_pte_fault()로 최종 4KB PTE 레벨 폴트를 처리합니다.
ℹ️

Speculative Page Fault: 커널 6.x에서 논의 중인 최적화입니다. mmap_lock을 잡지 않고 낙관적으로 폴트를 처리한 뒤, 실패하면 락을 잡고 재시도합니다. 멀티스레드 워크로드에서 mmap_lock 경합을 크게 줄여줍니다.

NUMA 밸런싱과 TLB

NUMA(Non-Uniform Memory Access) 시스템에서 프로세스가 원격 노드(remote node)의 메모리에 접근하면 레이턴시가 높아집니다. 리눅스 커널의 NUMA 밸런싱(NUMA Balancing)은 프로세스가 실제로 접근하는 메모리 패턴을 런타임에 측정하여 데이터를 가까운 노드로 자동 마이그레이션합니다.

PROT_NONE 트릭: 페이지 프로빙

NUMA 밸런싱의 핵심 기법은 PTE의 Present 비트를 일시적으로 제거(PROT_NONE 설정)하는 것입니다. Present 비트가 없으면 해당 페이지 접근 시 페이지 폴트가 발생하고, 커널은 그 폴트 핸들러에서 접근한 CPU와 페이지의 물리 위치를 비교하여 이동 여부를 결정합니다.

/* kernel/sched/fair.c — NUMA 스캐닝 주기 */

/*
 * task_numa_work(): 주기적으로 호출되어 VMA의 PTE를
 * PROT_NONE으로 변경합니다. 이후 접근 시 do_numa_page()가
 * 호출되어 마이그레이션을 결정합니다.
 */
static void task_numa_work(struct callback_head *work)
{
    struct task_struct *p = current();
    struct mm_struct   *mm = p->mm;
    struct vm_area_struct *vma;
    unsigned long start, end;

    /* 스캔 범위 결정 (scan_size 제한) */
    start = mm->numa_scan_offset;
    end   = start + NUMA_MIGRATION_RATE;

    mmap_read_lock(mm);
    for_each_vma_range(vmi, vma, start, end) {
        if (!(vma_is_accessible(vma)))
            continue;
        /* PTE를 PROT_NONE으로 변경 → TLB 무효화 유발 */
        change_prot_numa(vma, start, end);
    }
    mmap_read_unlock(mm);

    /* 다음 스캔 위치 갱신 */
    mm->numa_scan_offset = end;
}
/* mm/memory.c — NUMA 폴트 핸들러 */

static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page = NULL;
    int page_nid = NUMA_NO_NODE;
    int target_nid, last_cpupid = -1;
    bool migrated = false;

    /* 1단계: PTE 잠금 및 페이지 조회 */
    vmf_pte_lock(vmf);
    page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
    if (!page)
        goto out_unlock;

    /* 2단계: 현재 페이지의 NUMA 노드 파악 */
    page_nid = page_to_nid(page);

    /* 3단계: 마이그레이션 대상 노드 결정 */
    target_nid = numa_migrate_prep(page, vmf, vmf->address,
                                    page_nid, &flags);

    if (target_nid == NUMA_NO_NODE) {
        /* 이미 최적 위치: PTE 복원만 수행 */
        put_page(page);
        goto out_map;
    }

    /* 4단계: 페이지 마이그레이션 시도 */
    migrated = migrate_misplaced_page(page, vmf, target_nid);

out_map:
    /* 5단계: PTE 복원 (PROT_NONE → 원래 접근 권한) */
    pte_mknuma_restore(vmf);
    return 0;
}
NUMA 밸런싱 사이클 (PROT_NONE 트릭) NUMA Node 0 (CPU 0-15) 프로세스 CPU 3에서 실행 로컬 메모리 레이턴시: 100ns task_numa_work(): PTE → PROT_NONE TLB 무효화 발생 NUMA Node 1 (CPU 16-31) 원격 메모리 레이턴시: 300ns 원격 페이지 이동 전 원격 메모리 페이지 (이동 대상) page_nid = 1, target_nid = 0 PROT_NONE PTE 접근 → 페이지 폴트 → do_numa_page() 호출 TLB Miss 발생 (의도적): CPU ID + 페이지 노드 ID 비교 후 마이그레이션 결정 migrate_misplaced_page(): Node 1 → Node 0 마이그레이션 후 PTE 복원 (PROT_NONE 해제) 로컬 메모리 접근: 레이턴시 감소 TLB 영향 단기: 추가 TLB Miss 발생 장기: 로컬 접근으로 TLB 효율 향상

task_numa_placement()와 마이그레이션 결정

task_numa_placement()는 주기적으로 각 태스크의 NUMA 통계를 집계하여 최적의 실행 노드와 메모리 노드를 결정합니다. 페이지 폴트 통계(numa_faults[])를 분석해 CPU 친화성(CPU affinity)과 메모리 배치를 함께 조정합니다.

/* kernel/sched/fair.c */

static void task_numa_placement(struct task_struct *p)
{
    int seq, nid, max_nid = NUMA_NO_NODE;
    unsigned long max_faults = 0;

    for_each_online_node(nid) {
        unsigned long faults;
        /* 노드별 폴트 수 집계: 로컬+원격 폴트 합산 */
        faults = task_faults(p, nid);
        if (faults > max_faults) {
            max_faults = faults;
            max_nid = nid;  /* 가장 많이 접근한 노드 */
        }
    }

    /* 선호 노드가 바뀌면 태스크 마이그레이션 검토 */
    if (max_nid != p->numa_preferred_nid)
        sched_setnuma(p, max_nid);
}
NUMA 밸런싱 TLB 영향 분석
단계동작TLB 영향
PTE 마킹 (PROT_NONE)change_prot_numa(): PTE Present 비트 제거해당 페이지 TLB 무효화 (INVLPG / TLBI)
페이지 접근PROT_NONE PTE → 페이지 폴트 발생TLB Miss (의도적 프로빙)
마이그레이션 결정do_numa_page(): 노드 비교 후 이동PTE 업데이트 → TLB 갱신
마이그레이션 완료새 PFN으로 PTE 재설정신규 TLB 엔트리 (로컬 노드 PA)
장기 효과로컬 메모리 접근 증가TLB Hit율 향상, 레이턴시 감소

NUMA 밸런싱 튜닝

# NUMA 밸런싱 활성화/비활성화
echo 1 > /proc/sys/kernel/numa_balancing

# 스캔 주기 (ms 단위, 기본 1000ms)
echo 1000 > /proc/sys/kernel/numa_balancing_scan_period_min_ms
echo 60000 > /proc/sys/kernel/numa_balancing_scan_period_max_ms

# 스캔 크기 (MiB 단위, 기본 256MiB)
echo 256 > /proc/sys/kernel/numa_balancing_scan_size_mb

# NUMA 폴트 통계 확인
grep numa /proc/<PID>/sched
NUMA 밸런싱 트레이드오프: PROT_NONE 프로빙은 의도적인 TLB Miss를 유발하므로 단기적으로 성능을 저하시킬 수 있습니다. 반복적인 마이그레이션이 발생하는 경우(NUMA thrashing) numa_balancing을 비활성화하거나 numactl --membind로 메모리 배치를 명시적으로 고정하는 것이 효과적입니다.

do_anonymous_page() 소스 분석

do_anonymous_page()는 익명(Anonymous) 매핑에서 최초 페이지 접근 시 호출됩니다. 커널은 메모리를 아끼기 위해 제로 페이지(Zero Page) 최적화를 사용합니다. 최초 읽기 접근이면 공유 제로 페이지를 매핑하고, 최초 쓰기 접근이면 새 물리 페이지를 할당하여 0으로 초기화합니다.

/* mm/memory.c — 익명 매핑 폴트 핸들러 */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct folio          *folio;
    pte_t                   entry;
    vm_fault_t              ret = 0;

    /* ① 익명 VMA 역매핑 준비 */
    if (vma_is_anonymous(vma) && unlikely(anon_vma_prepare(vma)))
        return VM_FAULT_OOM;

    /* ② 읽기 폴트: 공유 제로 페이지 매핑 (물리 페이지 미할당) */
    if (!(vmf->flags & FAULT_FLAG_WRITE) &&
        !mm_forbids_zeropage(vma->vm_mm)) {

        entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
                               vma->vm_page_prot));
        vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                                        vmf->address, &vmf->ptl);
        if (!pte_none(*vmf->pte)) {       /* 경쟁: 이미 매핑됨 */
            goto unlock;
        }
        set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return 0;                          /* PF 해결: 물리 메모리 0 bytes 소비 */
    }

    /* ③ 쓰기 폴트: 새 페이지 할당 + 0 초기화 */
    folio = vma_alloc_zeroed_movable_folio(vma, vmf->address);
    if (!folio)
        return VM_FAULT_OOM;

    /* ④ 익명 역매핑(Reverse Mapping) 등록 */
    folio_get(folio);
    folio_add_new_anon_rmap(folio, vma, vmf->address, RMAP_EXCLUSIVE);
    folio_add_lru_vma(folio, vma);          /* LRU 리스트에 추가 */

    /* ⑤ PTE 생성: R/W, Dirty, Young 플래그 설정 */
    entry = mk_pte(&folio->page, vma->vm_page_prot);
    entry = pte_sw_mkyoung(entry);
    if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry), vma);

    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                                    vmf->address, &vmf->ptl);
    if (!pte_none(*vmf->pte)) {           /* 경쟁: 다른 CPU가 먼저 처리 */
        folio_put(folio);
        goto unlock;
    }
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);

unlock:
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return ret;
}
코드 설명
  • ② 읽기 경로FAULT_FLAG_WRITE가 없으면 읽기 폴트입니다. my_zero_pfn()은 현재 NUMA 노드의 공유 제로 페이지 PFN을 반환합니다. pte_mkspecial()은 해당 PTE를 "특수 매핑"으로 표시하여 역매핑 시스템이 이 페이지를 일반 folio로 취급하지 않도록 합니다.
  • ③ 쓰기 경로vma_alloc_zeroed_movable_folio()MIGRATE_MOVABLE 타입의 folio를 할당하고 clear_page()로 0 초기화합니다. MOVABLE 타입은 메모리 압축(Compaction) 시 이동 가능하여 단편화를 줄입니다. 이 함수는 커널 6.1 이후 alloc_page_vma()를 대체했습니다.
  • ④ 역매핑 등록folio_add_new_anon_rmap()은 역매핑 자료구조(anon_vma)를 folio에 연결합니다. 이후 페이지 회수(Reclaim)나 COW 시 해당 PTE를 빠르게 찾을 수 있습니다. RMAP_EXCLUSIVE는 이 folio가 이 VMA에만 매핑됨을 표시합니다.
  • ⑤ PTE 설정pte_sw_mkyoung()은 소프트웨어 Accessed 비트를 설정합니다. 하드웨어가 Accessed 비트를 자동으로 설정하지 않는 아키텍처(일부 RISC-V)를 위한 폴백입니다. 쓰기 VMA라면 pte_mkwrite(pte_mkdirty())로 바로 R/W + Dirty PTE를 만들어 다음 쓰기 폴트를 방지합니다.
익명 매핑 Page Fault 발생 do_anonymous_page() 진입 FAULT_FLAG_WRITE 설정? (쓰기 접근인가?) 아니오 (읽기) 공유 제로 페이지 매핑 my_zero_pfn() + pte_mkspecial() 물리 메모리 추가 소비: 0 bytes 공유 제로 페이지 CPU당 1개 (NUMA 지역성 고려) 수천 개 PTE가 동시에 공유 가능 calloc(1GB): 즉시 반환, 물리 메모리 0 소비 희소 배열, 대형 버퍼에서 효과 극대화 폴트 해결 (return 0) 예 (쓰기) 새 folio 할당 + 0 초기화 vma_alloc_zeroed_movable_folio() MIGRATE_MOVABLE + clear_page() 역매핑 등록 folio_add_new_anon_rmap(RMAP_EXCLUSIVE) PTE 설정 R/W + Dirty + Young 플래그 TLB 업데이트 update_mmu_cache_range() 읽기 / 쓰기 경로 분기 지점

제로 페이지 최적화의 실용적 효과: calloc(1GB, 1) 호출 시 운영체제는 1GB 물리 메모리를 즉시 할당하지 않습니다. 모든 PTE를 공유 제로 페이지로 설정하여 실제 쓰기가 일어나는 페이지만 물리 메모리를 소비합니다. 희소 행렬(Sparse Matrix), 대형 버퍼 사전 할당, 데이터베이스 버퍼 풀 등의 패턴에서 메모리 사용량을 수십 배 줄일 수 있습니다.

do_fault() 파일 매핑 폴트 분석

파일 기반 매핑(File-backed VMA)에서 발생하는 폴트는 do_fault()가 처리합니다. do_fault()는 폴트 종류에 따라 세 가지 하위 핸들러로 분기합니다.

/* mm/memory.c — 파일 매핑 폴트 디스패처 */
static vm_fault_t do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct        *vma     = vmf->vma;
    struct vm_operations_struct  *vma_ops = vma->vm_ops;
    vm_fault_t                    ret;

    if (!vma_ops->fault)
        return VM_FAULT_SIGBUS;    /* vm_ops 없는 특수 매핑 */

    if (!(vmf->flags & FAULT_FLAG_WRITE)) {
        /* ① 읽기 폴트: 페이지 캐시에서 읽어 PTE 설정 */
        ret = do_read_fault(vmf);
    } else if (!(vma->vm_flags & VM_SHARED)) {
        /* ② 쓰기 폴트 + private 매핑: COW 복사 후 새 PTE */
        ret = do_cow_fault(vmf);
    } else {
        /* ③ 쓰기 폴트 + shared 매핑: 캐시 페이지를 Dirty로 직접 설정 */
        ret = do_shared_fault(vmf);
    }
    return ret;
}

/* ① 읽기 폴트 경로 — do_fault_around() + filemap_fault() 위임 */
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
    vm_fault_t ret = 0;

    /* 선행 매핑(Fault-around): 주변 페이지 64KB 미리 PTE 설정 */
    if (vmf->vma->vm_ops->map_pages &&
        fault_around_bytes() != PAGE_SIZE) {
        if (likely(!(vmf->flags & FAULT_FLAG_TRIED)))
            ret = do_fault_around(vmf);
        if (ret)
            return ret;
    }
    /* vma->vm_ops->fault() 호출 → filemap_fault() */
    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        return ret;

    finish_fault(vmf);    /* set_pte_at() + update_mmu_cache_range() */
    return ret;
}

/* mm/filemap.c — 페이지 캐시 조회 및 리드어헤드 */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    struct file          *file    = vmf->vma->vm_file;
    struct address_space *mapping = file->f_mapping;
    pgoff_t               offset  = vmf->pgoff;
    struct folio         *folio;
    vm_fault_t            ret = 0;

    /* ① XArray(페이지 캐시 트리)에서 folio 조회 */
    folio = filemap_get_folio(mapping, offset);

    if (IS_ERR(folio)) {
        /* 캐시 미스: 동기 리드어헤드로 디스크에서 읽기 */
        page_cache_sync_readahead(mapping,
            &file->f_ra, file, offset,
            ra_pages(mapping));    /* ② 동기 리드어헤드 (블로킹) */
        folio = filemap_get_folio(mapping, offset);
        if (IS_ERR(folio)) {
            ret = VM_FAULT_OOM;
            goto out;
        }
    }

    /* 리드어헤드 윈도우 끝 마커 감지: 다음 윈도우 비동기 로드 */
    if (PageReadahead(&folio->page))
        page_cache_async_readahead(mapping,
            &file->f_ra, file, folio, offset,
            ra_pages(mapping));    /* ③ 비동기 리드어헤드 (논블로킹) */

    /* 페이지가 최신(Uptodate) 상태인지 확인 후 블록 I/O */
    if (unlikely(!folio_test_uptodate(folio))) {
        folio_lock(folio);
        if (!folio_test_uptodate(folio))
            ret = filemap_read_folio(file, file->f_op->read_iter, folio);
        folio_unlock(folio);
    }

    vmf->page = &folio->page;    /* vm_fault에 페이지 연결 후 반환 */
out:
    return ret;
}
코드 설명
  • do_fault() ① 읽기do_read_fault()는 먼저 do_fault_around()로 폴트 주소 주변 최대 64KB를 미리 매핑합니다(fault_around_bytes 기본값). 이는 mmap된 파일을 순차 접근할 때 폴트 횟수를 크게 줄입니다. 이 값은 /proc/sys/vm/fault_around_bytes로 조정 가능합니다.
  • do_fault() ② private 쓰기do_cow_fault()는 페이지 캐시에서 원본 페이지를 읽은 뒤 새 익명 페이지에 복사하고 그 새 페이지를 PTE에 쓰기 가능으로 연결합니다. 수정 내용은 파일에 반영되지 않습니다.
  • do_fault() ③ shared 쓰기do_shared_fault()는 페이지 캐시 folio에 직접 쓰기 권한을 부여합니다. 수정된 내용은 dirty 페이지로 표시되어 나중에 writeback 스레드가 파일시스템에 반영합니다. mmap(MAP_SHARED) 쓰기의 동작입니다.
  • filemap_fault() ②page_cache_sync_readahead()는 현재 스레드가 블로킹되어 최소 1개 페이지를 읽어들입니다. file_ra_state(f_ra)가 리드어헤드 윈도우 크기를 동적으로 조절합니다. 첫 접근 시 4 페이지(16KB), 순차 접근 감지 시 최대 512 페이지(2MB)까지 확장됩니다.
  • filemap_fault() ③PageReadahead 플래그는 리드어헤드 윈도우의 마지막 페이지에 설정됩니다. 이 페이지에 접근하면 page_cache_async_readahead()로 다음 윈도우를 백그라운드에서 미리 로드합니다. 현재 폴트 처리는 블로킹되지 않습니다.
  • TLB 연동finish_fault() 내부에서 set_pte_at()으로 PTE를 설정한 뒤 update_mmu_cache_range()를 호출합니다. ARM64에서는 DSB + ISB 배리어로 TLB 일관성을 보장하고, x86에서는 새 PTE 설정 후 TLB가 자동으로 갱신됩니다.
파일 매핑 Page Fault 발생 VMA: vm_ops->fault = filemap_fault do_fault() 분기 FAULT_FLAG_WRITE + VM_SHARED 조합으로 결정 읽기 폴트 do_read_fault() 쓰기+private do_cow_fault() 쓰기+shared do_shared_fault() filemap_fault() — 페이지 캐시 조회 filemap_get_folio(mapping, pgoff) via XArray 페이지 캐시 히트? 캐시 히트 folio 즉시 반환 비동기 리드어헤드 확인 캐시 미스 동기 리드어헤드 page_cache_sync_readahead() 블록 I/O 요청 파일시스템 → 디스크 읽기 finish_fault() set_pte_at() + update_mmu_cache_range() → TLB 리드어헤드 윈도우: 첫 접근 16KB, 순차 감지 시 최대 2MB 비동기 경로는 현재 폴트를 블로킹하지 않음

COW (Copy-on-Write)

fork() 시 부모와 자식 프로세스의 페이지 테이블은 같은 물리 페이지를 가리키되, PTE를 읽기 전용(Read-Only)으로 표시합니다. 쓰기가 발생하면 페이지 폴트를 통해 복사가 이루어집니다. 이를 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;
}

커널 소스 분석: 폴트 처리 콜 체인

페이지 폴트 처리의 핵심 콜 체인인 do_page_fault() → handle_mm_fault() → handle_pte_fault() → do_wp_page()를 커널 소스 레벨에서 단계별로 분석합니다. 각 함수의 역할 분담과 인자 전달 방식을 이해하면 메모리 관련 버그를 정확히 진단할 수 있습니다.

콜 체인 전체 구조

페이지 폴트 콜 체인 (mm/memory.c) exc_page_fault() / do_page_fault() arch/x86/mm/fault.c — CR2 읽기, error_code 파싱 vmf 초기화 handle_mm_fault() mm/memory.c — mmap_lock 보유, 통계 갱신 __handle_mm_fault() 호출 __handle_mm_fault() → handle_pte_fault() PGD→P4D→PUD→PMD 워크/할당 후 PTE 레벨 폴트 종류 판별 폴트 종류 분기 do_anonymous_page() 익명 페이지 할당 do_fault() 파일 매핑 폴트 do_wp_page() COW 처리 (핵심) do_swap_page() 스왑 복원 refcount == 1? wp_page_reuse() PTE R/W 복원만 Yes wp_page_copy() 새 페이지 복사 No
그림: 페이지 폴트 콜 체인 — exc_page_fault()에서 do_wp_page()까지의 전체 경로 (mm/memory.c)

handle_pte_fault() 소스 분석

/* mm/memory.c */
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
    pte_t entry;

    if (unlikely(pmd_none(*vmf->pmd))) {
        /* PMD가 없으면 PTE 테이블 자체가 아직 없음 */
        vmf->pte = NULL;
        vmf->flags &= ~FAULT_FLAG_WRITE;
    } else {
        /* PTElock 없이 orig_pte 스냅샷 획득 */
        vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
        vmf->orig_pte = ptep_get_lockless(vmf->pte);

        if (pte_none(vmf->orig_pte)) {
            pte_unmap(vmf->pte);
            vmf->pte = NULL;
        }
    }

    if (!vmf->pte) {
        /* PTE 없음: 익명 페이지 또는 파일 매핑으로 분기 */
        if (vma_is_anonymous(vmf->vma))
            return do_anonymous_page(vmf);
        else
            return do_fault(vmf);
    }

    if (!pte_present(vmf->orig_pte))
        return do_swap_page(vmf);  /* 스왑 아웃된 페이지 */

    if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
        return do_numa_page(vmf);  /* NUMA 밸런싱 폴트 */

    /* PTElock 획득 후 재확인 (race 방지) */
    vmf_pte_lock(vmf);
    entry = vmf_orig_pte_dup(vmf);

    if (unlikely(!pte_same(entry, vmf->orig_pte))) {
        /* 락 사이에 다른 CPU가 PTE를 변경함 */
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return 0;
    }

    if (vmf->flags & (FAULT_FLAG_WRITE | FAULT_FLAG_UNSHARE)) {
        if (!pte_write(entry))
            return do_wp_page(vmf);  /* COW: 쓰기 보호 해제 */
        entry = pte_mkdirty(entry);
    }

    entry = pte_mkyoung(entry);    /* Accessed 비트 설정 */
    if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte,
                               entry, vmf->flags & FAULT_FLAG_WRITE)) {
        update_mmu_cache_range(vmf, vmf->vma, vmf->address,
                               vmf->pte, 1);
    }
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return 0;
}
코드 설명
  • 3행pmd_none() 체크: PMD 엔트리가 아직 없으면 PTE 테이블 자체가 존재하지 않으므로 vmf->pte = NULL로 설정하고 익명/파일 폴트 경로로 진입합니다.
  • 9행ptep_get_lockless(): 락 없이 원자적으로 PTE 값을 읽습니다. 이 스냅샷은 orig_pte에 저장되어 이후 경쟁 조건 검사에 사용됩니다.
  • 20행vma_is_anonymous()로 분기: 익명 VMA면 do_anonymous_page()(제로 페이지 또는 새 페이지 할당), 파일 매핑이면 do_fault()(filemap_fault 경유 읽기)로 처리합니다.
  • 26행pte_present()가 거짓이면 스왑 아웃 상태입니다. do_swap_page()가 스왑 파티션에서 페이지를 읽어 다시 매핑합니다.
  • 29행pte_protnone(): NUMA 밸런싱을 위해 PTE를 의도적으로 접근 불가로 표시한 경우입니다. do_numa_page()가 더 가까운 NUMA 노드로 페이지를 마이그레이션합니다.
  • 33행vmf_pte_lock()으로 PTElock(스핀락)을 획득한 뒤 PTE 값을 다시 읽어 orig_pte와 비교합니다. 락을 잡는 사이 다른 CPU가 PTE를 이미 변경했다면 0을 반환하고 폴트를 재시도하게 합니다.
  • 41행쓰기 폴트(FAULT_FLAG_WRITE)이고 현재 PTE가 쓰기 불가(!pte_write())이면 do_wp_page()를 호출하여 COW를 처리합니다. 이것이 콜 체인의 핵심 분기점입니다.
  • 46행pte_mkyoung()은 하드웨어 Accessed 비트를 소프트웨어적으로 설정합니다. ptep_set_access_flags()가 TLB를 업데이트하며, 변경이 있었으면 update_mmu_cache_range()로 아키텍처별 캐시 동기화를 수행합니다.

do_wp_page() 분석

/* mm/memory.c — COW 핵심 함수 */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
    struct vm_area_struct *vma = vmf->vma;
    struct folio *folio = NULL;
    pte_t pte;

    if (userfaultfd_pte_wp(vma, vmf_orig_pte_dup(vmf))) {
        /* userfaultfd WP 이벤트: 사용자 공간 폴트 핸들러에 위임 */
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return handle_userfault(vmf, VM_UFFD_WP);
    }

    vmf->page = vm_normal_page(vma, vmf->address, vmf_orig_pte_dup(vmf));
    if (!vmf->page) {
        /* 특수 매핑 (pfnmap, zero page): 단순 복사 */
        if (unshare) {
            pte_unmap_unlock(vmf->pte, vmf->ptl);
            return 0;
        }
        return wp_page_copy(vmf);
    }

    folio = page_folio(vmf->page);

    if (folio_is_zone_device(folio)) {
        /* DAX / HMEM 디바이스 메모리: 항상 복사 */
        folio_get(folio);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return wp_page_copy(vmf);
    }

    folio_lock(folio);
    if (folio_test_anon(folio) &&
        folio_test_large(folio) &&
        !PageAnonExclusive(vmf->page)) {
        /* 대형 익명 folio에서 단일 페이지 분리 필요 */
        folio_unlock(folio);
        folio_get(folio);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        if (folio_referenced(folio, 0, vma->vm_mm, NULL) != 0)
            return VM_FAULT_RETRY;
        return wp_page_copy(vmf);
    }

    /* 단독 참조(folio_ref_count == 1) + 익명 + 비-KSM: reuse */
    if (PageAnonExclusive(vmf->page)) {
        wp_page_reuse(vmf, folio);
        return 0;
    }

    folio_unlock(folio);
    return wp_page_copy(vmf);
}
코드 설명
  • 9행userfaultfd_pte_wp(): userfaultfd(UFFD) WP 모드가 활성화된 VMA이면 COW 처리를 사용자 공간의 폴트 핸들러에 위임합니다. 가상화 환경의 실시간 마이그레이션(live migration)에서 핵심적으로 활용됩니다.
  • 14행vm_normal_page(): PTE에서 일반 struct page 포인터를 반환합니다. pfnmap(디바이스 메모리 직접 매핑)이나 zero page 같은 특수 매핑은 NULL을 반환하며, 이 경우 복사로 직행합니다.
  • 24행page_folio(): 커널 6.x부터 도입된 folio(연속 페이지 묶음) 인터페이스로 변환합니다. folio는 THP(Transparent Huge Page) 같은 대형 페이지도 단일 단위로 관리합니다.
  • 27행Zone device(DAX, HMEM) 메모리는 참조 카운트 기반 최적화를 적용할 수 없으므로 항상 wp_page_copy()를 호출합니다. folio_get()으로 락을 해제하기 전에 참조를 먼저 증가시킵니다.
  • 34행대형(large) 익명 folio에서 특정 페이지만 COW가 발생했을 때의 처리입니다. PageAnonExclusive가 설정되지 않은 경우 folio를 분리해야 하므로 wp_page_copy()로 이동합니다.
  • 46행PageAnonExclusive: 이 페이지가 현재 단일 프로세스에만 독점적으로 매핑되어 있음을 의미하는 플래그입니다. 설정되어 있으면 복사 없이 wp_page_reuse()로 PTE의 쓰기 보호만 해제합니다. 이것이 가장 빠른 COW 경로입니다.

flush_tlb_mm_range() 소스 분석

COW 완료 후에는 반드시 TLB를 무효화해야 합니다. flush_tlb_page()는 내부적으로 flush_tlb_mm_range()를 호출하며, SMP 환경에서는 TLB Shootdown IPI를 발생시킵니다.

/* arch/x86/mm/tlb.c */
void flush_tlb_mm_range(struct mm_struct *mm,
                        unsigned long start, unsigned long end,
                        unsigned int stride_shift, bool freed_tables)
{
    struct flush_tlb_info *info;
    u64 new_tlb_gen;
    int cpu = get_cpu();

    /* 로컬 CPU 정보 설정 */
    info = get_flush_tlb_info(mm, start, end, stride_shift,
                              freed_tables, TLB_GENERATION_INVALID);

    /* mm이 현재 CPU에서 활성화된 경우 로컬 플러시 */
    if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {
        /* 다른 CPU들에 TLB Shootdown IPI 전송 */
        inc_irq_stat(irq_tlb_count);
        on_each_cpu_mask(mm_cpumask(mm), flush_tlb_func, info, 1);
    }

    if (this_cpu_read(cpu_tlbstate.loaded_mm) == mm) {
        flush_tlb_func(info);  /* 로컬 CPU TLB 직접 플러시 */
    }

    put_flush_tlb_info();
    put_cpu();
}

/* 단일 페이지 flush: flush_tlb_page() 래퍼 */
void flush_tlb_page(const struct vm_area_struct *vma, unsigned long start)
{
    flush_tlb_mm_range(vma->vm_mm, start, start + PAGE_SIZE,
                       PAGE_SHIFT, false);
}

/* TLB Shootdown IPI 실제 처리 함수 */
static void flush_tlb_func(void *info)
{
    const struct flush_tlb_info *f = info;

    if (f->end == TLB_FLUSH_ALL) {
        local_flush_tlb_one_user(f->start); /* INVLPG 또는 invlpgb */
    } else if ((f->end - f->start) <= PAGE_SIZE << f->stride_shift) {
        local_flush_tlb_one_user(f->start);
    } else {
        /* 범위가 넓으면 전체 TLB 플러시 (CR3 재로드) */
        local_flush_tlb();
    }
}
코드 설명
  • 1행flush_tlb_mm_range()는 x86-64의 TLB 무효화 핵심 함수입니다. start~end 범위의 가상 주소에 대한 TLB 엔트리를 무효화하며, stride_shift로 일반 4KB 페이지와 Huge Page를 구분합니다.
  • 10행get_flush_tlb_info(): per-CPU 변수에서 flush 요청 정보 구조체를 가져옵니다. 이 구조체는 flush 범위와 TLB 세대(generation) 번호를 담아 불필요한 중복 flush를 방지합니다.
  • 14행mm_cpumask(mm): 해당 mm을 현재 사용 중인 CPU 집합을 반환합니다. 현재 CPU 이외에 사용 중인 CPU가 있으면 IPI(프로세서 간 인터럽트)로 TLB Shootdown을 전파합니다.
  • 17행on_each_cpu_mask(): 대상 CPU 집합의 모든 CPU에 IPI를 보내 flush_tlb_func를 동기적으로 실행합니다. 마지막 인자 1은 완료를 기다림(synchronous)을 의미합니다.
  • 20행로컬 CPU가 대상 mm을 현재 실행 중이면 IPI 없이 직접 flush_tlb_func()를 호출합니다. 멀티코어에서 IPI 오버헤드를 피하는 최적화입니다.
  • 37행x86에서 단일 페이지 무효화는 INVLPG 명령어로 수행합니다(local_flush_tlb_one_user()). 무효화 범위가 넓으면 INVLPG를 반복하는 것보다 CR3 재로드(전체 TLB flush)가 더 빠릅니다.

mm_struct 핵심 필드 한국어 주석

mm_struct는 프로세스의 가상 주소 공간 전체를 표현하는 최상위 자료구조입니다. task_structmm 필드가 이 구조체를 가리킵니다.

/* include/linux/mm_types.h */
struct mm_struct {
    struct {
        struct maple_tree   mm_mt;         /* VMA 트리 (Maple Tree, 6.1+) */
        unsigned long        mmap_base;     /* mmap 영역 시작 주소 */
        unsigned long        mmap_legacy_base; /* 레거시 mmap 배치 기준 */
        unsigned long        task_size;     /* 사용자 공간 최대 크기 */
        pgd_t               *pgd;           /* 프로세스 페이지 글로벌 디렉터리 */

        atomic_t             mm_users;      /* 이 mm을 사용하는 스레드 수 */
        atomic_t             mm_count;      /* 참조 카운트 (마지막 해제 시 파괴) */

        atomic_long_t        pgtables_bytes; /* 페이지 테이블이 차지하는 바이트 */
        int                  map_count;     /* VMA 개수 (RLIMIT_AS 검사용) */

        spinlock_t           page_table_lock; /* PMD/PUD 레벨 락 (PTElock은 별도) */
        struct rw_semaphore  mmap_lock;    /* VMA 트리 읽기/쓰기 세마포어 */

        struct list_head     mmlist;       /* init_mm.mmlist 연결 리스트 */

        unsigned long        hiwater_rss;  /* RSS 최고수위 (KB 단위 통계용) */
        unsigned long        hiwater_vm;   /* 가상 메모리 최고수위 (페이지 수) */

        unsigned long        total_vm;     /* 전체 매핑된 가상 페이지 수 */
        unsigned long        locked_vm;    /* mlock()으로 잠긴 페이지 수 */
        atomic64_t           pinned_vm;    /* GUP으로 핀된 페이지 수 */
        unsigned long        data_vm;      /* 데이터 세그먼트 페이지 수 */
        unsigned long        exec_vm;      /* 실행 가능 페이지 수 */
        unsigned long        stack_vm;     /* 스택 페이지 수 */

        unsigned long        def_flags;    /* 새 VMA 기본 플래그 */

        /**
         * 세그먼트 경계 주소들
         */
        unsigned long        start_code;   /* 코드 세그먼트 시작 주소 */
        unsigned long        end_code;     /* 코드 세그먼트 끝 주소 */
        unsigned long        start_data;   /* 데이터 세그먼트 시작 주소 */
        unsigned long        end_data;     /* 데이터 세그먼트 끝 주소 */
        unsigned long        start_brk;    /* 힙 시작 주소 */
        unsigned long        brk;          /* 현재 힙 끝 주소 (brk() 시스템 콜로 확장) */
        unsigned long        start_stack;  /* 스택 시작(최상단) 주소 */
        unsigned long        arg_start;    /* 프로그램 인자(argv) 시작 */
        unsigned long        arg_end;      /* 프로그램 인자 끝 */
        unsigned long        env_start;    /* 환경변수(envp) 시작 */
        unsigned long        env_end;      /* 환경변수 끝 */

        unsigned long        saved_auxv[AT_VECTOR_SIZE]; /* ELF auxiliary vector */

        struct mm_rss_stat   rss_stat;     /* RSS 통계: 익명/파일/스왑 구분 */

        struct linux_binfmt  *binfmt;      /* 실행 파일 포맷 핸들러 */

        mm_context_t         context;      /* 아키텍처별 문맥 (x86: PCID/LDT, ARM64: ASID) */

        unsigned long        flags;        /* 상태 플래그 (MMF_DUMP_*, MMF_OOM_* 등) */

        struct user_namespace *user_ns;   /* 프로세스 소속 사용자 네임스페이스 */
        struct file          *exe_file;    /* 실행 파일 /proc/pid/exe 참조 */

        struct mmu_notifier_subscriptions *notifier_subscriptions;
        /* MMU 변경 알림 구독자 목록 (KVM, HMM 등이 등록) */
    } __randomize_layout;  /* 구조체 레이아웃 KASLR 랜덤화 */

    /* 멀티스레드 접근이 없는 필드: 별도 캐시라인 배치 */
    unsigned long          cpu_bitmap[];  /* 이 mm이 활성화된 CPU 비트맵 (가변 크기) */
};
코드 설명
  • 4행mm_mt(Maple Tree): 리눅스 6.1부터 기존 레드-블랙 트리를 대체한 VMA 저장소입니다. 캐시 친화적인 B-tree 계열로 범위 검색과 삽입이 빠릅니다.
  • 8행pgd: 이 프로세스의 페이지 테이블 루트입니다. x86-64에서는 CR3 레지스터에 로드됩니다. context_switch() 시 이 값이 CR3에 쓰여 주소 공간이 전환됩니다.
  • 10행mm_users는 이 주소 공간을 공유하는 스레드 수(스레드 그룹 크기)입니다. mm_count는 모든 종류의 참조를 포함하며 0이 되면 mm_struct를 해제합니다.
  • 15행page_table_lock은 PUD/PMD 레벨 테이블을 보호하는 스핀락입니다. PTE 레벨은 별도의 PTElock(페이지 구조체 embedded)을 사용하여 락 경합을 줄입니다.
  • 16행mmap_lock은 VMA 트리 전체를 보호하는 읽기/쓰기 세마포어입니다. mmap(), munmap(), fork() 등은 쓰기 락을, 페이지 폴트는 읽기 락을 잡습니다. 멀티스레드 환경에서 경합이 심한 핫스팟입니다.
  • 23~28행가상 메모리 사용량 통계 필드들입니다. total_vm/proc/pid/statusVmSize, locked_vmVmLck, pinned_vmVmPin에 대응합니다.
  • 34~44행프로세스 메모리 맵의 세그먼트 경계 주소들입니다. /proc/pid/maps/proc/pid/stat에서 확인할 수 있으며, brk() 시스템 콜은 brk 필드를 늘려 힙을 확장합니다.
  • 52행context는 아키텍처별로 다릅니다. x86-64에서는 PCID(Process Context ID)와 LDT 포인터를 담고, ARM64에서는 ASID(Address Space ID)를 담아 Context Switch 시 TLB 플러시를 최소화합니다.
  • 57행notifier_subscriptions: MMU가 페이지 매핑을 변경할 때 KVM, HMM(Heterogeneous Memory Management), RDMA 등의 하위 시스템에 알림을 보내는 콜백 목록입니다.
  • 62행__randomize_layout: 컴파일 타임에 구조체 필드 순서를 무작위화하는 KASLR 보안 기법을 적용합니다. 구조체 오프셋을 이용한 익스플로잇을 어렵게 만듭니다.

pte_t 및 관련 헬퍼 구조체 분석

/* arch/x86/include/asm/pgtable_types.h */
typedef struct { pteval_t pte; } pte_t;  /* x86-64: 64비트 PTE 래퍼 */

/* 주요 PTE 비트 상수 */
#define _PAGE_PRESENT   0x001  /* bit 0: RAM에 존재 */
#define _PAGE_RW        0x002  /* bit 1: 쓰기 가능 */
#define _PAGE_USER      0x004  /* bit 2: 사용자 접근 가능 */
#define _PAGE_PWT       0x008  /* bit 3: Page Write-Through */
#define _PAGE_PCD       0x010  /* bit 4: Page Cache Disable */
#define _PAGE_ACCESSED  0x020  /* bit 5: 하드웨어 Accessed (LRU용) */
#define _PAGE_DIRTY     0x040  /* bit 6: 하드웨어 Dirty (writeback용) */
#define _PAGE_PSE       0x080  /* bit 7: 큰 페이지 (PMD=2MB, PUD=1GB) */
#define _PAGE_GLOBAL    0x100  /* bit 8: Context Switch 후에도 TLB 유지 */
#define _PAGE_SOFTW1    0x200  /* bit 9: 소프트웨어 정의 플래그 1 */
#define _PAGE_SOFTW2    0x400  /* bit 10: 소프트웨어 정의 플래그 2 */
#define _PAGE_SOFTW3    0x800  /* bit 11: 소프트웨어 정의 플래그 3 */
#define _PAGE_NX       (1ULL << 63) /* bit 63: No-eXecute (DEP/W^X) */

/* 커널이 소프트웨어 비트를 사용하는 예 */
#define _PAGE_UFFD_WP   _PAGE_SOFTW2  /* userfaultfd 쓰기 보호 마커 */
#define _PAGE_SAVED_DIRTY _PAGE_SOFTW3 /* 소프트-Dirty 트래킹 (/proc/pid/pagemap) */

/* COW에서 자주 쓰이는 헬퍼 */
static inline pte_t pte_mkwrite(pte_t pte)
{
    return pte_set_flags(pte, _PAGE_RW);   /* R/W 비트 설정 → 쓰기 가능 */
}

static inline pte_t pte_wrprotect(pte_t pte)
{
    return pte_clear_flags(pte, _PAGE_RW); /* R/W 비트 해제 → COW 트리거 준비 */
}

static inline bool pte_write(pte_t pte)
{
    return pte_flags(pte) & _PAGE_RW;     /* 쓰기 가능 여부 검사 */
}
코드 설명
  • 2행pte_t는 단순히 64비트 정수를 감싸는 래퍼 구조체입니다. 타입 안전성을 위해 unsigned long을 직접 쓰지 않고 구조체로 감쌉니다. pte_val(pte)로 원시 값을 꺼냅니다.
  • 4~16행PTE 비트 상수들입니다. 하드웨어가 직접 해석하는 비트(0~8)와 소프트웨어용 예약 비트(9~11, 52~62)가 구분됩니다. _PAGE_ACCESSED_PAGE_DIRTY는 하드웨어가 자동으로 설정하지만 커널도 소프트웨어적으로 조작합니다.
  • 19~20행커널이 소프트웨어 예약 비트를 실제로 활용하는 예입니다. _PAGE_UFFD_WP는 userfaultfd write-protect 마커이고, _PAGE_SAVED_DIRTY/proc/pid/pagemap의 soft-dirty 트래킹에 사용됩니다.
  • 23~31행pte_mkwrite()pte_wrprotect()는 COW의 핵심 조작입니다. fork() 시에는 pte_wrprotect()로 부모·자식 모두의 PTE에서 _PAGE_RW를 제거하고, COW 폴트 처리 완료 후 pte_mkwrite()로 복원합니다.
  • 33~35행pte_write()handle_pte_fault()에서 쓰기 폴트 시 do_wp_page() 호출 여부를 결정하는 핵심 검사입니다. !pte_write(entry)가 참이면 COW 경로로 진입합니다.

vm_fault 구조체 — 폴트 처리 컨텍스트

/* include/linux/mm_types.h */
struct vm_fault {
    const struct vm_fault_flags flags;  /* 폴트 플래그 (FAULT_FLAG_WRITE 등) */
    pgoff_t                       pgoff;  /* 파일 매핑 오프셋 (페이지 단위) */
    unsigned long                 address; /* 폴트가 발생한 가상 주소 */
    unsigned long                 real_address; /* 정렬 전 실제 접근 주소 */
    struct vm_area_struct        *vma;    /* 폴트 발생 VMA */
    pte_t                        *pte;    /* 폴트 PTE 포인터 (락 이후 유효) */
    pte_t                         orig_pte; /* 락 전 스냅샷 (경합 검사용) */
    struct page                  *page;   /* 폴트 처리에 사용할 struct page */
    struct folio                 *folio;  /* page가 속한 folio (6.x+) */
    pmd_t                        *pmd;    /* 상위 PMD 포인터 */
    pud_t                        *pud;    /* 상위 PUD 포인터 */
    spinlock_t                   *ptl;    /* PTE 레벨 스핀락 포인터 */
    pgtable_t                     prealloc_pte; /* 사전 할당 PTE 테이블 */
};
코드 설명
  • 3행flags: FAULT_FLAG_WRITE(쓰기 폴트), FAULT_FLAG_USER(사용자 공간에서), FAULT_FLAG_REMOTE(다른 프로세스 mm), FAULT_FLAG_UNSHARE(KSM 비공유화) 등의 비트 플래그입니다.
  • 5행address: PAGE_MASK로 정렬된 폴트 페이지의 시작 주소입니다. 실제 접근 주소(real_address)와 구별됩니다.
  • 8행orig_pte: 스핀락 없이 읽은 PTE의 스냅샷입니다. 락 획득 후 현재 PTE와 비교하여 그 사이에 다른 CPU가 변경했는지 확인합니다(ABA 문제 방지).
  • 14행ptl: PTElock 스핀락 포인터입니다. PTE 페이지 프레임에 임베드된 락으로, pte_lockptr()로 획득합니다. pte_unmap_unlock(vmf->pte, vmf->ptl)로 PTE 매핑 해제와 동시에 락을 풉니다.
  • 15행prealloc_pte: 슬랩에서 미리 할당해둔 PTE 테이블입니다. 재귀적 락 획득 없이 PTE 테이블을 안전하게 추가할 수 있도록 폴트 핸들러 진입 전에 미리 준비합니다.

per-VMA Lock (커널 6.1+)

커널 6.1에서 도입된 per-VMA Lock은 페이지 폴트 처리의 핵심 병목인 mmap_lock 경합을 해소하는 기법입니다. 기존에는 모든 페이지 폴트가 mmap_read_lock()으로 전체 주소 공간을 직렬화했지만, per-VMA Lock은 개별 VMA 단위의 RCU 기반 읽기 락을 사용하여 병렬 폴트 처리를 가능하게 합니다.

mmap_lock 병목 문제

멀티스레드 프로세스에서 여러 스레드가 동시에 서로 다른 VMA 영역에서 페이지 폴트를 발생시키면, 모두 같은 mmap_read_lock()을 두고 경합합니다. 스레드 수가 늘어날수록 대기 시간이 급증하여 성능이 선형적으로 저하됩니다.

기존: mmap_lock (직렬화) mmap_read_lock(mm) 전체 주소 공간 단일 락 스레드 1 (처리 중) VMA-A 폴트 처리 스레드 2 (락 대기) VMA-B 폴트 대기 스레드 3 (락 대기) VMA-C 폴트 대기 스레드 4 (락 대기) VMA-D 폴트 대기 성능 특성 스레드 N개 → 락 경합 N-1개 대기 처리량: 스레드 수 증가 시 오히려 감소 CONFIG_PER_VMA_LOCK=n (기본값: 미설정) 병목: 모든 폴트가 단일 락에 직렬화 멀티스레드 서버에서 특히 심각 커널 6.1+: per-VMA Lock (병렬) lock_vma_under_rcu() VMA별 개별 락 + RCU 보호 스레드 1 (처리 중) VMA-A 폴트 처리 스레드 2 (동시 처리!) VMA-B 폴트 처리 스레드 3 (동시 처리!) VMA-C 폴트 처리 스레드 4 (동시 처리!) VMA-D 폴트 처리 성능 특성 서로 다른 VMA → 락 경합 없음 처리량: 스레드 수에 비례하여 증가 CONFIG_PER_VMA_LOCK=y (커널 6.1+ 권장) 실패 시 mmap_lock 폴백 처리 캐시 집약적 서버 워크로드에서 효과 극대

lock_vma_under_rcu() 동작 원리

lock_vma_under_rcu()mmap_lock 없이 VMA를 안전하게 잠그는 핵심 함수입니다. RCU 읽기 락으로 VMA 포인터를 안전하게 조회한 뒤, VMA 자체의 읽기 세마포어(vm_lock)를 획득합니다.

/* mm/memory.c — per-VMA Lock 기반 폴트 처리 경로 (커널 6.1+) */

/* struct vm_area_struct 내부의 per-VMA 락 (CONFIG_PER_VMA_LOCK=y) */
struct vm_area_struct {
    /* ... 기존 필드 ... */
#ifdef CONFIG_PER_VMA_LOCK
    int               vm_lock_seq;    /* 시퀀스 번호 (mmap_lock 일관성 검증) */
    struct vma_lock  *vm_lock;        /* per-VMA 읽기/쓰기 세마포어 */
#endif
};

/* per-VMA Lock으로 VMA를 잠그는 함수 */
static inline struct vm_area_struct *
lock_vma_under_rcu(struct mm_struct *mm, unsigned long address)
{
    struct vm_area_struct *vma;

    /* ① RCU 읽기 락: VMA 트리 순회를 안전하게 */
    rcu_read_lock();
    vma = find_vma_rcu(mm, address);     /* GC 보호 하에 VMA 탐색 */
    if (!vma)
        goto inval;

    /* ② VMA 유효성 검증: 이 VMA가 아직 유효한가? */
    if (!vma_is_accessible(vma))
        goto inval;

    /* ③ per-VMA 읽기 락 획득 */
    if (vma_read_trylock(vma)) {
        rcu_read_unlock();

        /* ④ 시퀀스 번호로 mmap_lock과의 일관성 검증 */
        if (unlikely(vma->vm_lock_seq != READ_ONCE(mm->mm_lock_seq))) {
            vma_read_unlock(vma);
            return NULL;    /* mmap_lock 폴백 트리거 */
        }
        return vma;         /* 성공: per-VMA Lock 보유 중 */
    }

inval:
    rcu_read_unlock();
    return NULL;            /* 실패: 호출자가 mmap_lock으로 재시도 */
}

/* 페이지 폴트 메인 경로: per-VMA Lock 우선 시도 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
                            unsigned long address,
                            unsigned int flags,
                            struct pt_regs *regs)
{
    struct mm_struct *mm = vma->vm_mm;
    vm_fault_t ret;

#ifdef CONFIG_PER_VMA_LOCK
    if (!(flags & FAULT_FLAG_VMA_LOCK)) {
        vma = lock_vma_under_rcu(mm, address);
        if (vma) {
            /* 빠른 경로: mmap_lock 없이 폴트 처리 */
            ret = __handle_mm_fault(vma, address,
                                    flags | FAULT_FLAG_VMA_LOCK);
            vma_read_unlock(vma);
            if (!(ret & VM_FAULT_RETRY))
                return ret;
        }
    }
#endif

    /* 느린 경로: mmap_lock 필요 (VMA 변경, 스택 확장 등) */
    mmap_read_lock(mm);
    ret = __handle_mm_fault(vma, address, flags);
    mmap_read_unlock(mm);
    return ret;
}
코드 설명
  • ① RCU 읽기 락rcu_read_lock()은 락이 아닌 선점 비활성화입니다. RCU 보호 아래 VMA 트리를 순회하여 포인터를 얻습니다. VMA 객체는 RCU 유예 기간(Grace Period)이 지날 때까지 메모리에서 해제되지 않으므로 안전합니다.
  • ③ vma_read_trylock()vma_read_trylock()vm_lock의 읽기 세마포어를 논블로킹으로 획득합니다. 실패하면 NULL을 반환하여 호출자가 mmap_lock으로 폴백합니다. 같은 VMA에 여러 스레드가 동시에 읽기 락을 획득할 수 있습니다.
  • ④ 시퀀스 번호 검증vm_lock_seqmm->mm_lock_seq를 비교합니다. mmap_write_lock()mm_lock_seq를 증가시키므로, VMA가 수정되었다면 두 값이 달라집니다. 이 경우 mmap_lock으로 폴백하여 최신 VMA로 재시도합니다.
  • FAULT_FLAG_VMA_LOCK이 플래그가 설정된 폴트는 이미 per-VMA Lock 보호 하에 있습니다. 재귀 시도를 방지하고, mmap_lock 폴백이 필요한 경우(스택 확장, VMA 병합 등)를 구분하는 데 사용됩니다.
  • mmap_lock 폴백per-VMA Lock이 실패하거나 VM_FAULT_RETRY가 반환되면 전통적인 mmap_read_lock() 경로로 폴백합니다. VMA 수정(mremap, mprotect), 스택 확장, anon_vma 초기화 등은 여전히 mmap_lock이 필요합니다.

성능 개선 데이터

per-VMA Lock은 서로 다른 VMA를 동시에 접근하는 멀티스레드 워크로드에서 가장 큰 효과를 발휘합니다.

워크로드스레드 수기존 (mmap_lock)per-VMA Lock개선율
익명 페이지 폴트 (각 스레드 별도 영역)16기준+2.5x+150%
파일 매핑 폴트 (각 스레드 별도 파일)16기준+1.8x+80%
단일 스레드1기준동일0%
같은 VMA 경합16기준+1.05x+5%

CONFIG_PER_VMA_LOCK 활성화 조건: 커널 6.1에서 도입되었으며 CONFIG_PER_VMA_LOCK=y로 활성화합니다. 비용은 struct vm_area_struct당 읽기/쓰기 세마포어 1개 추가(약 32바이트)와 vm_lock_seq(4바이트)입니다. 메모리 오버헤드가 작아 대부분의 배포판에서 기본 활성화됩니다. 동일 VMA를 여러 스레드가 경합하는 패턴(단일 공유 메모리 영역)에서는 개선 효과가 제한적입니다.

KPTI (Kernel Page Table Isolation)

KPTI는 2018년 공개된 Meltdown (CVE-2017-5754) 취약점에 대한 커널 완화 기법입니다. 사용자 모드에서 커널 메모리를 읽을 수 있는 하드웨어 취약점을 소프트웨어적으로 차단합니다.

Meltdown 문제

Meltdown 공격 원리:

1. 사용자 코드에서 커널 주소 읽기 시도
2. CPU는 권한 검사 전에 투기적 실행(speculative execution)
3. 투기적으로 읽은 커널 데이터가 캐시에 남음
4. 캐시 타이밍 사이드채널로 커널 데이터 유출

문제: PTE의 U/S 비트로 보호하지만, 투기적 실행이 권한 검사를 우회
원인: Intel CPU의 투기적 실행이 PTE 권한 검사보다 먼저 데이터를 가져옴
KPTI 전 (취약) 단일 페이지 테이블 (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
코드 설명
  • 3~4행KPTI의 핵심 아이디어입니다. 커널용 PGD와 사용자용 PGD를 물리적으로 인접한 2개의 4KB 페이지에 배치합니다. PTI_USER_PGTABLE_BIT=12이므로, 사용자 PGD는 커널 PGD보다 정확히 4096바이트 뒤에 위치합니다. 소스: arch/x86/include/asm/pgtable.h
  • 7~10행SWITCH_TO_USER_CR3_NOSTACK 매크로는 커널→사용자 전환 시 CR3의 bit 12를 OR로 설정하여 사용자 PGD 주소로 변경합니다. 사용자 PGD에는 커널 매핑이 최소한만 포함(syscall/interrupt 진입점만)되어 Meltdown 공격을 차단합니다.
  • 13~16행SWITCH_TO_KERNEL_CR3 매크로는 사용자→커널 전환 시 bit 12를 AND NOT으로 제거하여 커널 PGD 주소를 복원합니다. 이 전환은 entry_SYSCALL_64, idtentry 등 모든 커널 진입 경로에서 수행됩니다. 단 하나의 비트 연산만 필요하므로 극히 빠릅니다.
KPTI 성능 비용: 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)
코드 설명
  • 3~11행KPTI + PCID 조합의 핵심 설계입니다. 각 프로세스에 짝수(커널용)와 홀수(사용자용) 두 개의 PCID를 할당합니다. syscall 진입 시 CR3에서 PGD 주소와 PCID가 동시에 바뀌므로, 사용자 TLB 엔트리와 커널 TLB 엔트리가 각각의 PCID로 보존됩니다. 소스: arch/x86/mm/tlb.c
  • 13~22행build_cr3()는 PGD 물리 주소, ASID(PCID), 그리고 CR3_NOFLUSH 비트를 결합하여 CR3 값을 생성합니다. static_cpu_has(X86_FEATURE_PCID)는 컴파일 타임 최적화된 CPU 기능 검사로, PCID 미지원 CPU에서는 분기 비용이 0입니다.
  • CR3_NOFLUSH (bit 63)CR3_NOFLUSH는 Intel이 PCID와 함께 도입한 최적화입니다. CR3에 이 비트를 설정하면, CR3 변경 시 현재 PCID의 TLB 엔트리를 플러시하지 않습니다. KPTI에서 사용자↔커널 전환마다 CR3를 바꿔야 하므로, 이 비트 없이는 매번 전체 TLB가 플러시되어 5~30% 성능 저하가 발생합니다.

KPTI 관련 부트 파라미터

파라미터설명보안 영향
pti=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 마운트(Mount), mmap 플래그)투명 (커널 자동 관리)
예약부트 시 또는 sysctl로 예약동적 할당
크기2MB, 1GB (x86)2MB (PMD 레벨)
스왑 지원스왑 불가스왑 가능 (분할 후)
OOM 위험예약 기반으로 안전할당 실패 시 4KB 폴백
관리 데몬없음khugepaged (백그라운드 합체)
분할/합체없음 (고정)split_huge_page / collapse
사용 사례DB (Oracle, PostgreSQL), DPDK범용 워크로드
설정/proc/sys/vm/nr_hugepages/sys/kernel/mm/transparent_hugepage/enabled
메모리 낭비내부 단편화(Fragmentation) 가능적음 (필요 시 분할)

THP 생명 주기

THP (Transparent Huge Page) 생명 주기:

1. 할당 (Allocation):
   - 페이지 폴트 시 2MB compound page 할당 시도
   - 실패하면 4KB 폴백 (defrag 설정에 따라 compaction 시도)
   - defrag=always → 동기 compaction (지연 증가)
   - defrag=defer+madvise → 비동기 compaction (권장)

2. 합체 (Collapse) - khugepaged:
   - 백그라운드 스캔 (5초 간격, /sys/kernel/mm/.../scan_sleep_millisecs)
   - 512개 연속 4KB PTE → 1개 2MB PMD 전환
   - 물리 메모리 compaction 필요할 수 있음

3. 분할 (Split):
   - mprotect()로 부분 권한 변경 시
   - swap out 시 (개별 4KB 단위로 스왑)
   - madvise(MADV_DONTNEED) 부분 적용 시
   - 분할은 비용이 큼 (512개 PTE 설정 + TLB 무효화)

4. 해제 (Free):
   - 일반 페이지와 동일하게 참조 카운트로 관리
   - compound page 전체가 한번에 해제됨

khugepaged 동작

/* mm/khugepaged.c */

/* khugepaged: 백그라운드에서 4KB 페이지들을 2MB로 합체 */
static void khugepaged_scan_mm_slot(struct mm_slot *mm_slot)
{
    struct mm_struct *mm = mm_slot->mm;
    struct vm_area_struct *vma;

    /* VMA를 순회하며 합체 후보 탐색 */
    for (vma = mm->mmap; vma; vma = vma->vm_next) {
        if (!hugepage_vma_check(vma))
            continue;

        /* 2MB 정렬된 범위 내의 512개 PTE가 모두 채워져 있는지 검사 */
        if (khugepaged_scan_pmd(mm, vma, address, &result) == SCAN_SUCCEED) {
            /* 합체: 512개 4KB 페이지 → 1개 2MB 페이지 */
            collapse_huge_page(mm, address, &result);
        }
    }
}

/* 합체 조건:
 * 1. 512개 연속 PTE가 모두 present
 * 2. 모든 페이지가 동일 VMA에 속함
 * 3. VMA 플래그가 huge page 허용
 * 4. 2MB 물리 메모리 연속 할당 가능
 * 5. refcount 조건 충족 (GUP pin 없음) */
THP 튜닝: echo madvise > /sys/kernel/mm/transparent_hugepage/enabled로 설정하면 madvise(MADV_HUGEPAGE)를 호출한 영역만 THP를 적용합니다. 이는 예측 불가능한 지연(compaction)을 피하면서 선택적으로 THP를 활용하는 운영 환경에서 권장됩니다.

folio와 대형 페이지 TLB

커널 5.16부터 도입된 folio(폴리오)는 복합 페이지(compound page)를 추상화하는 새로운 자료 구조입니다. 기존 struct page의 head/tail 구조를 대체하여, 2MB THP와 같은 대형 페이지를 단일 개체로 표현합니다. 이는 TLB 관리와 페이지 테이블 처리의 효율을 크게 향상시킵니다.

folio vs 기존 compound page

기존 struct page 기반의 복합 페이지 구조에서는 2MB THP가 512개의 struct page로 구성되었습니다. head 페이지 하나와 511개의 tail 페이지로 이루어지며, tail 페이지의 refcount를 별도 추적해야 했습니다. folio는 이를 단일 개체로 통합합니다.

folio vs 기존 compound page (2MB THP 기준) 기존: compound page (512 struct page) head page (struct page) _refcount, flags, mapping, index ... tail page[1]: compound_head 포인터만 tail page[2]: compound_head 포인터만 ... (tail page x 510개) ... tail page[511]: compound_head 포인터만 512개 struct page 순회 필요 캐시 라인 오염, refcount 복잡 신규: folio (단일 개체) struct folio order: 9 (2^9 = 512 pages) _refcount: 단일 참조 카운터 flags: 단일 플래그 세트 mapping, index: 직접 포함 단일 개체로 2MB 전체 표현 refcount 1개, 순회 불필요 PMD 단위 TLB 엔트리 1개 매핑 TLB 영향: folio 사용 시 2MB → PMD 단일 TLB 엔트리 (512개 PTE 대신) folio_order(folio) = 9 이면 PMD 매핑 가능 → TLB 슬롯 1개 사용 (기존 512개 대비)
/* include/linux/mm_types.h — folio 핵심 API */

/* folio 크기 관련 */
static inline unsigned int  folio_order(struct folio *folio);
static inline unsigned long folio_nr_pages(struct folio *folio);
static inline size_t        folio_size(struct folio *folio);

/* folio 상태 확인 */
static inline bool folio_test_large(struct folio *folio);
static inline bool folio_test_hugetlb(struct folio *folio);
static inline bool folio_test_anon(struct folio *folio);

/* 사용 예: THP folio 처리 */
struct folio *folio = page_folio(page);
if (folio_test_large(folio)) {
    unsigned int order = folio_order(folio);
    unsigned long nr    = folio_nr_pages(folio);
    /* order=9 이면 2MB THP: PMD 단일 엔트리로 매핑 가능 */
    if (order == PMD_ORDER)
        set_pmd_at(mm, addr, pmdp, mk_large_pmd(folio));
}

mTHP: 멀티사이즈 THP (커널 6.8+)

커널 6.8에서 도입된 mTHP(multi-size THP)는 2MB 고정 크기 THP 외에도 16KB, 32KB, 64KB, 128KB, 256KB, 512KB 등 다양한 크기의 익명 folio를 지원합니다. TLB 관점에서는 ARM64의 Contiguous PTE와 결합되어 더욱 효율적인 TLB 활용이 가능합니다.

# mTHP 지원 크기 확인 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*kB/

# 64KB mTHP 활성화
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled

# folio 통계 확인
grep -i thp /proc/vmstat | grep -i folio
folio 도입에 따른 TLB 효과
구분기존 compound pagefolio (커널 5.16+)mTHP folio (커널 6.8+)
자료 구조512개 struct page (head+tail)단일 struct folio단일 struct folio (가변 order)
refcount 관리head page만 유효folio 단위 단일 카운터folio 단위 단일 카운터
TLB 엔트리 수 (2MB)PMD 1개 또는 PTE 512개PMD 1개 (명시적)크기별 최적 엔트리
TLB 무효화 범위folio 경계 불명확folio 크기 = 무효화 범위order 기반 정확한 범위
ARM64 PTE_CONT 결합제한적지원64KB folio + PTE_CONT 최적화
ℹ️

folio와 TLB 무효화: folio 기반 코드에서 TLB 무효화는 folio_size()로 정확한 범위를 계산하여 수행합니다. 기존 compound page에서는 hpage_nr_pages()로 유사하게 처리했지만, folio API가 더 명시적이고 안전합니다. folio_remove_rmap_range()와 같은 함수들이 folio 단위로 rmap과 TLB를 함께 처리합니다.

아키텍처별 TLB 비교

x86, ARM64, RISC-V, MIPS는 각각 다른 TLB 관리 방식을 사용합니다. 가장 큰 차이는 하드웨어 TLB refill소프트웨어 TLB refill입니다.

특성x86-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)

AMD invlpgb (하드웨어 브로드캐스트 TLB 무효화)

기존 x86 아키텍처에서 멀티코어 TLB 무효화는 소프트웨어 IPI(Inter-Processor Interrupt) 기반의 TLB Shootdown에 의존했습니다. CPU 수가 증가할수록 IPI 오버헤드가 선형으로 증가하는 구조적 한계가 있었습니다. AMD Zen 4(Genoa, 2022)부터 도입된 INVLPGB(INValidate TLB Pages Broadcast) 명령어는 하드웨어가 직접 모든 CPU에 TLB 무효화를 브로드캐스트하여 이 문제를 해결합니다.

INVLPGB vs IPI 기반 TLB Shootdown 비교

IPI 기반 TLB Shootdown vs AMD INVLPGB 기존: IPI 기반 TLB Shootdown CPU 0 INVLPG + IPI 발송 CPU 1 INVLPG (인터럽트) CPU 2 INVLPG (인터럽트) CPU N INVLPG (인터럽트) CPU 0: 완료 ACK 대기 O(N) 오버헤드 — CPU 수 비례 신규: AMD INVLPGB CPU 0 INVLPGB 단일 명령 하드웨어 브로드캐스트 패브릭 (CPU 인터럽트 없이 TLB 직접 무효화) CPU 1 TLB CPU 2 TLB CPU N TLB TLBSYNC: 완료 대기 IPI 방식 비용 (128코어 기준) IPI 발송: 127개 인터럽트 레이턴시: ~수 마이크로초 (코어 수 비례) INVLPGB 비용 (128코어 기준) 명령 1회 + TLBSYNC 레이턴시: ~수십 나노초 (일정)

INVLPGB 명령어 상세

INVLPGB는 기존 INVLPG의 브로드캐스트 버전입니다. 단일 명령으로 시스템 내 모든 논리 CPU의 TLB에서 지정된 범위를 무효화합니다. CPU 인터럽트를 발생시키지 않으므로 다른 CPU의 실행 흐름을 방해하지 않습니다.

/* arch/x86/include/asm/tlb.h (CONFIG_X86_INVLPGB) */

/*
 * INVLPGB: 브로드캐스트 TLB 무효화
 * - rax: 가상 주소 + 속성 비트
 * - edx: PCID (상위 16비트) + 범위 길이 (하위 16비트)
 * - ecx: 추가 제어 플래그
 */
static inline void invlpgb_flush_user_nr(unsigned long pcid,
                                         unsigned long addr,
                                         long nr, bool pmd_stride)
{
    u64 rax = addr | INVLPGB_VA_VALID;
    u32 edx = (pcid << 16) | (nr - 1);
    u32 ecx = pmd_stride ? INVLPGB_STRIDE_2MB : 0;

    asm volatile(
        ".byte 0x0f, 0x01, 0xfe\n\t"  /* INVLPGB 인코딩 */
        :
        : "a"(rax), "c"(ecx), "d"(edx)
        : "memory"
    );
}

/* TLBSYNC: INVLPGB 완료 대기 배리어 */
static inline void tlbsync(void)
{
    asm volatile(
        ".byte 0x0f, 0x01, 0xff\n\t"  /* TLBSYNC 인코딩 */
        ::: "memory"
    );
}

/* 커널 6.x 통합: arch_tlbbatch_flush()에서 INVLPGB 사용 */
void native_flush_tlb_multi(const struct cpumask *cpumask,
                           const struct flush_tlb_info *info)
{
    if (cpu_feature_enabled(X86_FEATURE_INVLPGB) &&
        info->end - info->start <= INVLPGB_MAX_RANGE) {
        /* INVLPGB 사용: IPI 불필요 */
        invlpgb_flush_user_nr(info->pcid,
                              info->start,
                              (info->end - info->start) >> PAGE_SHIFT,
                              false);
        tlbsync();
    } else {
        /* 폴백: 기존 IPI 방식 */
        smp_call_function_many(cpumask, flush_tlb_func,
                              (void *)info, 1);
    }
}
INVLPGB vs IPI 기반 TLB Shootdown 비교
항목IPI 기반 (기존 x86)INVLPGB (AMD Zen 4+)ARM64 TLBI IS
무효화 방식소프트웨어 IPI + INVLPG하드웨어 브로드캐스트하드웨어 브로드캐스트
CPU 인터럽트 발생O (각 CPU마다 인터럽트)X (하드웨어 직접 처리)X (하드웨어 직접 처리)
레이턴시 (128코어)~수 마이크로초~수십 나노초~수십 나노초
확장성O(N) — 코어 수 비례O(1) — 코어 수 무관O(1) — 코어 수 무관
완료 배리어IPI ACK 대기TLBSYNC 명령DSB ISH
커널 지원항상CONFIG_X86_INVLPGB (6.x+)항상 (ARM64)
성능 향상 (많은 코어)기준최대 10배 향상최대 10배 향상
INVLPGB 사용 조건: AMD Zen 4(Genoa) 이상 프로세서에서 CONFIG_X86_INVLPGB=y로 빌드된 커널이 필요합니다. CPUID 확인: CPUID[8000_0008h].EBX[bit3]. 현재 커널은 PCID와 함께 사용하는 경우에만 INVLPGB를 활성화합니다. /proc/cpuinfo에서 invlpgb 플래그를 확인할 수 있습니다.

PCID/ASID 구현

PCID(x86)와 ASID(ARM)는 Context Switch 시 TLB 플러시를 피하기 위한 핵심 기법입니다. 하지만 ID 공간이 제한적이므로(PCID 12비트=4096, ASID 8/16비트), 할당과 롤오버 관리가 중요합니다.

x86 PCID 할당 알고리즘

/* arch/x86/mm/tlb.c */

/*
 * PCID 할당 전략:
 * - 최대 6개 PCID만 사용 (TLB_NR_DYN_ASIDS)
 *   (4096개 전부 사용하면 관리 비용이 커짐)
 * - per-CPU 단위로 PCID ↔ mm 매핑 관리
 * - 세대(generation) 번호로 유효성 추적
 */

#define TLB_NR_DYN_ASIDS  6

/* per-CPU PCID 상태 */
DEFINE_PER_CPU(struct tlb_state, cpu_tlbstate) = {
    .loaded_mm = &init_mm,
    .next_asid = 1,
    .cr4 = ~0UL,
};

struct tlb_state {
    struct mm_struct *loaded_mm;
    u16             loaded_mm_asid;
    u16             next_asid;
    u64             ctxs[TLB_NR_DYN_ASIDS]; /* 세대 번호 */
};

static void choose_new_asid(struct mm_struct *next,
                             u64 next_tlb_gen,
                             u16 *new_asid, bool *need_flush)
{
    u16 asid;

    /* 이미 할당된 ASID가 유효한지 검사 */
    for (asid = 0; asid < TLB_NR_DYN_ASIDS; asid++) {
        if (this_cpu_read(cpu_tlbstate.ctxs[asid]) ==
            next->context.ctx_id) {
            /* 기존 ASID 재사용 - TLB 플러시 불필요! */
            *new_asid = asid;
            *need_flush = false;
            return;
        }
    }

    /* 새 ASID 할당 (Round-Robin) */
    asid = this_cpu_read(cpu_tlbstate.next_asid);
    if (asid >= TLB_NR_DYN_ASIDS)
        asid = 0;

    *new_asid = asid;
    *need_flush = true;  /* 이전 mm의 TLB 엔트리 무효화 필요 */

    this_cpu_write(cpu_tlbstate.ctxs[asid], next->context.ctx_id);
    this_cpu_write(cpu_tlbstate.next_asid, asid + 1);
}
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으로 재활용(Recycling)

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_A000_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커널 모듈(Kernel Module)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투기적 실행(Speculative Execution)이 U/S 비트 우회KPTI (페이지 테이블 격리(Isolation))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마이크로아키텍처 버퍼(Buffer) 데이터 유출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: 가상화(Virtualization) 환경에서 L1TF는 게스트 VM이 호스트 또는 다른 VM의 L1D 캐시 데이터를 읽을 수 있는 심각한 취약점입니다. 완화를 위해 VM 전환 시 L1D 캐시를 플러시하며, 이로 인해 VM Exit 비용이 크게 증가합니다. kvm-intel.vmentry_l1d_flush=always로 설정합니다.

Spectre v2와 BTB/RSB

Spectre v2는 분기 예측(Branch Prediction)기(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단계: 핫스팟 프로파일링(Profiling)

# dTLB miss가 가장 많이 발생하는 함수 찾기
perf record -e dTLB-load-misses -g \
    -p $(pgrep -x postgres | head -1) -- sleep 10

perf report --stdio --sort=dso,symbol | head -30

# 출력 예:
# 42.3%  postgres  [kernel]      [k] clear_page_rep
# 18.7%  postgres  postgres      [.] hash_search_with_hash
# 12.1%  postgres  postgres      [.] ExecScanFetch
# 8.4%   postgres  libc-2.31.so  [.] __memmove_avx_unaligned
#
# 분석: clear_page_rep에서 42% → 새 페이지 할당/초기화가 빈번
# hash_search에서 18% → 해시 테이블이 큰 메모리 영역에 분산

3단계: BPF 기반 TLB 분석

# bpftrace로 TLB flush 이벤트 추적
bpftrace -e '
tracepoint:tlb:tlb_flush {
    @flush_reason[args->reason] = count();
    @flush_pages = hist(args->pages);
}

interval:s:10 {
    print(@flush_reason);
    print(@flush_pages);
    clear(@flush_reason);
    clear(@flush_pages);
}
'

# 출력 예:
# @flush_reason[3]: 15234    ← TLB_REMOTE_SHOOTDOWN (가장 많음!)
# @flush_reason[0]: 2341     ← TLB_FLUSH_ON_TASK_SWITCH
# @flush_reason[2]: 891      ← TLB_LOCAL_MM_SHOOTDOWN
#
# @flush_pages:
# [1]      |@@@@@@@@@@@@@@                    | 3456
# [2, 4)   |@@@@@@@@@@@@@@@@@@@@@@@@@@@@      | 7890
# [4, 8)   |@@@@@@@@@@@@@@@@@@                | 4567
# [8, 16)  |@@@@@@                            | 1234
#
# 진단: Remote Shootdown이 지배적 → 다수 CPU에서 mmap 관련 변경이 빈번

4단계: 근본 원인 파악

# mmap/munmap 시스템 콜 빈도 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_mmap { @mmap = count(); }
tracepoint:syscalls:sys_enter_munmap { @munmap = count(); }
tracepoint:syscalls:sys_enter_mprotect { @mprotect = count(); }
tracepoint:syscalls:sys_enter_madvise { @madvise = count(); }

interval:s:5 {
    print(@mmap); print(@munmap);
    print(@mprotect); print(@madvise);
    clear(@mmap); clear(@munmap);
    clear(@mprotect); clear(@madvise);
}
'

# 결과: mmap/munmap이 5초당 수천 회 → jemalloc의 공격적 매핑/해제
# PostgreSQL의 work_mem 사용 시 임시 메모리 할당/해제가 빈번

5단계: 튜닝 적용

# 해결 방법 1: Transparent Huge Pages 활성화
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

# 해결 방법 2: PostgreSQL huge_pages 설정
# postgresql.conf:
# huge_pages = try
# shared_buffers = 32GB

# 해결 방법 3: HugeTLB 예약
echo 16384 > /proc/sys/vm/nr_hugepages  # 32GB를 2MB hugepages로

# 해결 방법 4: jemalloc retain 설정 (mmap/munmap 감소)
export MALLOC_CONF="retain:true,dirty_decay_ms:10000,muzzy_decay_ms:30000"

# 튜닝 결과 확인
perf stat -e dTLB-loads,dTLB-load-misses \
    -p $(pgrep -x postgres | head -1) -- sleep 30

# 결과:
#  12,345,678,901  dTLB-loads
#      98,765,432  dTLB-load-misses  # 0.8% ← 8% → 0.8% (10배 개선!)

튜닝 전후 비교

지표튜닝 전튜닝 후개선율
dTLB miss 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)해시 테이블(Hash Table) 랜덤 접근THP madvise, jemalloc retain
JVM 애플리케이션힙이 큰 경우 TLB 부족-XX:+UseHugeTLBFS, THP
HPC / 과학 계산대규모 배열 순차 접근1GB HugeTLB 페이지
웹 서버 (많은 프로세스)Context Switch 시 TLB 플러시PCID 확인, 프로세스 수 제한
컨테이너 (K8s)cgroup 내 mmap 경합THP per-cgroup 설정, shootdown 모니터링
DPDK / 네트워크패킷(Packet) 버퍼 메모리1GB HugeTLB, IOMMU hugepage

고급 BPF 분석 스크립트

/* tlb_analysis.bpf.c - TLB 성능 분석용 BPF 프로그램 */

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);       /* pid */
    __type(value, u64);     /* shootdown count */
} shootdown_count SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HISTOGRAM);
    __uint(max_entries, 64);
    __type(key, u64);
} shootdown_latency SEC(".maps");

SEC("tp/tlb/tlb_flush")
int trace_tlb_flush(struct trace_event_raw_tlb_flush *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *count = bpf_map_lookup_elem(&shootdown_count, &pid);

    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        u64 init_val = 1;
        bpf_map_update_elem(&shootdown_count, &pid, &init_val, BPF_ANY);
    }

    /* 플러시된 페이지 수 히스토그램 */
    u64 pages = ctx->pages;
    bpf_map_update_elem(&shootdown_latency, &pages, &pages, BPF_ANY);

    return 0;
}

참고자료

커널 문서

LWN 기사

커널 소스 코드