새 anonymous mapping에서 모든 page를 읽었는데 data RSS는 거의 늘지 않을 수 있습니다. kernel은 바뀌지 않는 zero page를 여러 주소의 read-only translation으로 공유할 수 있기 때문입니다. 반대로 첫 접근이 write라면 그 주소의 private contents를 보관할 backing이 필요합니다.
zero page도 PTE 설치는 필요합니다
v6.0 generic my_zero_pfn()은 zero_pfn을 돌려주며 init_zero_pfn()이 early initcall에서 이를 설정합니다. x86 ZERO_PAGE()는 kernel image의 empty_zero_page를 virt_to_page()로 바꾸고, 다시 page_to_pfn()으로 PFN을 얻습니다. 이 미리 확보된 zeroed backing을 pfn_pte()와 pte_mkspecial()로 연결하는 것이지, 매번 zero page를 allocation하는 것은 아닙니다.
PTE table을 확보할 때도 pte_alloc() 뒤 huge-PMD 경쟁을 확인하고 나서 entry를 다룹니다. mmap read lock은 다른 fault를 배제하지 않습니다. check_stable_address_space()는 OOM reaper가 address space를 불안정하게 만든 상태에 새 mapping을 추가하지 않기 위한 검사입니다. userfaultfd missing 등록이 있으면 PTE lock 아래 조건을 확인한 뒤 kernel이 임의로 backing을 설치하는 대신 해당 handler로 넘기는 분기도 있습니다.
일반적인 private anonymous missing fault에서 read라면 my_zero_pfn()으로 zero page를 선택할 수 있습니다. x86의 ZERO_PAGE는 미리 zero 초기화한 kernel memory에서 제공하며, PTE는 read-only·special로 설치합니다. VMA에 VM_WRITE가 있어도 공유 zero page를 writable로 연결해서는 안 됩니다.
zero page를 금지하는 mm·architecture 조건은 먼저 확인합니다. 사용하는 경우에도 PTE table allocation, entry 경쟁 검사와 주소 공간 안정성 검사는 남습니다. shared anonymous API가 내부에서 shmem fault를 쓴다는 점도 이 최적화의 적용 범위를 제한합니다.
MAP_SHARED | MAP_ANONYMOUS는 v6.0의 shmem_zero_setup()에서 shmem_kernel_file_setup()·__shmem_file_setup()을 거쳐 inode를 가진 unlinked shmem backing을 만듭니다. /dev/zero의 contents를 복제한다는 뜻은 아닙니다. file operation을 가진 VMA라는 분류와 swap-backed folio가 anonymous 쪽 LRU에 속한다는 분류는 서로 다른 축입니다. 이 VMA가 ordinary private anonymous handler로 들어가면 안 되는 이유도 여기에 있습니다.

