페이지 테이블 일반 (Page Table Generic)

개요 (Overview)

Linux 커널의 pgtable-generic.c는 아키텍처 독립적인 페이지 테이블 조작 메커니즘을 제공하는 코어 모듈입니다. x86_64, ARM64, RISC-V 등 다양한 아키텍처에서 공통으로 사용되는 페이지 테이블 연산(접근 권한 설정, 엔트리 클리어, TLB 플러시, huge page 처리, PTE 매핑/잠금 등)을 구현하며, 특정 아키텍처가 자체 최적화된 구현을 제공하지 않을 때 폴백(fallback)으로 사용됩니다.

페이지 테이블은 커널이 가상 주소를 물리 주소로 변환하는 핵심 자료구조로, 4단계(PGD→PUD→PMD→PTE) 또는 5단계(PGD→P4D→PUD→PMD→PTE) 계층 구조를 갖습니다. pgtable-generic.c는 이 계층에서 각 레벨에 해당하는 엔트리의 플래그 검증, 잠금 기반 안전한 접근, huge page deposit/withdraw, 비동기 해제(async free) 같은 공통 로직을 담당합니다. Linux 7.0에서는 CONFIG_ASYNC_KERNEL_PGTABLE_FREE를 통해 커널 페이지 테이블 해제를 워커 스레드에 위임하는 비동기 메커니즘이 추가되었습니다.

소스 파일 경로:
├── mm/pgtable-generic.c                ← 공통 페이지 테이블 메서드 (447줄)
├── include/linux/pgtable.h             ← PTE/PMD/PUD/PGD 인라인/매크로 (2372줄)
├── include/linux/pgalloc.h             ← 페이지 테이블 할당.populate 매크로 (29줄)
├── include/linux/page_table_check.h    ← 페이지 테이블 검증 인터페이스 (166줄)
└── include/asm-generic/tlb.h           ← MMU gather 구조체

빠른 점검 명령

# 1. 페이지 테이블 레벨 확인 (4단계/5단계)
cat /boot/config-$(uname -r) | grep CONFIG_PGTABLE_LEVELS

# 2. 커널 페이지 테이블 구조 확인 (x86_64)
cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40

# 3. pgtable 관련 커널 심볼 확인
cat /proc/kallsyms | grep -E "ptep_set_access_flags|pte_offset_map|pmdp_huge"

# 4. THP(Transparent Huge Page) 활성화 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled

# 5. 페이지 테이블 검증 활성화 여부
cat /proc/cmdline | grep page_table_check
grep CONFIG_PAGE_TABLE_CHECK /boot/config-$(uname -r)

# 6. 페이지 테이블 해제 방식 확인 (비동기 지원 여부)
grep CONFIG_ASYNC_KERNEL_PGTABLE_FREE /boot/config-$(uname -r)

# 7. 프로세스별 PTE/PMD 엔트리 수 확인
cat /proc/vmstat | grep -E "nr_pte|nr_pmd|nr_pud"

# 8. 페이지 테이블 할당 통계
cat /proc/buddyinfo

# 9. lazy MMU 모드 관련 심볼 확인
cat /proc/kallsyms | grep -E "lazy_mmu_mode"

# 10. 페이지 테이블 관련 커널 로그 확인
dmesg | grep -i "pgtable\|page_table"

# 11. 페이지 테이블 레벨별 엔트리 수 확인 (x86_64 기준)
cat /proc/cpuinfo | grep "page sizes"  # 아키텍처별 페이지 테이블 깊이

# 12. 커널 페이지 테이블 메모리 사용량 확인
cat /proc/meminfo | grep -i "page tables"

# 13. 페이지 테이블 할당/해제 추적 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_free/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20

핵심 자료구조

1. `pte_t` / `pmd_t` / `pud_t` / `p4d_t` / `pgd_t`

각 페이지 테이블 레벨의 엔트리 타입입니다. 아키텍처마다 내부 표현이 다르지만, generic 코드에서는 값 비교(pte_same, pmd_same)와 비트 조작(pte_mkold, pte_wrprotect) 인터페이스를 통해 접근합니다.

