주소에서 PTE pointer를 찾았다고 그 entry를 바로 수정해도 될까요? 다른 CPU가 그 사이 상위 table을 바꿀 수 있고, 수정 뒤에도 TLB는 이전 translation을 보유할 수 있습니다. table의 위치, entry의 일관성, cached translation은 서로 다른 보호가 필요합니다.
이 차이를 이해하려면 page-table helper를 주소 계산 함수 목록으로만 읽지 않아야 합니다. 어떤 helper가 allocation을 하는지, 어떤 lock을 돌려주는지, 결과를 언제까지 사용할 수 있는지를 함께 보겠습니다. 현재 API 설명은 upstream Linux v6.18 기준입니다.
offset helper와 상태 검사
walker는 root에서 시작해 주소의 해당 bit로 entry를 고릅니다. pgd_offset()은 mm의 PGD를, pgd_offset_k()는 kernel 쪽 root를 사용하는 계열입니다. p4d_offset(), pud_offset(), pmd_offset()은 다음 level로 내려갑니다. folded level도 같은 공통 인터페이스로 통과할 수 있습니다.
offset을 계산했다는 사실은 entry가 present이거나 다음 table이 존재한다는 뜻이 아닙니다. pXX_none()으로 부재를, 해당 level의 pXX_bad()로 부적절한 상태를 확인해야 합니다. pXX_offset() 자체를 empty 검사 함수처럼 읽어서는 안 됩니다.
PUD·PMD가 huge leaf이면 하위 table pointer로 해석하지 않습니다. leaf의 PFN과 주소의 큰 offset을 조합해야 합니다. 일반 PTE walk만 지원하는 helper와 huge leaf까지 지원하는 helper의 계약도 다릅니다.
helper의 이름도 어떤 종류의 결과를 돌려주는지 나누어 읽으면 덜 헷갈립니다. pXX_index()는 table 안의 번호를, pXX_offset()은 선택한 entry의 pointer를 계산합니다. pXX_addr_end()는 범위 walk에서 현재 level의 경계를 넘지 않는 다음 끝을 구하는 계열입니다. PFN을 꺼내는 pXX_pfn()과 flags를 꺼내는 pXX_flags()는 entry의 내용을 분해하는 작업입니다. pXX_page()가 가리키는 객체는 non-leaf에서는 다음 table page일 수 있으므로 언제나 최종 application data page라고 읽으면 안 됩니다.
반대 방향에는 PFN과 protection으로 entry 값을 만드는 pfn_pte()·pfn_pmd()·pfn_pud(), page 객체에서 출발하는 mk_pte()·mk_pmd() 계열이 있습니다. pXX_val()·__pXX()의 raw 변환, pXX_same()의 encoded entry 비교와도 목적이 다릅니다. 같은 PFN을 갖더라도 permission이나 software 상태가 다르면 같은 entry가 아닐 수 있습니다. 값 구성 helper는 allocation도 lock 획득도 하지 않으며, 모든 level에서 같은 constructor가 제공되는 것도 아닙니다.
pte_modify()·pmd_modify()처럼 protection을 바꾼 값을 만드는 작업과 set_pte_at() 같은 live slot 설치도 구별해야 합니다. _at 인자가 있다고 caller의 VMA permission, lock, lifetime을 전부 검증한다는 뜻은 아닙니다. pXX_clear() 역시 entry를 비우는 작업이지 하위 table page 해제와 TLB flush를 합친 작업이 아닙니다. 이 역할 구분은 x86 helper와 공통 page-table 인터페이스를 함께 읽을 때 필요합니다.
교과서적인 하향 구조를 다음처럼 생각할 수 있습니다. 구현코드가 아닌 개념 순서이며 특정 함수가 이 조건을 모두 같은 방식으로 처리한다는 뜻은 아닙니다.
root for the selected mm
PGD entry: absent / invalid / next table
P4D entry: absent / invalid / next table
PUD entry: absent / leaf / next table
PMD entry: absent / leaf / next table
PTE: empty / nonpresent encoding / present mapping
범위 helper는 끝점의 산술까지 포함한 계약입니다. v6.18의 pgd_addr_end(addr, end)는 다음 PGD 경계인 boundary를 만든 뒤 boundary - 1 < end - 1을 비교합니다. 보통의 유효한 nonempty 범위를 순회할 때 요청 끝을 넘지 않으면서, 최상단 경계를 넘겨 boundary가 unsigned 0으로 wrap한 경우도 처리하려는 식입니다. 단순히 min(boundary, end)로 바꾸면 wrap한 0을 더 작은 정상 주소로 잘못 고릅니다. P4D·PUD·PMD에도 같은 계열이 있으며, 임의의 empty range를 받아 정규화해 주는 함수는 아닙니다.
folding은 상태 검사에도 영향을 줍니다. v6.18 x86에서 runtime 5-level paging이 꺼진 경우 pgd_present()는 1, pgd_none()·pgd_bad()는 0을 반환하는 분기가 있습니다. p4d_offset()도 이때 별도 table을 읽지 않고 같은 slot을 P4D pointer로 취급합니다. 따라서 folded PGD의 none 결과만 보고 실제 다음 table이 존재한다고 결론 내릴 수 없습니다. 실제 level의 entry를 검사해야 합니다. pgd_same()·p4d_same()의 raw encoded 값 비교 역시 backing page의 lifetime이나 table 구조 전체의 동일성을 보증하지 않습니다. 이는 확인한 x86 runtime-folded 경로의 설명이며 모든 architecture의 compile-time folding 구현을 하나로 단정하지 않습니다.
helper를 찾아볼 때에는 다음처럼 작업별로 묶으면 좋습니다. 이름의 pXX를 아무 level로 치환할 수 있다는 뜻은 아닙니다.
| 작업 | 대표 helper | 호출 뒤에도 남는 책임 |
|---|---|---|
| 값 분해·구성 | pXX_val, __pXX, pXX_pfn, pXX_pgprot |
encoded kind와 leaf 여부를 먼저 정합니다. PGD·P4D의 flags helper는 pgd_flags, p4d_flags입니다. |
| entry 값 변환 | pud_set_flags, pmd_set_flags, pte_set_flags와 대응 clear_flags |
반환된 값을 live slot에 설치하고 동기화하는 일은 별개입니다. |
| live slot 변경 | set_pgd, set_p4d, set_pud, set_pmd, set_pte |
해당 slot의 lock·ordering·TLB 계약을 caller가 지킵니다. _at suffix도 범용 validator라는 뜻이 아닙니다. |
| table 준비·연결 | pgd_alloc, p4d_alloc, pud_alloc, pmd_alloc, pte_alloc, pXX_populate |
root 생성과 child 준비를 구별하며 실패·경쟁·초기화 순서를 처리합니다. |
| 저장소 반환 | pgd_free, p4d_free, pud_free, pmd_free, pte_free |
live walk에서 분리하고 안전한 시점에 반환해야 합니다. entry clear만으로 이를 대신하지 않습니다. |
pfn_pud()·pfn_pmd()·pfn_pte()는 PFN과 protection을 합칩니다. mk_pmd()·mk_pte()는 page에서 출발하고, pmd_modify()·pte_modify()는 보존해야 할 기존 entry 상태와 새 protection을 결합합니다. PTE에는 더 아래 table을 연결하는 pte_populate() 단계가 없습니다. pte_page()도 special·nonpresent encoding에 무조건 적용할 수 있는 object lookup은 아닙니다. 공통 helper 정의와 x86 정의는 이런 비대칭을 확인할 기준입니다.
조회 결과가 허용하는 일은 제한적입니다
예전 kernel의 follow_pte()는 manual walk와 PTE lock을 이해하기 좋은 사례입니다. 그러나 이것을 일반 RAM page를 오래 보유하는 API로 바꿔 읽을 수는 없습니다. entry를 조사하는 일과 backing page의 reference를 확보하는 일은 별개입니다.
그 예전 형태의 walk에서는 PGD·P4D·PUD·PMD를 고를 때마다 none·bad 상태를 만나면 실패로 빠집니다. PMD huge mapping을 일반 PTE table로 내려가지 않는다는 전제도 있습니다. 마지막 PTE는 mapping과 lock을 함께 얻고 present를 확인합니다. 실패하면 그 mapping과 lock을 풀고 -EINVAL을 반환하지만, 성공하면 PTE pointer와 lock을 caller에게 넘깁니다. 따라서 성공 return이 모든 보호를 해제했다는 뜻이 아닙니다. caller의 unlock 책임까지 포함해서 읽어야 하는 API이며, 이 흐름 자체가 page reference 획득을 대신하지는 않습니다.
그 형태를 실제로 읽을 수 있도록 공식 v6.0의 전체 follow_pte()를 아래에 제공합니다. 당시 kernel-doc의 입력 계약은 IO mapping 또는 raw PFN mapping만 허용하고 caller가 mmap read lock을 보유하는 것입니다. PMD의 VM_BUG_ON(pmd_trans_huge(*pmd))는 huge mapping을 지원하는 fallback이 아니라 이 walker의 전제 검사입니다. 성공하면 caller가 pte_unmap_unlock()을 수행할 때까지 PTE mapping과 lock이 남습니다. 그 뒤에도 결과를 사용하는 경우에는 MMU notifier 등 invalidation 보호가 별도로 필요합니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds */
/*
* Linux v6.0, mm/memory.c:5269-5305
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/memory.c
* Complete function: follow_pte
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
int follow_pte(struct mm_struct *mm, unsigned long address,
pte_t **ptepp, spinlock_t **ptlp)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep;
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto out;
p4d = p4d_offset(pgd, address);
if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
goto out;
pud = pud_offset(p4d, address);
if (pud_none(*pud) || unlikely(pud_bad(*pud)))
goto out;
pmd = pmd_offset(pud, address);
VM_BUG_ON(pmd_trans_huge(*pmd));
if (pmd_none(*pmd) || unlikely(pmd_bad(*pmd)))
goto out;
ptep = pte_offset_map_lock(mm, pmd, address, ptlp);
if (!pte_present(*ptep))
goto unlock;
*ptepp = ptep;
return 0;
unlock:
pte_unmap_unlock(ptep, *ptlp);
out:
return -EINVAL;
}
이는 GPL-2.0-only의 역사적 전체 함수이며 copyright notice를 유지했습니다. 출처·버전·범위와 license text를 제공합니다. v6.0이 책의 정확한 source tag라고 주장하지 않으며, 이 함수를 현재 driver에 그대로 사용하라는 예제도 아닙니다. kernel 내부 code이고 실행하지 않았습니다.
현재 v6.18의 mm/memory.c에는 같은 공개 함수 이름 대신 follow_pfnmap_start()·follow_pfnmap_end()의 계약을 확인할 수 있습니다. 시작 전에 VMA와 주소를 준비하고 성공한 start와 end 사이에서만 결과를 사용합니다. 대상은 허용된 IO·raw PFN mapping이며 이 API로 PTE를 수정해서는 안 됩니다.
mmap read 보호도 end 전에 놓을 수 없습니다. end 뒤에 PFN을 계속 이용하려면 caller가 추가 동기화를 갖춰야 합니다. refcounted page를 나중에 접근하는 상황에서는 MMU notifier 등 invalidation과의 관계도 필요합니다. lock이 풀린 뒤의 PFN 숫자가 여전히 같은 객체의 안전한 handle이라는 보장은 없습니다.
이 version 차이는 이름이 비슷한 helper를 임의로 교체하면 안 되는 이유입니다. manual walk를 배우는 목적, driver의 PFN 조회, GUP의 page reference 확보는 같은 작업이 아닙니다.
table allocation은 게시 전에 초기화합니다
다음 level이 없을 때 사용하는 p4d_alloc()·pud_alloc()·pmd_alloc() 계열은 단순한 offset 계산보다 많은 일을 합니다. 필요한 table page를 얻고 초기화한 뒤 상위 entry에서 보이게 해야 합니다. 다른 CPU가 반쯤 초기화된 table을 따라가서는 안 됩니다.
동시에 같은 위치에 table을 준비하는 CPU가 있을 수도 있습니다. 잠들 수 있는 allocation을 먼저 수행하고, 상위 entry를 보호하는 lock 아래에서 다시 부재를 확인한 뒤 자신의 table을 설치하는 형태가 필요합니다. 경쟁에서 이미 다른 table이 설치됐다면 여분 allocation을 반환해야 합니다.
root 자체를 만드는 pgd_alloc()과 이미 존재하는 root 아래에서 다음 level을 준비하는 allocation도 다릅니다. pXX_alloc_one()은 새 table 저장소를 준비하는 쪽이고, pXX_populate()는 부모 entry를 그 table에 연결하는 쪽입니다. 대응 pXX_free()는 table 저장소의 반환이며 data page 해제와 같지 않습니다. live table을 제거하는 문맥에서는 hardware walk와 TLB가 더 이상 접근하지 않는 시점까지 포함한 teardown 경로를 사용해야 합니다.
현재 __p4d_alloc()·__pud_alloc()은 mm->page_table_lock으로 게시를 보호합니다. __pmd_alloc()은 pud_lock()을, __pte_alloc()이 호출하는 pmd_install()은 pmd_lock()을 통해 부모 entry를 보호합니다. 새 table을 clear하고 lock을 초기화한 결과가 부모 pointer보다 먼저 보이도록 하는 memory ordering도 필요합니다. allocation 실패의 -ENOMEM과 경쟁에서 진 뒤 여분 table을 반환하는 성공 경로를 같은 실패로 취급해서는 안 됩니다. memory.c의 완전한 함수 정의에서 이 분기들을 확인할 수 있습니다.
pXX_alloc_track()은 allocation 여부뿐 아니라 어느 level이 바뀌었는지 mask에 기록하는 계열입니다. kernel mapping에서 다른 root에 상위 변경을 반영해야 하는 architecture가 이 정보를 이용합니다. table을 할당한 사실과 모든 process의 kernel root가 그 table에 도달할 수 있다는 사실을 구별하는 장치입니다.
PTE 쪽에는 구체적으로 pte_alloc_kernel_track()이 있습니다. 현재 pgalloc-track.h는 kernel PTE table이 없으면 __pte_alloc_kernel()을 호출하고, 성공하면 PMD 변경을 기록한 뒤 pte_offset_kernel() 결과를 반환합니다. user table에서 mapping과 PTE lock을 한꺼번에 얻는 pte_offset_map_lock()과 같은 API가 아닙니다. allocation을 tracking하는지, 일시적인 kernel mapping을 얻는지, entry lock까지 보유하는지를 이름 한 부분만으로 추측하지 않아야 합니다.
PTE table에는 mapping과 lock 획득까지 결합한 helper가 있습니다. pte_alloc_map_lock()은 필요하면 table을 준비하고 해당 lock을 얻는 계약이며, pte_offset_map_lock()은 기존 table의 mapping과 lock을 얻습니다. NULL 반환을 무시하고 pointer를 dereference하면 allocation 실패나 동시 table 변경을 잘못 처리하게 됩니다.
split lock을 선택하는 기준
mm의 모든 page-table 갱신을 mm->page_table_lock 하나로 막으면 thread가 서로 다른 주소에서 fault해도 경쟁합니다. split page-table lock은 PTE table별, 지원되는 경우 PMD table별 lock으로 이 직렬화 범위를 줄입니다. 더 높은 level의 table은 여전히 mm 단위 lock을 사용하는 부분이 있습니다.
PTE split 여부는 runtime에서 현재 online CPU 수를 매번 세어 결정하는 것이 아닙니다. CONFIG_SPLIT_PTLOCK_CPUS와 build의 NR_CPUS를 비교하는 compile-time 조건입니다. PMD split에는 architecture 지원도 필요합니다. SMP 장비라는 사실만으로 모든 level에 split lock이 있다고 말할 수 없습니다.
역사적 표기를 읽을 기준으로 v6.0 mm_types_task.h의 세 macro 전체를 함께 보겠습니다. 두 번째 macro의 줄 연결까지 하나의 정의입니다. ALLOC_SPLIT_PTLOCKS는 split 여부 자체가 아니라 lock을 위한 별도 storage가 필요한지를 판단합니다. 기준은 SPINLOCK_SIZE가 BITS_PER_LONG / 8 byte보다 큰지입니다. CPU 수 조건과 lock 객체의 크기 조건을 같은 것으로 읽으면 안 됩니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.0, include/linux/mm_types_task.h:22-25
* Source: https://github.com/torvalds/linux/blob/v6.0/include/linux/mm_types_task.h
* Complete macro_block: split_ptlock_macros
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
#define USE_SPLIT_PTE_PTLOCKS (NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS)
#define USE_SPLIT_PMD_PTLOCKS (USE_SPLIT_PTE_PTLOCKS && \
IS_ENABLED(CONFIG_ARCH_ENABLE_SPLIT_PMD_PTLOCK))
#define ALLOC_SPLIT_PTLOCKS (SPINLOCK_SIZE > BITS_PER_LONG/8)
이 block은 header의 GPL-2.0 표시를 유지한 공식 v6.0 비교용 code입니다. 현재 v6.18의 설정 이름으로 재작성한 pseudo-code가 아닙니다.
lock 저장 방식도 configuration에 따라 다릅니다. spinlock이 정해진 storage에 들어가면 inline으로, 더 크면 별도 allocation을 통해 보관할 수 있습니다. DEBUG_SPINLOCK·lockdep 같은 설정이 이 크기에 영향을 줍니다. table constructor의 lock 준비도 실패할 수 있으므로 data page allocation만 검사해서는 충분하지 않습니다.
이 때문에 descriptor의 ptl union을 직접 만지기보다 lock helper를 사용해야 합니다. pte_offset_map_ro_nolock()·pte_offset_map_rw_nolock()이 lock pointer를 반환해도 lock을 획득했다는 뜻은 아닙니다. table의 mapping, 대응 lock을 알아내는 일과 실제 lock 획득을 구분해야 합니다.
lock pointer를 구하는 helper는 특히 level을 바꾸어 읽으면 안 됩니다. v6.18의 pte_lockptr(mm, pmd)는 split 설정일 때 PMD가 가리키는 자식 PTE table의 ptdesc에서 lock을 얻습니다. 반면 pmd_lockptr(mm, pmd)는 그 PMD entry를 담고 있는 PMD table의 descriptor를 사용합니다. 두 함수 모두 pointer를 반환할 뿐 lock을 잡지 않습니다. nonsplit 구성에서는 둘 다 mm->page_table_lock을 선택합니다.
pud_lockptr()는 확인한 구현에서 mm의 공유 lock을 돌려줍니다. pmd_lock()·pud_lock()는 선택한 lock을 실제로 획득하고 held pointer를 반환합니다. PTE의 결합형 pte_offset_map_lock()은 mapping과 lock을 얻으며, 성공한 caller는 pte_unmap_unlock()으로 둘을 정리합니다. _nolock 계열의 결과와 바꿔 쓸 수 없습니다.
내부 ptlock_ptr()는 ALLOC_SPLIT_PTLOCKS 조건에 따라 ptdesc->ptl이 별도 pointer인지 inline lock인지도 감춥니다. 현재 header의 분기 이름은 CONFIG_SPLIT_PTE_PTLOCKS·CONFIG_SPLIT_PMD_PTLOCKS이며 예전 source의 USE_SPLIT_* 표기와 다릅니다. 이 lock 선택 규칙 어디에도 data folio의 reference나 pin을 얻는 동작은 없습니다. include/linux/mm.h의 pointer helper, 획득 helper와 대응 unlock을 한 묶음으로 보아야 합니다.
PTE lock은 TLB를 비우지 않습니다
PTE lock을 보유하면 해당 entry의 software 변경을 serialize할 수 있습니다. 이미 CPU 안에 들어간 translation까지 자동으로 invalidation하는 것은 아닙니다. virtual address를 PFN A에서 PFN B로 바꾼 뒤 stale writable TLB가 A를 계속 가리키면 잘못된 객체에 store할 수 있습니다.
보통 translation을 제거·제한하는 entry 변경을 먼저 하고 필요한 invalidation을 완료한 뒤 예전 backing이나 table을 재사용할 수 있게 해야 합니다. 반대로 먼저 TLB만 비우고 old PTE를 남겨 두면 hardware가 그 entry로 translation을 다시 채울 수 있습니다. pointer 보호와 CPU cache의 수명은 같은 lock으로 해결되지 않습니다.
INVLPG처럼 주소를 지정하는 invalidation과 더 넓은 context flush에는 비용 차이가 있습니다. 작은 범위는 무관한 translation을 보존할 수 있지만 명령 수가 많아집니다. 큰 flush는 실행 자체가 간단해도 이후 page walk와 refill 비용을 늘릴 수 있습니다.
PCID와 huge translation
x86에서 CR3는 현재 root 선택에 관여하지만 CR3 write를 모든 TLB의 무조건적인 전체 flush로 요약해서는 안 됩니다. GLOBAL translation, PCID와 no-flush 선택을 함께 보아야 합니다. 현재 choose_new_asid()와 switch_mm_irqs_off()는 보유 context와 TLB generation을 비교해 필요한 처리를 고릅니다.
PTI는 user·kernel root와 그 cached context를 함께 고려합니다. 이전 process로 돌아왔다는 이유만으로 모든 translation이 사라져 있다고 가정할 수 없고, 반대로 PCID가 있다는 이유로 mapping 변경의 invalidation을 생략할 수도 없습니다. generation은 CPU가 어느 변경까지 반영했는지 판단하는 정보입니다.
한 주소를 무효화하는 실제 분기는 다음 전체 함수에서 확인할 수 있습니다. v6.18 native_flush_tlb_one_user()는 먼저 현재 kernel PCID 쪽에 INVLPG를 실행합니다. PTI가 없으면 여기서 끝나고, PTI가 있으면 대응 user PCID도 처리합니다. CPU의 INVPCID 지원뿐 아니라 현재 CPU의 CR4.PCIDE가 켜졌는지도 검사합니다. 이 조건이 맞으면 주소와 user PCID를 지정한 invalidation을 수행하고, 아니면 invalidate_user_asid()로 그 context의 후속 사용 때 flush가 필요하도록 표시합니다. 지원 feature만 확인한 채 초기 boot에 INVPCID를 실행해 발생할 수 있는 #GP를 피하려는 조건입니다.
책의 예전 listing은 X86_FEATURE_INVPCID_SINGLE을 중심으로 분기하지만 아래 v6.18은 X86_FEATURE_INVPCID와 CR4 상태를 함께 봅니다. 이름만 바꾼 같은 code라고 취급하지 않습니다. code는 upstream GPL-2.0-only 전체 함수이며 kernel 내부 의존성은 별도입니다. license와 출처 목록, GPL-2.0을 함께 제공합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/tlb.c:1622-1646
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/tlb.c
* Complete function: native_flush_tlb_one_user
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
STATIC_NOPV void native_flush_tlb_one_user(unsigned long addr)
{
u32 loaded_mm_asid;
bool cpu_pcide;
/* Flush 'addr' from the kernel PCID: */
invlpg(addr);
/* If PTI is off there is no user PCID and nothing to flush. */
if (!static_cpu_has(X86_FEATURE_PTI))
return;
loaded_mm_asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);
cpu_pcide = this_cpu_read(cpu_tlbstate.cr4) & X86_CR4_PCIDE;
/*
* invpcid_flush_one(pcid>0) will #GP if CR4.PCIDE==0. Check
* 'cpu_pcide' to ensure that *this* CPU will not trigger those
* #GP's even if called before CR4.PCIDE has been initialized.
*/
if (boot_cpu_has(X86_FEATURE_INVPCID) && cpu_pcide)
invpcid_flush_one(user_pcid(loaded_mm_asid), addr);
else
invalidate_user_asid(loaded_mm_asid);
}
2 MiB leaf는 4 KiB page 512개 범위를, 1 GiB leaf는 262,144개 범위를 한 translation으로 표현할 수 있습니다. 이것은 같은 범위에 필요한 translation 수의 비교이지 실측 성능 배수가 아닙니다. page-size별 TLB 구성, working set, contiguous backing 확보 비용은 따로 남습니다.
page table을 읽고 수정하는 코드를 볼 때는 세 질문으로 돌아오면 됩니다. 지금의 pointer는 어떤 보호 아래 유효한지, entry를 바꿀 권한은 누가 갖는지, CPU가 옛 translation을 언제까지 사용할 수 있는지입니다. 셋 중 하나라도 놓치면 올바른 주소 계산만으로는 안전한 memory 접근을 만들 수 없습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 3 §3.1.1, §3.1.4~§3.1.5, §3.4.
- Linux v6.18 memory.c: table allocation과 follow_pfnmap의 사용 계약.
- Split page table lock: helper·build 조건과 constructor 실패.
- Linux v6.18 pgalloc-track.h, pgalloc.h: table 연결과 변경 level 기록.
- Linux v6.18 tlb.c, x86 TLB, PTI.