Ryotta's Linux 7.0 MM

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

페이지 테이블 검증 (Page Table Check)

개요 (Overview)

Linux 커널의 페이지 테이블 검증(Page Table Check)은 사용자 페이지 테이블에 대한 잘못된 매핑을 실시간으로 감지하는 디버깅 메커니즘입니다. 이 기능은 anonymous 페이지의 이중 쓰기 매핑(Double Write Mapping)과 anonymous/file 페이지의 부당한 공유를 탐지하여, 메모리 누수, 데이터 손상, 무결성 문제를 동기적으로 발견합니다.

페이지 테이블 검증은 page_table_check.c에서 구현되며, 페이지 테이블 엔트리(PTE/PMD/PUD)가 설정되거나 해제될 때마다 호출되어 카운터 기반의 일관성 검사를 수행합니다. 이 메커니즘은 CONFIG_PAGE_TABLE_CHECK 커널 컨피그 옵션으로 제어되며, x86_64, ARM64, RISC-V, PowerPC 아키텍처를 지원합니다. page_table_check=on 부트 파라미터 또는 CONFIG_PAGE_TABLE_CHECK_ENFORCED 옵션으로 활성화할 수 있습니다.

소스 파일 경로:
├── mm/page_table_check.c           ← 검증 코어 로직
├── include/linux/page_table_check.h ← 외부 인터페이스
├── include/linux/page_ext.h         ← 페이지 확장 프레임워크
├── include/linux/leafops.h          ← 소프트 리프 엔트리 연산
└── mm/Kconfig.debug                 ← 커널 컨피그 옵션

빠른 점검 명령

# 페이지 테이블 검증 활성화 상태 확인
cat /proc/cmdline | grep page_table_check

# 커널 컨피그에서 PAGE_TABLE_CHECK 확인
grep PAGE_TABLE_CHECK /boot/config-$(uname -r)

# 페이지 테이블 검증 관련 커널 로그 확인
dmesg | grep -i "page_table_check"

# page_ext 메모리 사용량 확인
cat /proc/meminfo | grep -i "page_tables"

# 페이지 테이블 관련 통계 확인
cat /proc/vmstat | grep -i "pgfault\|pgmajfault"

# 페이지 테이블 검증 활성화 (부트 파라미터)
# GRUB 설정에 추가: page_table_check=on

# 현재 페이지 테이블 검증 상태 확인 (커널 빌드 시)
grep CONFIG_PAGE_TABLE_CHECK /boot/config-$(uname -r)

# 페이지 확장 메모리 사용량 확인
cat /sys/kernel/debug/page_ext/

# 페이지 테이블 엔트리 수 확인
cat /proc/vmstat | grep -i "nr_pte\|nr_pmd\|nr_pud"

핵심 자료구조

1. `struct page_table_check` (mm/page_table_check.c:16)

struct page_table_check {
	atomic_t anon_map_count;   // anonymous 페이지 매핑 카운터
	atomic_t file_map_count;   // 파일 기반 페이지 매핑 카운터
};
// 각 페이지마다 page_ext에 연결되어 관리됨
// anon과 file은 서로 배타적이어야 함 (동시 증가 시 BUG_ON)

2. `struct page_ext_operations` (include/linux/page_ext.h:25)

struct page_ext_operations {
	size_t offset;             // page_ext 내 클라이언트 데이터 오프셋
	size_t size;               // 클라이언트 데이터 크기
	bool (*need)(void);        // page_ext 필요 여부 함수 포인터
	void (*init)(void);        // 초기화 함수 포인터 (선택)
	bool need_shared_flags;    // 공유 플래그 필드 사용 여부
};
// page_table_check_ops는 이 구조체를 통해 page_ext 프레임워크에 등록됨

3. `struct page_ext` (include/linux/page_ext.h:52)

struct page_ext {
	unsigned long flags;       // 페이지 확장 플래그
};
// 모든 페이지에 대해 page_ext가 할당되며, pfn에 대응됨
// page_table_check는 이 구조체의 확장 데이터로 저장됨

4. `struct page_ext_iter` (include/linux/page_ext.h:113)

struct page_ext_iter {
	unsigned long index;       // 현재 순회 인덱스
	unsigned long start_pfn;   // 시작 페이지 프레임 번호
	struct page_ext *page_ext; // 현재 page_ext 포인터
};
// for_each_page_ext 매크로에서 사용되는 이터레이터