// include/linux/pgtable.h:8-9
#define PMD_ORDER  (PMD_SHIFT - PAGE_SHIFT)
#define PUD_ORDER  (PUD_SHIFT - PAGE_SHIFT)
// PMD/PUD 한 레벨이 차지하는 페이지 테이블 페이지의 order

2. `enum pgtable_level` (include/linux/pgtable.h:2169)

enum pgtable_level {
PGTABLE_LEVEL_PTE = 0,
PGTABLE_LEVEL_PMD,
PGTABLE_LEVEL_PUD,
PGTABLE_LEVEL_P4D,
PGTABLE_LEVEL_PGD,
};
// 각 레벨을 정수로 식별
// pgtable_level_to_str()로 문자열 변환 가능

3. `pgtbl_mod_mask` (include/linux/pgtable.h:2167)

typedef unsigned int pgtbl_mod_mask;
// 페이지 테이블 변경 추적 비트마스크
// PGTBL_PGD_MODIFIED ~ PGTBL_PTE_MODIFIED 비트 사용
// vmalloc/ioremap에서 arch_sync_kernel_mappings 호출 시점 결정

4. `struct kernel_pgtable_work` (mm/pgtable-generic.c:414)

static struct {
struct list_head list;    // 해제 대기 ptdesc 목록
spinlock_t lock;          // 목록 보호 잠금
struct work_struct work;  // 워커 스레드 작업 구조체
} kernel_pgtable_work = {
.list = LIST_HEAD_INIT(kernel_pgtable_work.list),
.lock = __SPIN_LOCK_UNLOCKED(kernel_pgtable_work.lock),
.work = __WORK_INITIALIZER(kernel_pgtable_work.work, kernel_pgtable_work_func),
};
// CONFIG_ASYNC_KERNEL_PGTABLE_FREE 활성화 시 사용
// 커널 페이지 테이블 해제를 지연 처리하여 잠금 경합 최소화
// kernel_pgtable_work_func()에서 iommu_sva_invalidate_kva_range() 호출 후
// __pagetable_free()로 실제 해제 수행

5. PTE 매핑 관련 매크로 (include/linux/pgtable.h:103-119)

// CONFIG_HIGHPTE 활성화 시 kmapping 필요
#ifdef CONFIG_HIGHPTE
#define __pte_map(pmd, address) \
((pte_t *)kmap_local_page(pmd_page(*(pmd))) + pte_index((address)))
#define pte_unmap(pte) do {    \
kunmap_local((pte));       \
rcu_read_unlock();         \
} while (0)
#else
static inline pte_t *__pte_map(pmd_t *pmd, unsigned long address) {
return pte_offset_kernel(pmd, address);
}
static inline void pte_unmap(pte_t *pte) {
rcu_read_unlock();
}
#endif
// HIGHMEM 환경에서는 가상 메모리에 매핑 필요
// x86_64는 보통 HIGHPTE 비활성화

핵심 함수

1. `ptep_set_access_flags()` (mm/pgtable-generic.c:70)

PTE의 접근 플래그(dirty, accessed)와 쓰기 권한을 설정합니다. 아키텍처가 별도 구현을 제공하지 않을 때(__HAVE_ARCH_PTEP_SET_ACCESS_FLAGS 미정의) 사용됩니다.

int ptep_set_access_flags(struct vm_area_struct *vma,
unsigned long address, pte_t *ptep,
pte_t entry, int dirty)
{
int changed = !pte_same(ptep_get(ptep), entry);  // 현재 PTE와 비교
if (changed) {
set_pte_at(vma->vm_mm, address, ptep, entry);  // 새 PTE 설정
flush_tlb_fix_spurious_fault(vma, address, ptep);  // TLB 무효화
}
return changed;  // PTE 변경 여부 반환 → 호출자가 mmu_cache 갱신 결정
}

