Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# Page Table Walk 🚶

관련 소스: mm/pagewalk.c, include/linux/pagewalk.h, mm/internal.h

개요 (Overview)

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

핵심 자료구조

struct mm_walk_ops

콜백 함수 포인터 테이블 — 호출자가 순회 동작을 커스터마이징합니다.

// 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 등)

struct mm_walk

순회 상태를 담당하는 구조체 — 콜백 간 공유됩니다.

// 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;                     // 콜백용 사용자 데이터
};

enum page_walk_action

pmd_entry/pud_entry 콜백이 반환하는 동작 제어 값:

// include/linux/pagewalk.h:100-107
enum page_walk_action {
    ACTION_SUBTREE = 0,  // 하위 레벨로 내려감 (THP 분할 포함)
    ACTION_CONTINUE = 1, // 같은 레벨의 다음 엔트리로 이동
    ACTION_AGAIN = 2     // 현재 엔트리를 다시 처리
};

enum page_walk_lock

잠금 모드 — 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 읽기 잠금 이미 획득됨 (검증만)
};

struct folio_walk

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;                         // 페이지 테이블 잠금
};

folio_walk 플래그

// include/linux/pagewalk.h:155-158
#define FW_MIGRATION  BIT(0)  // migration 엔트리도 순회
#define FW_ZEROPAGE   BIT(1)  // 공유 zero page도 반환

핵심 함수

1. walk_page_range()

외부 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);
}
  • 반환값: 0=성공, <0=에러, >0=콜백 정의 조기 종료
  • 잠금: 호출자가 mm->mmap_lock 보유 필요
  • 보안: install_pte가 설정된 ops는 외부 API에서 차단
  • 2. walk_page_range_mm_unsafe()

    실제 순회 로직 — 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);
  • VMA 탐색: find_vma()로 시작 주소의 VMA를 찾고, VMA 간 gap은 pte_hole로 처리
  • VMA 바깥 영역: pte_hole(start, next, -1, &walk) 호출
  • VMA 내부: walk_page_test()로 VMA 검증 후 __walk_page_range() 호출
  • 3. __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;
    }

    4. walk_pgd_range() → walk_p4d_range() → walk_pud_range() → walk_pmd_range() → walk_pte_range()

    계층적 순회 체인 — 각 레벨에서 엔트리 존재/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. 하위 레벨 함수 재귀 호출

    5. folio_walk_start()

    단일 가상 주소에서 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;
    }
  • 반환: 매핑된 folio, 없으면 NULL
  • 특징: VMA, addr, flags로 직접 folio 획득, PTL 유지
  • folio_walk_end(): PTL 해제 + pte_unmap + vma_pgtable_walk_end

  • 호출 흐름

    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() 호출 흐름

    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)

    조건별 비교

    1. API 유형별 비교

    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()`커널 테이블아니오locklessvmalloc, ioremap
    `folio_walk_start()`단일 주소예 (호출 필요)PTL 획득rmap, migrate, huge_memory

    2. walk_lock 모드별 비교

    모드mmap_lockVMA lock사용 시나리오
    `PGWALK_RDLOCK`읽기없음일반적인 읽기 순회 (mincore, mlock)
    `PGWALK_WRLOCK`쓰기쓰기 획득수정 필요한 순회 (mprotect, madvise FREE)
    `PGWALK_WRLOCK_VERIFY`쓰기쓰기 검증이미 잠긴 VMA (madvise 내부)
    `PGWALK_VMA_RDLOCK_VERIFY`-읽기 검증VMA read lock 이미 획득됨

    3. walk->action별 동작

    action 값동작적용 레벨
    `ACTION_SUBTREE` (기본)하위 레벨로 내려감, THP 필요시 분할pud_entry, pmd_entry
    `ACTION_CONTINUE`다음 엔트리로 이동 (하위 무시)pud_entry, pmd_entry
    `ACTION_AGAIN`현재 엔트리를 다시 처리 (동기화 재확인)pmd_entry, pud_entry

    4. hole 처리 방식 비교

    조건pte_hole 동작depth 값
    VMA 없음 (find_vma 실패)pte_hole(start, end, -1)-1 (모름)
    VMA 바깥 gappte_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없음 (항상 엔트리 설치)-

    5. folio_walk_start() vs walk_page_range()

    항목folio_walk_start()walk_page_range()
    목적단일 주소 → folio 반환범위 순회 + 콜백
    반환값folio* 또는 NULL에러 코드
    PTLcaller가 folio_walk_end()로 해제내부에서 pte_unmap_unlock
    콜백없음ops->pte_entry 등
    범위하나의 주소만임의 범위
    사용 시나리오rmap, migrate, huge_memorymlock, madvise, mincore 등

    관련 문서

  • 메모리 관리 개요
  • 03 — VMA / mmap
  • 10 — Huge Pages / THP
  • 12 — Folio / Page Cache
  • 20 — Migration
  • 28 — Reverse Mapping
  • 33 — GUP (Get User Pages)
  • 36 — mprotect
  • 37 — mremap
  • 39 — vmpressure