5. `page_table_check_disabled` (mm/page_table_check.c:24)

DEFINE_STATIC_KEY_TRUE(page_table_check_disabled);
// 정적 키 기반 분기 최적화
// 기본값: true (검증 비활성화)
// init_page_table_check()에서 false로 변경 시 검증 활성화

6. `__page_table_check_enabled` (mm/page_table_check.c:21)

static bool __page_table_check_enabled __initdata =
				IS_ENABLED(CONFIG_PAGE_TABLE_CHECK_ENFORCED);
// 커널 부팅 시 초기화되는 활성화 플래그
// CONFIG_PAGE_TABLE_CHECK_ENFORCED: 항상 활성화
// page_table_check=on 부트 파라미터: 선택적 활성화

핵심 함수

1. `page_table_check_clear()` (mm/page_table_check.c:63)

static void page_table_check_clear(unsigned long pfn, unsigned long pgcnt)
{
	struct page_ext_iter iter;
	struct page_ext *page_ext;
	struct page *page;
	bool anon;

	if (!pfn_valid(pfn))
		return;

	page = pfn_to_page(pfn);
	BUG_ON(PageSlab(page));          // Slab 페이지는 검증 대상 아님
	anon = PageAnon(page);           // anonymous/file 구분

	rcu_read_lock();
	for_each_page_ext(page, pgcnt, page_ext, iter) {
		struct page_table_check *ptc = get_page_table_check(page_ext);

		if (anon) {
			BUG_ON(atomic_read(&ptc->file_map_count));      // anonymous인데 file 카운터 존재 시 BUG
			BUG_ON(atomic_dec_return(&ptc->anon_map_count) < 0); // 음수 카운터 시 BUG
		} else {
			BUG_ON(atomic_read(&ptc->anon_map_count));      // file인데 anon 카운터 존재 시 BUG
			BUG_ON(atomic_dec_return(&ptc->file_map_count) < 0); // 음수 카운터 시 BUG
		}
	}
	rcu_read_unlock();
}
// 역할: PTE/PMD/PUD 제거 시 해당 페이지의 매핑 카운터를 감소시키고 검증
// 호출자: __page_table_check_pte_clear, __page_table_check_pmd_clear, __page_table_check_pud_clear

2. `page_table_check_set()` (mm/page_table_check.c:97)

static void page_table_check_set(unsigned long pfn, unsigned long pgcnt,
				 bool rw)
{
	struct page_ext_iter iter;
	struct page_ext *page_ext;
	struct page *page;
	bool anon;

	if (!pfn_valid(pfn))
		return;

	page = pfn_to_page(pfn);
	BUG_ON(PageSlab(page));
	anon = PageAnon(page);

	rcu_read_lock();
	for_each_page_ext(page, pgcnt, page_ext, iter) {
		struct page_table_check *ptc = get_page_table_check(page_ext);

		if (anon) {
			BUG_ON(atomic_read(&ptc->file_map_count));      // anonymous인데 file 카운터 존재 시 BUG
			BUG_ON(atomic_inc_return(&ptc->anon_map_count) > 1 && rw); // anonymous 쓰기 이중 매핑 시 BUG
		} else {
			BUG_ON(atomic_read(&ptc->anon_map_count));      // file인데 anon 카운터 존재 시 BUG
			BUG_ON(atomic_inc_return(&ptc->file_map_count) < 0); // 음수 카운터 시 BUG
		}
	}
	rcu_read_unlock();
}
// 역할: PTE/PMD/PUD 설정 시 해당 페이지의 매핑 카운터를 증가시키고 검증
// anonymous 페이지의 쓰기 이중 매핑 감지 (anon_map_count > 1 && rw)

3. `__page_table_check_zero()` (mm/page_table_check.c:131)

void __page_table_check_zero(struct page *page, unsigned int order)
{
	struct page_ext_iter iter;
	struct page_ext *page_ext;

	BUG_ON(PageSlab(page));

	rcu_read_lock();
	for_each_page_ext(page, 1 << order, page_ext, iter) {
		struct page_table_check *ptc = get_page_table_check(page_ext);

		BUG_ON(atomic_read(&ptc->anon_map_count));   // 매핑 카운터가 0이 아니면 BUG
		BUG_ON(atomic_read(&ptc->file_map_count));   // 매핑 카운터가 0이 아니면 BUG
	}
	rcu_read_unlock();
}
// 역할: 페이지 할당 시 또는 해제 시 카운터가 0인지 확인
// 호출자: page_table_check_alloc, page_table_check_free