호출 흐름: 페이지 폴트 처리 → handle_pte_fault → do_wp_page/do_fault → ptep_set_access_flags

2. `pte_offset_map_lock()` (mm/pgtable-generic.c:391)

PTE 테이블에 대한 안전한 잠금 기반 매핑을 제공합니다. 페이지 테이블이 RCU로 제거되거나 THP로 교체될 수 있으므로, 잠금 획득 후 pmd_same() 검사를 통해 안전성을 보장합니다.

pte_t *pte_offset_map_lock(struct mm_struct *mm, pmd_t *pmd,
unsigned long addr, spinlock_t **ptlp)
{
spinlock_t *ptl;
pmd_t pmdval;
pte_t *pte;
again:
pte = __pte_offset_map(pmd, addr, &pmdval);  // RCU 하에서 PTE 매핑
if (unlikely(!pte))
return pte;
ptl = pte_lockptr(mm, &pmdval);  // PTE 레벨 잠금 획득
spin_lock(ptl);
if (likely(pmd_same(pmdval, pmdp_get_lockless(pmd)))) {
*ptlp = ptl;
return pte;  // 안전하게 PTE 반환
}
pte_unmap_unlock(pte, ptl);
goto again;  // PMD가 변경되었으면 재시도
}

반환값: 성공 시 PTE 포인터 + *ptlp에 잠금 포인터, 실패 시 NULL

3. `pmdp_huge_clear_flush()` (mm/pgtable-generic.c:139)

THP(Transparent Huge Page)의 PMD 엔트리를 원자적으로 제거하고 해당 범위의 TLB를 플러시합니다.

pmd_t pmdp_huge_clear_flush(struct vm_area_struct *vma, unsigned long address,
pmd_t *pmdp)
{
pmd_t pmd;
VM_BUG_ON(address & ~HPAGE_PMD_MASK);           // 주소 정렬 확인
VM_BUG_ON(pmd_present(*pmdp) && !pmd_trans_huge(*pmdp));  // THP여야 함
pmd = pmdp_huge_get_and_clear(vma->vm_mm, address, pmdp);  // 원자적 제거
flush_pmd_tlb_range(vma, address, address + HPAGE_PMD_SIZE);  // TLB 플러시
return pmd;
}

4. `pgtable_trans_huge_deposit()` / `pgtable_trans_huge_withdraw()` (mm/pgtable-generic.c:166,182)

THP 해제 시 보조 PTE 테이블 페이지를 PMD에 연결(deposit)하고, 재사용 시 꺼내는(withdraw) FIFO 큐입니다.

// mm/pgtable-generic.c:166-177
void pgtable_trans_huge_deposit(struct mm_struct *mm, pmd_t *pmdp,
pgtable_t pgtable)
{
assert_spin_locked(pmd_lockptr(mm, pmdp));  // 잠금 보유 확인
if (!pmd_huge_pte(mm, pmdp))
INIT_LIST_HEAD(&pgtable->lru);          // 첫 페이지 → 새 목록 생성
else
list_add(&pgtable->lru, &pmd_huge_pte(mm, pmdp)->lru);  // 스택 PUSH
pmd_huge_pte(mm, pmdp) = pgtable;           // head 갱신
}

// mm/pgtable-generic.c:182-195
pgtable_t pgtable_trans_huge_withdraw(struct mm_struct *mm, pmd_t *pmdp)
{
pgtable_t pgtable;
assert_spin_locked(pmd_lockptr(mm, pmdp));
pgtable = pmd_huge_pte(mm, pmdp);           // 현재 head 획득
pmd_huge_pte(mm, pmdp) = list_first_entry_or_null(&pgtable->lru,
struct page, lru);
if (pmd_huge_pte(mm, pmdp))
list_del(&pgtable->lru);                // 다음 페이지를 새 head로
return pgtable;                              // 해제 대상 반환
}

5. `__pte_offset_map()` (mm/pgtable-generic.c:283)

