페이지 테이블 일반 (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()` | PMD | HPAGE_PMD_SIZE | THP 해제 |
| `pudp_huge_clear_flush()` | PUD | HPAGE_PUD_SIZE | THP 해제 (CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD) |
| `pmdp_collapse_flush()` | PMD | HPAGE_PMD_SIZE | khugepaged 병합 |
페이지 테이블 레벨별 엔트리 수 비교 (x86_64 기준)
| 레벨 | 엔트리 수 | 엔트리당 크기 | 전체 페이지 크기 | 가상 주소 범위 |
|---|---|---|---|---|
| PGD | 512 | 8B | 4KB | 256TB (48비트) |
| P4D | 512 | 8B | 4KB | 512GB (39비트) |
| PUD | 512 | 8B | 4KB | 1GB (30비트) |
| PMD | 512 | 8B | 4KB | 2MB (21비트) |
| PTE | 512 | 8B | 4KB | 4KB (12비트) |
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 재개 | ✅ |