4. `__page_table_check_ptes_set()` (mm/page_table_check.c:202)

void __page_table_check_ptes_set(struct mm_struct *mm, unsigned long addr,
				 pte_t *ptep, pte_t pte, unsigned int nr)
{
	unsigned int i;

	if (&init_mm == mm)           // init_mm는 검증 대상 아님
		return;

	page_table_check_pte_flags(pte);  // uffd_wp 플래그 검증

	for (i = 0; i < nr; i++)
		__page_table_check_pte_clear(mm, addr + PAGE_SIZE * i, ptep_get(ptep + i));
	if (pte_user_accessible_page(pte, addr))
		page_table_check_set(pte_pfn(pte), nr, pte_write(pte));
}
// 역할: PTE 배치 시 기존 PTE 해제 및 새 PTE 설정 검증
// 페이지 폴트 처리, mmap, fork 등에서 호출됨

5. `__page_table_check_pmds_set()` (mm/page_table_check.c:231)

void __page_table_check_pmds_set(struct mm_struct *mm, unsigned long addr,
		pmd_t *pmdp, pmd_t pmd, unsigned int nr)
{
	unsigned long stride = PMD_SIZE >> PAGE_SHIFT;
	unsigned int i;

	if (&init_mm == mm)
		return;

	page_table_check_pmd_flags(pmd);  // uffd_wp 플래그 검증

	for (i = 0; i < nr; i++)
		__page_table_check_pmd_clear(mm, addr + PMD_SIZE * i, *(pmdp + i));
	if (pmd_user_accessible_page(pmd, addr))
		page_table_check_set(pmd_pfn(pmd), stride * nr, pmd_write(pmd));
}
// 역할: PMD 배치 시 기존 PMD 해제 및 새 PMD 설정 검증
// THP(Transparent Huge Page) 할당 시 호출됨

6. `__page_table_check_pte_clear_range()` (mm/page_table_check.c:265)

void __page_table_check_pte_clear_range(struct mm_struct *mm,
					unsigned long addr,
					pmd_t pmd)
{
	if (&init_mm == mm)
		return;

	if (!pmd_bad(pmd) && !pmd_leaf(pmd)) {
		pte_t *ptep = pte_offset_map(&pmd, addr);
		unsigned long i;

		if (WARN_ON(!ptep))
			return;
		for (i = 0; i < PTRS_PER_PTE; i++) {
			__page_table_check_pte_clear(mm, addr, ptep_get(ptep));
			addr += PAGE_SIZE;
			ptep++;
		}
		pte_unmap(ptep - PTRS_PER_PTE);
	}
}
// 역할: PMD에 속한 모든 PTE를 범위 단위로 해제 검증
// 호출자: khugepaged (THP 병합/해제 시)

호출 흐름

PTE 설정 시 검증 흐름

페이지 폴트 / mmap / fork
    │
    ▼
__page_table_check_ptes_set()
    │
    ├── page_table_check_pte_flags()     ← uffd_wp 플래그 검증
    │
    ├── __page_table_check_pte_clear()   ← 기존 PTE 해제 검증
    │       │
    │       ▼
    │   page_table_check_clear()         ← anon/file 카운터 감소
    │       │
    │       ▼
    │   atomic_dec_return()              ← 음수 카운터 검증
    │
    └── page_table_check_set()           ← 새 PTE 설정 검증
            │
            ▼
        atomic_inc_return()              ← 이중 매핑 검증

페이지 할당/해제 시 검증 흐름

__alloc_pages() / __free_pages()
    │
    ▼
page_table_check_alloc() / page_table_check_free()
    │
    ▼
__page_table_check_zero()
    │
    ▼
atomic_read(anon_map_count) == 0?  ── 아니면 → BUG_ON
atomic_read(file_map_count) == 0?  ── 아니면 → BUG_ON

THP 병합/해제 시 검증 흐름

khugepaged (THP 병합/해제)
    │
    ▼
page_table_check_pte_clear_range()
    │
    ▼