RCU 보호 하에서 PMD에서 PTE 테이블을 매핑하는 내부 함수입니다. pmd_none, pmd_bad, pmd_trans_huge 등 비정상 상태를 검사하고 안전하게 매핑된 PTE 포인터를 반환합니다.

pte_t *__pte_offset_map(pmd_t *pmd, unsigned long addr, pmd_t *pmdvalp)
{
unsigned long irqflags;
pmd_t pmdval;
rcu_read_lock();
irqflags = pmdp_get_lockless_start();  // IRQ 저장 (SMP/PREEMPT_RCU)
pmdval = pmdp_get_lockless(pmd);       // 잠금 없이 PMD 읽기
pmdp_get_lockless_end(irqflags);
if (pmdvalp) *pmdvalp = pmdval;
if (unlikely(pmd_none(pmdval) || !pmd_present(pmdval)))
goto nomap;        // PMD가 비어있거나 present 아님
if (unlikely(pmd_trans_huge(pmdval)))
goto nomap;        // THP PMD → PTE 테이블 없음
if (unlikely(pmd_bad(pmdval))) {
pmd_clear_bad(pmd);  // 손상된 PMD 에러 보고 후 클리어
goto nomap;
}
return __pte_map(&pmdval, addr);  // PTE 테이블 매핑
nomap:
rcu_read_unlock();
return NULL;
}

6. `pagetable_free_kernel()` (mm/pgtable-generic.c:439)

커널 페이지 테이블 해제를 비동기 워커에 위임합니다. CONFIG_ASYNC_KERNEL_PGTABLE_FREE 활성화 시 사용되며, IOMMU SVA invalidated 후 __pagetable_free()를 호출합니다.

void pagetable_free_kernel(struct ptdesc *pt)
{
spin_lock(&kernel_pgtable_work.lock);
list_add(&pt->pt_list, &kernel_pgtable_work.list);  // 대기 목록에 추가
spin_unlock(&kernel_pgtable_work.lock);
schedule_work(&kernel_pgtable_work.work);  // 워커 스레드 스케줄링
}

호출 흐름

[페이지 폴트 처리]
handle_mm_fault()
→ handle_pte_fault()
→ do_fault() / do_wp_page()
→ ptep_set_access_flags()     ← 접근 플래그 설정
→ set_pte_at() + flush_tlb_fix_spurious_fault()

[PTE 매핑 (잠금 포함)]
pte_offset_map_lock(mm, pmd, addr, &ptl)
→ __pte_offset_map()              ← RCU 하에서 PMD→PTE 매핑
→ pmdp_get_lockless()           ← 잠금 없이 PMD 읽기
→ __pte_map() / kmap_local_page()
→ pte_lockptr() + spin_lock()     ← PTE 레벨 잠금
→ pmd_same() 재검증               ← PMD 변경 여부 확인

[THP PMD 제거]
pmdp_huge_clear_flush(vma, addr, pmdp)
→ pmdp_huge_get_and_clear()       ← PMD 원자적 제거
→ flush_pmd_tlb_range()           ← THP 범위 TLB 플러시

[PTE 테이블 deposit/withdraw]
pgtable_trans_huge_deposit(mm, pmdp, pgtable)
→ list_add(FIFO 스택)            ← 보조 PTE 테이블 연결
pgtable_trans_huge_withdraw(mm, pmdp)
→ list_first_entry_or_null()      ← FIFO에서 꺼내기

[PMD invalidation]
pmdp_invalidate(vma, addr, pmdp)
→ pmdp_establish(pmd_mkinvalid()) ← PMD를 invalid로 설정
→ flush_pmd_tlb_range()

[THP 해제 시 PTE 테이블 보관 흐름]
khugepaged 병합 또는 THP 해제 시
→ pgtable_trans_huge_deposit(mm, pmdp, pgtable)
→ assert_spin_locked()          ← pmd_lock 보유 확인
→ INIT_LIST_HEAD() 또는 list_add()  ← FIFO 큐에 추가
→ pmd_huge_pte(mm, pmdp) = pgtable  ← head 포인터 갱신

