# Page Table Walk 🚶
관련 소스:mm/pagewalk.c,include/linux/pagewalk.h,mm/internal.h
Linux 커널의 Page Table Walk는 가상 주소 공간을 순회하면서 각 페이지 테이블 엔트리(PGD, P4D, PUD, PMD, PTE)에 대해 콜백을 호출하는 프레임워크입니다. walk_page_range()를 통해 호출자는 지정된 가상 주소 범위 내 모든 페이지 테이블 레벨을 순회하면서 각 엔트리마다 pte_entry, pmd_entry, pud_entry 같은 콜백을 실행할 수 있습니다. 이 프레임워크는 mlock, madvise, mincore, mprotect, KSM, DAMON, memory-failure 등 메모리 관리 서브시스템 전반에서 사용됩니다.
페이지 테이블 구조는 아키텍처에 따라 4단계(PGD→PUD→PMD→PTE) 또는 5단계(PGD→P4D→PUD→PMD→PTE)로 구성되며, p4d/pud 수준은 아키텍처에 따라 fold될 수 있습니다. pagewalk 프레임워크는 real_depth() 함수를 통해 fold된 레벨을 보정하여 정확한 깊이 정보를 콜백에 전달합니다. 도서관으로 비유하면, 이 프레임워크는 서가 번호를 하나씩 따라가며 책이 있는 칸만 확인하는 사서와 비슷합니다. Linux 7.0에서는 folio_walk_start() API가 추가되어, 단일 가상 주소에서 해당하는 folio를 직접 찾아 PTL(PTE Lock)을 획득하는 간결한 인터페이스를 제공합니다. 페이지 폴트 처리기와 같은 경로를 직접 대체하는 것은 아니고, 이미 만들어진 매핑을 다른 서브시스템이 안전하게 훑거나 조정할 때 주로 쓰입니다.
mm/pagewalk.c ← 메인 구현 (1050줄)
include/linux/pagewalk.h ← API 정의 (207줄)
mm/internal.h ← _unsafe() 함수 선언
include/linux/leafops.h ← softleaf 타입/플래그
# 1. pagewalk 관련 커널 심볼 확인
cat /proc/kallsyms | grep -E "walk_page|folio_walk"
# 2. 커널 페이지 테이블 구조 (x86_64 4-level)
ls /sys/kernel/mm/page_tables/
# 3. pagewalk이 사용된 모듈/API 호출부 grep
grep -rn "walk_page_range\|walk_page_vma\|folio_walk_start" mm/
# 4. 현재 프로세스의 페이지 테이블 레벨 확인 (x86_64)
cat /proc/cpuinfo | grep "page table" || echo "4-level" # 기본 4-level
# 5. pte_offset_map_lock 등 페이지 테이블 액세스 함수 사용 확인
grep -rn "pte_offset_map\|pmd_offset\|pud_offset\|pgd_offset" mm/
# 6. CONFIG_PGTABLE_HAS_HUGE_LEAVES 활성화 여부
grep "PGTABLE_HAS_HUGE_LEAVES" /boot/config-$(uname -r) 2>/dev/null
# 7. pagewalk 관련 호출부 전체 확인
grep -rn "walk_page_range\|walk_page_range_vma\|walk_page_mapping\|folio_walk_start" mm/
# 8. 커널 페이지 테이블 덤프 확인
cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40
콜백 함수 포인터 테이블 — 호출자가 순회 동작을 커스터마이징합니다.
// include/linux/pagewalk.h:70-94
struct mm_walk_ops {
int (*pgd_entry)(pgd_t *pgd, unsigned long addr,
unsigned long next, struct mm_walk *walk);
int (*p4d_entry)(p4d_t *p4d, unsigned long addr,
unsigned long next, struct mm_walk *walk);
int (*pud_entry)(pud_t *pud, unsigned long addr,
unsigned long next, struct mm_walk *walk);
int (*pmd_entry)(pmd_t *pmd, unsigned long addr,
unsigned long next, struct mm_walk *walk);
int (*pte_entry)(pte_t *pte, unsigned long addr,
unsigned long next, struct mm_walk *walk);
int (*pte_hole)(unsigned long addr, unsigned long next,
int depth, struct mm_walk *walk);
int (*hugetlb_entry)(pte_t *pte, unsigned long hmask,
unsigned long addr, unsigned long next,
struct mm_walk *walk);
int (*test_walk)(unsigned long addr, unsigned long next,
struct mm_walk *walk);
int (*pre_vma)(unsigned long start, unsigned long end,
struct mm_walk *walk);
void (*post_vma)(struct mm_walk *walk);
int (*install_pte)(unsigned long addr, unsigned long next,
pte_t *ptep, struct mm_walk *walk);
enum page_walk_lock walk_lock;
};
| 필드 | 역할 |
|---|---|
| `pgd_entry` | 각 non-empty PGD 엔트리에서 호출 |
| `p4d_entry` | 각 non-empty P4D 엔트리에서 호출 |
| `pud_entry` | 각 non-empty PUD 엔트리에서 호출 |
| `pmd_entry` | 각 non-empty PMD 엔트리에서 호출 (huge page 처리 필요) |
| `pte_entry` | 각 PTE 엔트리에서 호출 (install_pte 설정 시 기존 PTE만) |
| `pte_hole` | hole 엔트리에서 호출, depth: -1(모름), 0:PGD~3:PMD |
| `hugetlb_entry` | HugeTLB 엔트리에서 호출 (VMA lock 유지) |
| `test_walk` | VMA 순회 전 테스트, 0:진행, >0:스킵, <0:중단 |
| `pre_vma` / `post_vma` | VMA 순회 전후 콜백 |
| `install_pte` | 누락된 PTE 자동 설치 (내부 메모리 관리 전용) |
| `walk_lock` | mmap_lock 잠금 모드 (RDLOCK/WRLOCK 등) |
순회 상태를 담당하는 구조체 — 콜백 간 공유됩니다.
// include/linux/pagewalk.h:121-129
struct mm_walk {
const struct mm_walk_ops *ops; // 콜백 테이블
struct mm_struct *mm; // 대상 프로세스 mm
pgd_t *pgd; // 커널 테이블 순회 시 PGD 포인터
struct vm_area_struct *vma; // 현재 순회 중인 VMA
enum page_walk_action action; // 다음 동작 (SUBTREE/CONTINUE/AGAIN)
bool no_vma; // VMA 무시 모드
void *private; // 콜백용 사용자 데이터
};
pmd_entry/pud_entry 콜백이 반환하는 동작 제어 값:
// include/linux/pagewalk.h:100-107
enum page_walk_action {
ACTION_SUBTREE = 0, // 하위 레벨로 내려감 (THP 분할 포함)
ACTION_CONTINUE = 1, // 같은 레벨의 다음 엔트리로 이동
ACTION_AGAIN = 2 // 현재 엔트리를 다시 처리
};
잠금 모드 — VMA 처리 시 mmap_lock 잠금 수준을 지정합니다:
// include/linux/pagewalk.h:10-19
enum page_walk_lock {
PGWALK_RDLOCK = 0, // mmap_lock 읽기 잠금
PGWALK_WRLOCK = 1, // VMA 쓰기 잠금 획득
PGWALK_WRLOCK_VERIFY = 2, // VMA 쓰기 잠금 이미 획득됨 (검증만)
PGWALK_VMA_RDLOCK_VERIFY = 3, // VMA 읽기 잠금 이미 획득됨 (검증만)
};
folio_walk_start() / folio_walk_end() API의 반환/상태 구조체:
// include/linux/pagewalk.h:177-194
struct folio_walk {
/* public */
struct page *page; // 참조된 정확한 페이지
enum folio_walk_level level; // PTE/PMD/PUD 중 엔트리 레벨
union {
pte_t *ptep; // PTE 포인터 (FW_LEVEL_PTE)
pud_t *pudp; // PUD 포인터 (FW_LEVEL_PUD)
pmd_t *pmdp; // PMD 포인터 (FW_LEVEL_PMD)
};
union {
pte_t pte; // PTE 값
pud_t pud; // PUD 값
pmd_t pmd; // PMD 값
};
/* private */
struct vm_area_struct *vma; // 대상 VMA
spinlock_t *ptl; // 페이지 테이블 잠금
};
// include/linux/pagewalk.h:155-158
#define FW_MIGRATION BIT(0) // migration 엔트리도 순회
#define FW_ZEROPAGE BIT(1) // 공유 zero page도 반환
외부 API 진입점 — check_ops_safe()로 install_pte 사용 여부 검증 후 walk_page_range_mm_unsafe() 호출.
// mm/pagewalk.c:598-606
int walk_page_range(struct mm_struct *mm, unsigned long start,
unsigned long end, const struct mm_walk_ops *ops,
void *private)
{
if (!check_ops_safe(ops))
return -EINVAL;
return walk_page_range_mm_unsafe(mm, start, end, ops, private);
}
mm->mmap_lock 보유 필요install_pte가 설정된 ops는 외부 API에서 차단실제 순회 로직 — VMA를 순회하면서 각 영역에 대해 __walk_page_range() 호출.
// mm/pagewalk.c:496-530
vma = find_vma(walk.mm, start);
do {
if (!vma) { /* 마지막 VMA 이후 */
walk.vma = NULL;
next = end;
if (ops->pte_hole)
err = ops->pte_hole(start, next, -1, &walk);
} else if (start < vma->vm_start) { /* VMA 바깥 */
walk.vma = NULL;
next = min(end, vma->vm_start);
if (ops->pte_hole)
err = ops->pte_hole(start, next, -1, &walk);
} else { /* VMA 내부 */
process_vma_walk_lock(vma, ops->walk_lock);
walk.vma = vma;
next = min(end, vma->vm_end);
vma = find_vma(mm, vma->vm_end);
err = walk_page_test(start, next, &walk);
if (err > 0) {
err = 0;
continue;
}
if (err < 0)
break;
err = __walk_page_range(start, next, &walk);
}
if (err)
break;
} while (start = next, start < end);
find_vma()로 시작 주소의 VMA를 찾고, VMA 간 gap은 pte_hole로 처리pte_hole(start, next, -1, &walk) 호출walk_page_test()로 VMA 검증 후 __walk_page_range() 호출VMA 내부 순회 — HugeTLB면 walk_hugetlb_range(), 아니면 walk_pgd_range() 호출.
// mm/pagewalk.c:408-435
static int __walk_page_range(unsigned long start, unsigned long end,
struct mm_walk *walk)
{
int err = 0;
struct vm_area_struct *vma = walk->vma;
const struct mm_walk_ops *ops = walk->ops;
bool is_hugetlb = is_vm_hugetlb_page(vma);
/* hugetlb PTE 설치는 지원하지 않는다. */
if (ops->install_pte && is_hugetlb)
return -EINVAL;
if (ops->pre_vma) {
err = ops->pre_vma(start, end, walk);
if (err)
return err;
}
if (is_hugetlb) {
if (ops->hugetlb_entry)
err = walk_hugetlb_range(start, end, walk);
} else
err = walk_pgd_range(start, end, walk);
if (ops->post_vma)
ops->post_vma(walk);
return err;
}
계층적 순회 체인 — 각 레벨에서 엔트리 존재/none/leaf 검사 후 하위 레벨로 내려감.
// mm/pagewalk.c:289-325
static int walk_pgd_range(unsigned long addr, unsigned long end,
struct mm_walk *walk)
{
pgd_t *pgd;
unsigned long next;
const struct mm_walk_ops *ops = walk->ops;
bool has_handler = ops->p4d_entry || ops->pud_entry || ops->pmd_entry ||
ops->pte_entry;
bool has_install = ops->install_pte;
int err = 0;
if (walk->pgd)
pgd = walk->pgd + pgd_index(addr);
else
pgd = pgd_offset(walk->mm, addr);
do {
next = pgd_addr_end(addr, end);
if (pgd_none_or_clear_bad(pgd)) {
if (has_install)
err = __p4d_alloc(walk->mm, pgd, addr);
else if (ops->pte_hole)
err = ops->pte_hole(addr, next, 0, walk);
if (err)
break;
if (!has_install)
continue;
}
if (ops->pgd_entry) {
err = ops->pgd_entry(pgd, addr, next, walk);
if (err)
break;
}
if (has_handler || has_install)
err = walk_p4d_range(pgd, addr, next, walk);
if (err)
break;
} while (pgd++, addr = next, addr != end);
return err;
}
공통 패턴 (모든 walk_*_range 함수):
1. 현재 레벨의 엔트리 포인터 획득 (pgd_offset, pmd_offset 등)
2. none_or_clear_bad 검사 → 없으면 __xxx_alloc() 또는 pte_hole()
3. 해당 레벨 콜백 호출 (pgd_entry, pmd_entry 등)
4. walk->action 확인:
- ACTION_SUBTREE → 하위 레벨로 내려감
- ACTION_CONTINUE → 다음 엔트리로 (하위 무시)
- ACTION_AGAIN → 다시 현재 엔트리 처리
5. 하위 레벨 함수 재귀 호출
단일 가상 주소에서 folio를 직접 찾는 간결한 API — folio_walk_end()로 해제.
// mm/pagewalk.c:969-1004
if (IS_ENABLED(CONFIG_PGTABLE_HAS_HUGE_LEAVES) &&
(!pmd_present(pmd) || pmd_leaf(pmd))) {
ptl = pmd_lock(vma->vm_mm, pmdp);
pmd = pmdp_get(pmdp);
entry_size = PMD_SIZE;
fw->level = FW_LEVEL_PMD;
fw->pmdp = pmdp;
fw->pmd = pmd;
if (pmd_none(pmd)) {
spin_unlock(ptl);
goto not_found;
} else if (pmd_present(pmd) && !pmd_leaf(pmd)) {
spin_unlock(ptl);
goto pte_table;
} else if (pmd_present(pmd)) {
page = vm_normal_page_pmd(vma, addr, pmd);
if (page)
goto found;
} else if ((flags & FW_MIGRATION) &&
pmd_is_migration_entry(pmd)) {
const softleaf_t entry = softleaf_from_pmd(pmd);
page = softleaf_to_page(entry);
expose_page = false;
goto found;
}
spin_unlock(ptl);
goto not_found;
}
walk_page_range(mm, start, end, ops, private)
│
├── check_ops_safe(ops) ← install_pte 차단
│
└── walk_page_range_mm_unsafe(mm, start, end, ops, private)
│
├── process_mm_walk_lock() ← mmap_lock 검증
│
└── VMA 순회 루프
│
├── VMA 바깥 → ops->pte_hole()
│
└── VMA 내부
│
├── process_vma_walk_lock() ← VMA 잠금
├── walk_page_test() ← test_walk 콜백
│
└── __walk_page_range(start, end, walk)
│
├── ops->pre_vma()
│
├── HugeTLB?
│ ├── YES → walk_hugetlb_range()
│ │ └── ops->hugetlb_entry()
│ └── NO → walk_pgd_range()
│ │
│ ├── ops->pgd_entry()
│ └── walk_p4d_range()
│ │
│ ├── ops->p4d_entry()
│ └── walk_pud_range()
│ │
│ ├── ops->pud_entry()
│ └── walk_pmd_range()
│ │
│ ├── ops->pmd_entry()
│ ├── split_huge_pmd()
│ └── walk_pte_range()
│ │
│ ├── ops->install_pte() 또는
│ └── ops->pte_entry()
│
└── ops->post_vma()
folio_walk_start(fw, vma, addr, flags)
│
├── pgd_offset(mm, addr) → PGD 검사
├── p4d_offset(pgdp, addr) → P4D 검사
├── pud_offset(p4dp, addr) → PUD 검사
│ └── CONFIG_PGTABLE_HAS_HUGE_LEAVES → PUD leaf 확인
│ └── vm_normal_page_pud() → found
├── pmd_offset(pudp, addr) → PMD 검사
│ └── PMD leaf 확인
│ ├── vm_normal_page_pmd() → found
│ └── is_huge_zero_pmd() → found (FW_ZEROPAGE)
└── pte_offset_map_lock() → PTE 검사
├── pte_present → vm_normal_page() → found
└── migration entry → softleaf_to_page() → found (FW_MIGRATION)
| API | 범위 | VMA 필요 | PTL 처리 | 주요 사용처 |
|---|---|---|---|---|
| `walk_page_range()` | 멀티 VMA | 아니오 | 내부 처리 | madvise, mprotect, mlock, mincore |
| `walk_page_range_vma()` | 단일 VMA | 예 | 내부 처리 | KSM, madvise |
| `walk_page_vma()` | 전체 VMA | 예 | 내부 처리 | 특수 용도 |
| `walk_page_mapping()` | address_space | 아니오 | 내부 처리 | writeback, dirty helpers |
| `walk_kernel_page_table_range()` | 커널 테이블 | 아니오 | lockless | vmalloc, ioremap |
| `folio_walk_start()` | 단일 주소 | 예 (호출 필요) | PTL 획득 | rmap, migrate, huge_memory |
| 모드 | mmap_lock | VMA lock | 사용 시나리오 |
|---|---|---|---|
| `PGWALK_RDLOCK` | 읽기 | 없음 | 일반적인 읽기 순회 (mincore, mlock) |
| `PGWALK_WRLOCK` | 쓰기 | 쓰기 획득 | 수정 필요한 순회 (mprotect, madvise FREE) |
| `PGWALK_WRLOCK_VERIFY` | 쓰기 | 쓰기 검증 | 이미 잠긴 VMA (madvise 내부) |
| `PGWALK_VMA_RDLOCK_VERIFY` | - | 읽기 검증 | VMA read lock 이미 획득됨 |
| action 값 | 동작 | 적용 레벨 |
|---|---|---|
| `ACTION_SUBTREE` (기본) | 하위 레벨로 내려감, THP 필요시 분할 | pud_entry, pmd_entry |
| `ACTION_CONTINUE` | 다음 엔트리로 이동 (하위 무시) | pud_entry, pmd_entry |
| `ACTION_AGAIN` | 현재 엔트리를 다시 처리 (동기화 재확인) | pmd_entry, pud_entry |
| 조건 | pte_hole 동작 | depth 값 |
|---|---|---|
| VMA 없음 (find_vma 실패) | pte_hole(start, end, -1) | -1 (모름) |
| VMA 바깥 gap | pte_hole(start, next, -1) | -1 (모름) |
| PMD none (install_pte 없음) | pte_hole(addr, next, 3) | 3 (PMD) |
| PUD none (install_pte 없음) | pte_hole(addr, next, 2) | 2 (PUD) |
| P4D none (install_pte 없음) | pte_hole(addr, next, 1) | 1 (P4D) |
| PGD none (install_pte 없음) | pte_hole(addr, next, 0) | 0 (PGD) |
| install_pte 설정 시 hole | 없음 (항상 엔트리 설치) | - |
| 항목 | folio_walk_start() | walk_page_range() |
|---|---|---|
| 목적 | 단일 주소 → folio 반환 | 범위 순회 + 콜백 |
| 반환값 | folio* 또는 NULL | 에러 코드 |
| PTL | caller가 folio_walk_end()로 해제 | 내부에서 pte_unmap_unlock |
| 콜백 | 없음 | ops->pte_entry 등 |
| 범위 | 하나의 주소만 | 임의 범위 |
| 사용 시나리오 | rmap, migrate, huge_memory | mlock, madvise, mincore 등 |