PMD 유효성 검사 (!pmd_bad && !pmd_leaf)
    │
    ▼
PTRS_PER_PTE 순회
    │
    ├── __page_table_check_pte_clear()   ← 각 PTE 해제 검증
    │       │
    │       ▼
    │   page_table_check_clear()         ← 카운터 감소
    │
    └── 완료

조건별 비교

1. 검증 대상별 비교

구분PTE (4KB)PMD (2MB)PUD (1GB)
**검증 함수**`__page_table_check_ptes_set``__page_table_check_pmds_set``__page_table_check_puds_set`
**플래그 검증**`page_table_check_pte_flags``page_table_check_pmd_flags`없음
**페이지 수 계산**`nr` (직접)`stride * nr` (PMD_SIZE >> PAGE_SHIFT)`stride * nr` (PUD_SIZE >> PAGE_SHIFT)
**사용 시나리오**일반 페이지 폴트, mmapTHP 할당HugeTLB 할당
**호출 빈도**가장 높음중간낮음

2. 활성화 방식 비교

방식컨피그 옵션부트 파라미터동작
**기본 비활성화**`CONFIG_PAGE_TABLE_CHECK=y`없음필요 시 `page_table_check=on`으로 활성화
**강제 활성화**`CONFIG_PAGE_TABLE_CHECK_ENFORCED=y`불필요항상 활성화
**동적 활성화**`CONFIG_PAGE_TABLE_CHECK=y``page_table_check=on`부팅 시 동적 활성화

3. 아키텍처 지원 비교

아키텍처지원 여부컨피그 위치비고
**x86_64**지원`arch/x86/Kconfig`64비트만 지원
**ARM64**지원`arch/arm64/Kconfig`전체 지원
**RISC-V**지원`arch/riscv/Kconfig`MMU 필요
**PowerPC**조건부 지원`arch/powerpc/Kconfig`HugeTLB 미사용 시 지원
**32비트**미지원--

4. 플래그 검증 조건 비교

플래그PTE 검증PMD 검증동작
**pte_uffd_wp**`WARN_ON_ONCE(pte_uffd_wp && pte_write)`-uffd-wp와 쓰기 동시 설정 시 경고
**pte_swp_uffd_wp**`WARN_ON_ONCE(cached_writable)`-스왑된 uffd-wp에 쓰기 가능 시 경고
**pmd_uffd_wp**-`WARN_ON_ONCE(pmd_uffd_wp && pmd_write)`PMD 수준 uffd-wp 검증
**pmd_swp_uffd_wp**-`WARN_ON_ONCE(cached_writable)`PMD 스왑 uffd-wp 검증

카운터 기반 검증 원리

anonymous/file 배타성 검증

모든 페이지는 다음 중 하나여야 함:
  1. anonymous 매핑만 있음 (anon_map_count >= 0, file_map_count == 0)
  2. file 매핑만 있음 (anon_map_count == 0, file_map_count >= 0)

위반 시 BUG_ON() 발생:
  - anon인데 file_map_count > 0 → BUG
  - file인데 anon_map_count > 0 → BUG

쓰기 이중 매핑 감지

anonymous 페이지의 경우:
  - anon_map_count > 1 && rw == true → BUG_ON 발생
  - 즉, 하나의 anonymous 페이지를 두 프로세스가 동시에 쓰기로 매핑하는 것 방지

file 페이지의 경우:
  - file_map_count 증가만 검증 (쓰기 이중 매핑 검증 없음)
  - file 페이지는 COW로 보호되므로 이중 쓰기 매핑이 발생하지 않음

빈 카운터 검증 (할당/해제 시)

페이지가 free list에 있거나 할당되는 시점:
  - anon_map_count == 0 이어야 함
  - file_map_count == 0 이어야 함
  - 위반 시 BUG_ON() 발생 (메모리 누수 또는 초기화 실패 감지)

관련 문서

  • 00 — 메모리 관리 개요
  • 03 — VMA / mmap
  • 10 — Huge Pages / THP
  • 12 — Folio / Page Cache
  • 27 — 디버그 도구
  • 38 — Page Table Walk

  • SVG 다이어그램

    자료구조 관계도

    페이지 테이블 검증 자료구조 관계도

    호출 흐름

    페이지 테이블 검증 호출 흐름