[THP 재생성 시 PTE 테이블 복원 흐름]
THP 해제 후 일반 페이지로 복원 시
→ pgtable_trans_huge_withdraw(mm, pmdp)
→ pgtable = pmd_huge_pte()       ← 현재 head 획득
→ list_first_entry_or_null()     ← 다음 엔트리 조회
→ list_del()                     ← 해제 대상 제거
→ return pgtable                 ← 재사용할 PTE 테이블 반환

[비동기 페이지 테이블 해제]
pagetable_free_kernel(pt)
→ list_add(kernel_pgtable_work.list)
→ schedule_work()                 ← 워커 스레드 스케줄링
→ kernel_pgtable_work_func()
→ iommu_sva_invalidate_kva_range()
→ __pagetable_free()            ← 실제 해제

조건별 비교

페이지 테이블 레벨별 clear_bad 동작 비교

레벨함수`__PAGETABLE_XX_FOLDED` 시 동작메모
PGD`pgd_clear_bad()`always 동작fold 없음
P4D`p4d_clear_bad()`no-op 매크로일부 아키텍처 fold
PUD`pud_clear_bad()`no-op 매크로일부 아키텍처 fold
PMD`pmd_clear_bad()`always 동작特殊: fold 시에도 upper-level 참조

PTE 매핑 함수 비교

함수잠금TLB 플러시사용 시점
`pte_offset_map()`❌ (RCU only)읽기 전용, 비간섭적
`pte_offset_map_ro_nolock()`❌ (ptlp 반환)R/O 접근, 이후 잠금 예정
`pte_offset_map_rw_nolock()`❌ (ptlp 반환)R/W 접근, pmdval 확인 필요
`pte_offset_map_lock()`✅ (spin_lock)PTE 수정 전 안전 접근

Huge page clear/flush 비교

함수대상TLB 범위시점
`pmdp_huge_clear_flush()`PMDHPAGE_PMD_SIZETHP 해제
`pudp_huge_clear_flush()`PUDHPAGE_PUD_SIZETHP 해제 (CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD)
`pmdp_collapse_flush()`PMDHPAGE_PMD_SIZEkhugepaged 병합

페이지 테이블 레벨별 엔트리 수 비교 (x86_64 기준)

레벨엔트리 수엔트리당 크기전체 페이지 크기가상 주소 범위
PGD5128B4KB256TB (48비트)
P4D5128B4KB512GB (39비트)
PUD5128B4KB1GB (30비트)
PMD5128B4KB2MB (21비트)
PTE5128B4KB4KB (12비트)
  • 5단계 페이지 테이블(PGD→P4D→PUD→PMD→PTE) 사용 시 최대 128PB 주소 공간 지원
  • 4단계 페이지 테이블(PGD→PUD→PMD→PTE) 사용 시 최대 256TB 주소 공간 지원
  • CONFIG_PGTABLE_LEVELS로 레벨 수 결정, 일부 레벨은 fold되어 1개 엔트리로 축소
  • Lazy MMU 모드 동작 비교

    함수동작중첩 지원
    `lazy_mmu_mode_enable()`lazy MMU 진입✅ (카운터 기반)
    `lazy_mmu_mode_disable()`lazy MMU 탈출✅ (카운터 기반)
    `lazy_mmu_mode_pause()`lazy MMU 일시 정지✅ (pause/resume 쌍)
    `lazy_mmu_mode_resume()`lazy MMU 재개

    관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 38-pagewalk.html — Page Table Walk 프레임워크
  • 61-page_table_check.html — 페이지 테이블 검증
  • 65-mmu_gather.html — MMU Gather (TLB batching)
  • 10-hugepage.html — Huge Pages / THP
  • 03-vma_mmap.html — VMA / mmap
  • 36-mprotect.html — mprotect (PTE 보호 변경)
  • 37-mremap.html — mremap (PTE 이동)
  • 64-nommu.html — MMU 없는 시스템

  • SVG 다이어그램

    페이지 테이블 일반 구조와 흐름