첫 write의 allocation 조건
고전 x86 macro의 GFP_HIGHUSER_MOVABLE|__GFP_ZERO는 zeroing뿐 아니라 __GFP_MOVABLE, direct reclaim·kswapd reclaim, I/O·filesystem 진입 허용, cpuset hardwall 제약을 결합합니다. 당시 KASAN 관련 skip flag는 instrumentation hint이지 사용자 byte를 초기화하지 않아도 된다는 허가가 아닙니다. reclaim 허용은 회수 성공을 보장하지 않으며 allocation이나 memcg charge가 실패하면 OOM 결과로 정리합니다.
anon_vma_prepare()는 아직 연결되지 않은 VMA의 anon_vma·anon_vma_chain 관계부터 준비합니다. 인접 관계를 재사용할 수도 있으며, 이 시점에 data page의 mapping field까지 완성되는 것은 아닙니다. v6.0의 alloc_page_vma()는 order 0의 vma_alloc_folio()에 위임하고 NUMA policy를 거쳐 physical allocator로 내려갑니다. 아래 전체 정의의 &folio->page는 첫 member 관계를 이용한 당시 표현이며 allocation 성공을 보증하는 검사로 해석해서는 안 됩니다.
첫 write는 anonymous rmap 준비와 user memory allocation으로 갑니다. NUMA policy와 cpuset 제한, memcg charge, reclaim 및 I/O 허용 조건이 공급에 영향을 줍니다. user-visible page는 이전 사용자의 byte를 노출하지 않도록 초기화해야 합니다. movable allocation은 나중에 physical location을 옮길 가능성을 허용한다는 뜻이지 VA가 바뀐다는 뜻은 아닙니다.
원래의 order-0 흐름에서는 zeroed user page를 얻어 uptodate로 표시합니다. 현재 구현은 policy에 따라 multi-page anonymous folio를 시도할 수도 있으므로 한 fault와 한 base page allocation을 항상 같은 단위로 보아서는 안 됩니다. 이 글의 그림과 이후 단일-entry 설명은 order-0 범위입니다.
공개 전 준비와 공개 직전 재검사
zeroing 뒤 __SetPageUptodate()의 barrier는 contents 초기화가 PTE 공개보다 먼저 보이게 하는 규칙입니다. anonymous의 uptodate는 file storage에 영속화됐다는 뜻이 아닙니다. x86에서는 dirty와 soft-dirty도 구별해야 하며, 해당 configuration의 pte_mkdirty()는 두 bit에 영향을 줄 수 있습니다. store instruction 재실행 전에 설정한 dirty bit를 실제 write byte 수로 읽어서는 안 됩니다.
VMA protection으로 entry를 만든 뒤 v6.0 code의 vma->vm_flags & VM_WRITE 조건이 맞으면 writable·dirty 상태를 더합니다. 이 조건은 현재 fault의 WRITE flag 자체가 아닙니다. Zero page를 사용할 수 없어 private allocation으로 내려온 read도 VMA의 write 허용 여부로 판정합니다. dirty는 store instruction이 다시 실행되기 전에도 설정하므로 실제 byte 수정 완료의 관찰값은 아닙니다. software young 처리가 필요한 architecture와 hardware accessed bit를 가진 x86의 차이도 구별해야 합니다.
allocation은 잠들 수 있어 PTE spinlock 밖에서 진행합니다. 설치 직전에 lock을 얻고 entry가 여전히 비어 있는지, mm이 안전한 상태인지 확인합니다. 다른 thread가 먼저 page를 연결했다면 새 page를 버려야 합니다. 뒤늦은 allocation 성공이 그 entry를 교체할 권리를 주지 않습니다.
rmap, LRU, RSS가 함께 바뀝니다
설치할 folio에는 anonymous mapping과 논리 index를 설정하고, 해당 mapping의 exclusive 상태를 정리하며, reclaim이 추적할 LRU 관계를 추가합니다. anonymous RSS도 조정합니다. 이 준비가 맞아야 PTE를 통해 공개한 page를 이후 reclaim·migration·fork가 올바르게 다룰 수 있습니다.
설치에 실패했다면 아직 어느 PTE도 소유하지 않는 target reference를 내려야 합니다. 성공했다면 mapping 수명으로 넘긴 reference를 임시 allocation 결과처럼 다시 반환해서는 안 됩니다. zero page의 special refcount 계약과 일반 private folio의 계약도 같지 않습니다. 그래서 이 경로의 핵심 비용은 단순히 memset(0)의 시간만으로 설명되지 않습니다.
Linux v6.0의 zero page와 anonymous fault 전체 정의
order-0 private anonymous 경로의 준비, PTE 공개와 실패 cleanup을 끝까지 확인할 수 있습니다. memcg와 userfaultfd 분기도 전체 정의에 포함합니다. v6.18의 multi-page folio 선택을 구현한 코드가 아닙니다.
아래는 Linux v6.0의 전체 함수·자료형 정의입니다. kernel의 header·configuration·호출 규약에 의존하는 코드이며 독립 실행 프로그램은 아닙니다. 본문 해설과는 별도로 각 source의 SPDX license를 적용합니다. 원래 copyright notice는 source notice에, GPL version 2 전문은 license 파일에 보존합니다. 표시상의 tab을 space로 바꾼 것 외에 함수 내부의 문장이나 configuration 분기는 제거하지 않았습니다.
do_anonymous_page()
Linux v6.0 mm/memory.c, SPDX GPL-2.0-only입니다.
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
vm_fault_t ret = 0;
pte_t entry;
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
/*
* Use pte_alloc() instead of pte_alloc_map(). We can't run
* pte_offset_map() on pmds where a huge pmd might be created
* from a different thread.
*
* pte_alloc_map() is safe to use under mmap_write_lock(mm) or when
* parallel threads are excluded by other means.
*
* Here we only have mmap_read_lock(mm).
*/
if (pte_alloc(vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM;
/* See comment in handle_pte_fault() */
if (unlikely(pmd_trans_unstable(vmf->pmd)))
return 0;
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!pte_none(*vmf->pte)) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto unlock;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte;
}
/* Allocate our own private page. */
if (unlikely(anon_vma_prepare(vma)))
goto oom;
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page)
goto oom;
if (mem_cgroup_charge(page_folio(page), vma->vm_mm, GFP_KERNEL))
goto oom_free_page;
cgroup_throttle_swaprate(page, GFP_KERNEL);
/*
* The memory barrier inside __SetPageUptodate makes sure that
* preceding stores to the page contents become visible before
* the set_pte_at() write.
*/
__SetPageUptodate(page);
entry = mk_pte(page, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
&vmf->ptl);
if (!pte_none(*vmf->pte)) {
update_mmu_cache(vma, vmf->address, vmf->pte);
goto release;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto release;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
put_page(page);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
page_add_new_anon_rmap(page, vma, vmf->address);
lru_cache_add_inactive_or_unevictable(page, vma);
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
release:
put_page(page);
goto unlock;
oom_free_page:
put_page(page);
oom:
return VM_FAULT_OOM;
}
init_zero_pfn()
Linux v6.0 mm/memory.c, SPDX GPL-2.0-only입니다.
static int __init init_zero_pfn(void)
{
zero_pfn = page_to_pfn(ZERO_PAGE(0));
return 0;
}
early_initcall(init_zero_pfn);
my_zero_pfn()
Linux v6.0 include/linux/pgtable.h, SPDX GPL-2.0입니다.
static inline unsigned long my_zero_pfn(unsigned long addr)
{
extern unsigned long zero_pfn;
return zero_pfn;
}
alloc_page_vma()
Linux v6.0 include/linux/gfp.h, SPDX GPL-2.0입니다.
static inline struct page *alloc_page_vma(gfp_t gfp,
struct vm_area_struct *vma, unsigned long addr)
{
struct folio *folio = vma_alloc_folio(gfp, 0, vma, addr, false);
return &folio->page;
}
zero page와 zeroed allocation의 architecture 정의
v6.0 arch/x86/include/asm/pgtable.h의 선언과 macro, arch/x86/include/asm/page.h의 allocation macro 전체입니다. 두 header의 SPDX는 GPL-2.0입니다. 위 init function의 SPDX는 GPL-2.0-only이며 early_initcall() 등록까지 함께 표시했습니다.
extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)]
__visible;
#define ZERO_PAGE(vaddr) ((void)(vaddr),virt_to_page(empty_zero_page))
#define alloc_zeroed_user_highpage_movable(vma, vaddr) \
alloc_page_vma(GFP_HIGHUSER_MOVABLE | __GFP_ZERO, vma, vaddr)
#define __HAVE_ARCH_ALLOC_ZEROED_USER_HIGHPAGE_MOVABLE
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.3–§6.3.5. Linux v6.18 mm/memory.c의 do_anonymous_page(), Process Addresses를 참고하실 수 있습니다.