present PTE가 있어도 vm_normal_page는 NULL일 수 있습니다

PTE에서 PFN을 읽을 수 있다면 언제나 struct page를 얻어 reference를 올려도 될까요. device memory와 special mapping에서는 그렇지 않습니다. RAM의 metadata가 존재하는지와 일반 page 수명 규칙을 적용해도 되는지는 다른 질문입니다.

normal이라는 판정의 의미

vm_normal_page()는 주어진 VMA·주소·PTE를 일반적인 page reference 대상으로 다룰 수 있는지 판단합니다. x86 special PTE, zero PFN, VM_PFNMAP·VM_MIXEDMAP와 callback 조건을 구분합니다. NULL은 “translation이 없다”가 아니라 “이 entry에 일반 page 계약을 적용하지 않는다”는 결과일 수 있습니다.

zero page에도 metadata가 존재하지만 일반 anonymous allocation처럼 reference를 관리하지 않습니다. raw device PFN에는 아예 보통의 struct page가 없을 수 있습니다. 반대로 device 관련 mapping 중에도 별도 metadata와 lifetime protocol을 가진 경우가 있어 PFNMAP라는 단어 하나로 전부 같은 대상으로 취급할 수 없습니다.

일반 RAM, shared zero page, struct page가 없는 raw device PFN을 구분합니다. x86 special-PTE 경로의 개념 예이며 devmap과 callback 예외는 생략했습니다.
일반 RAM, shared zero page, struct page가 없는 raw device PFN을 구분합니다. x86 special-PTE 경로의 개념 예이며 devmap과 callback 예외는 생략했습니다.

연속 PFN과 mixed mapping

v6.0 remap_pfn_range_notrack()는 page-aligned 시작 주소를 요구하고 크기는 PAGE_ALIGN으로 올려 끝을 구합니다. CoW 가능한 VMA라면 전체 VMA를 정확히 덮어야 하며, 시작 PFN을 vm_pgoff에 기록합니다. special PTE bit가 없는 architecture에서는 vm_pgoff + ((addr - vm_start) >> PAGE_SHIFT)와 실제 PFN이 같은지로 원래 PFN mapping을 식별하고, 그 관계를 벗어난 CoW page를 normal로 구분합니다. x86의 special-bit 경로와 이 fallback 규칙을 섞어 적용하면 안 됩니다.

이 helper가 설정하는 VM_IO는 side effect가 있을 수 있는 접근이라는 신호이고, VM_PFNMAP은 raw PFN 수명 규칙, VM_DONTEXPAND는 확장 제한, VM_DONTDUMP는 core dump 제외에 관계합니다. notrack 변형의 caller는 pgprot_t의 caching bit를 미리 검증해야 합니다. pfn -= addr >> PAGE_SHIFT 뒤 각 virtual address의 page 번호를 다시 더하는 것은 원하는 선형 PFN translation을 유지하려는 좌표 변환입니다. table allocation 실패는 error로 전파하며 이 helper가 그때까지 설치한 모든 entry를 자체적으로 rollback하는 함수라고 가정해서는 안 됩니다.

vm_normal_page()의 callback find_special_page가 있으면 그 계약이 먼저 적용될 수 있습니다. special bit가 없을 때 MIXEDMAP은 pfn_valid()로 normal metadata를 구분하고, special bit가 있는 x86에서는 entry별 special 판정이 우선합니다. 예기치 않은 special entry나 memmap 범위를 벗어난 PFN은 bad-PTE 진단 대상입니다. PageReserved page도 반환될 수 있으므로 normal이라는 결과가 곧 자유롭게 free 가능한 page라는 뜻은 아닙니다. devmap·ZONE_DEVICE의 역사적 reference 규칙은 일반 raw PFN과 다시 구분합니다.

remap_pfn_range() 계열은 driver가 지정한 PFN 구간을 user page table에 연결합니다. table walk와 필요한 table allocation, architecture protection 처리가 있지만 page allocator가 새 user data를 공급하는 anonymous fault와는 다릅니다. backing device의 수명과 teardown은 driver의 계약입니다.

VM_MIXEDMAP은 같은 VMA에 normal page와 PFN-only entry가 섞일 수 있는 경우를 나타냅니다. page 삽입 API와 vmf_insert_mixed() 같은 PFN fault API가 구별됩니다. VMA 하나에 속한다는 이유로 모든 PTE의 reference 처리가 같다고 가정해서는 안 됩니다.

special private CoW는 source를 어떻게 읽는가

v6.0의 정확한 실패 처리는 두 갈래입니다. 먼저 inatomic user copy가 실패하면 PTE lock을 얻고 original snapshot과 비교합니다. PTE가 달라졌으면 false로 돌아가 target을 버리고 refault할 수 있게 합니다. PTE가 같으면 lock 아래에서 한 번 더 copy를 시도하며, 여전히 읽을 수 없으면 WARN을 남기고 target page 전체를 zero-fill한 뒤 성공으로 취급합니다. 부분 copy의 나머지 byte만 놔두는 동작이 아닙니다. software accessed-bit architecture에서 이미 lock을 가진 경우에는 그 상태에 맞는 경로를 사용합니다.

아래 bool 반환의 전체 정의는 v6.0입니다. v6.18의 같은 helper는 kmap_local_page()와 명시적인 pagefault disable/enable을 사용하고, PTE 경쟁은 -EAGAIN, normal source의 machine-check copy 실패는 -EHWPOISON, 성공은 0으로 구분합니다. caller가 이 결과를 해석하는 방식까지 함께 바뀌었으므로 bool 성공 검사를 그대로 가져오면 안 됩니다. 이 역사적 best-effort zero-fill은 모든 device mapping에 대해 신뢰할 수 있는 snapshot copy를 제공한다는 약속이 아닙니다.

normal page가 없는 private source도 CoW가 필요할 수 있습니다. 일반 copy_user_highpage()에 page pointer를 전달할 수 없으므로 special copy 경로는 source의 user mapping을 통해 읽는 방식을 사용합니다. 그 접근에는 uaccess exception 복구가 필요합니다. driver backing을 무조건 direct-map pointer로 바꾸어 memcpy하는 방식이 아닙니다.

copy 중 entry가 바뀌거나 user access가 실패할 수 있습니다. fault handler는 PTE 상태를 다시 확인하고 복사하지 못한 byte가 새 private target에서 정보 유출로 이어지지 않도록 처리합니다. 이 세부 분기는 architecture와 callback에 민감하므로 normal RAM의 CoW 순서를 special PFN에 기계적으로 적용하지 않습니다.

shared writable PFN에는 copy 대신 승인 경로가 있습니다

shared PFN write fault에는 pfn_mkwrite를 호출하는 경로가 있습니다. 잠들 수 있도록 PTE lock을 놓고 callback을 수행한 뒤 재획득하여 snapshot을 비교합니다. callback이 없는 허용 경로는 PTE reuse로 끝날 수 있습니다. 일반 folio가 없는 만큼 file folio처럼 dirty flag와 put_page를 무조건 수행하면 안 됩니다.

special mapping을 다룰 때의 원칙은 명확합니다. PTE 상태, PFN의 종류, metadata 존재와 ownership protocol을 따로 확인해야 합니다. 이 구별은 fault뿐 아니라 GUP, munmap과 device driver의 buffer 수명에서도 그대로 필요합니다.

Linux v6.0의 special mapping 전체 정의

special-bit architecture와 그 bit가 없는 architecture의 판정 차이, raw PFN 설치와 CoW copy 실패 경로를 끝까지 비교합니다. 이 tag의 bool 반환 helper를 최신 error-code interface와 혼합하지 않습니다.

아래는 Linux v6.0의 전체 함수·자료형 정의입니다. kernel의 header·configuration·호출 규약에 의존하는 코드이며 독립 실행 프로그램은 아닙니다. 본문 해설과는 별도로 각 source의 SPDX license를 적용합니다. 원래 copyright notice는 source notice에, GPL version 2 전문은 license 파일에 보존합니다. 표시상의 tab을 space로 바꾼 것 외에 함수 내부의 문장이나 configuration 분기는 제거하지 않았습니다.

vm_normal_page()

Linux v6.0 mm/memory.c, SPDX GPL-2.0-only입니다.

struct page *vm_normal_page(struct vm_area_struct *vma, unsigned long addr,
                pte_t pte)
{
    unsigned long pfn = pte_pfn(pte);

    if (IS_ENABLED(CONFIG_ARCH_HAS_PTE_SPECIAL)) {
        if (likely(!pte_special(pte)))
            goto check_pfn;
        if (vma->vm_ops && vma->vm_ops->find_special_page)
            return vma->vm_ops->find_special_page(vma, addr);
        if (vma->vm_flags & (VM_PFNMAP | VM_MIXEDMAP))
            return NULL;
        if (is_zero_pfn(pfn))
            return NULL;
        if (pte_devmap(pte))
        /*
         * NOTE: New users of ZONE_DEVICE will not set pte_devmap()
         * and will have refcounts incremented on their struct pages
         * when they are inserted into PTEs, thus they are safe to
         * return here. Legacy ZONE_DEVICE pages that set pte_devmap()
         * do not have refcounts. Example of legacy ZONE_DEVICE is
         * MEMORY_DEVICE_FS_DAX type in pmem or virtio_fs drivers.
         */
            return NULL;

        print_bad_pte(vma, addr, pte, NULL);
        return NULL;
    }

    /* !CONFIG_ARCH_HAS_PTE_SPECIAL case follows: */

    if (unlikely(vma->vm_flags & (VM_PFNMAP|VM_MIXEDMAP))) {
        if (vma->vm_flags & VM_MIXEDMAP) {
            if (!pfn_valid(pfn))
                return NULL;
            goto out;
        } else {
            unsigned long off;
            off = (addr - vma->vm_start) >> PAGE_SHIFT;
            if (pfn == vma->vm_pgoff + off)
                return NULL;
            if (!is_cow_mapping(vma->vm_flags))
                return NULL;
        }
    }

    if (is_zero_pfn(pfn))
        return NULL;

check_pfn:
    if (unlikely(pfn > highest_memmap_pfn)) {
        print_bad_pte(vma, addr, pte, NULL);
        return NULL;
    }

    /*
     * NOTE! We still have PageReserved() pages in the page tables.
     * eg. VDSO mappings can cause them to exist.
     */
out:
    return pfn_to_page(pfn);
}

remap_pfn_range_notrack()

Linux v6.0 mm/memory.c, SPDX GPL-2.0-only입니다.

int remap_pfn_range_notrack(struct vm_area_struct *vma, unsigned long addr,
        unsigned long pfn, unsigned long size, pgprot_t prot)
{
    pgd_t *pgd;
    unsigned long next;
    unsigned long end = addr + PAGE_ALIGN(size);
    struct mm_struct *mm = vma->vm_mm;
    int err;

    if (WARN_ON_ONCE(!PAGE_ALIGNED(addr)))
        return -EINVAL;

    /*
     * Physically remapped pages are special. Tell the
     * rest of the world about it:
     *   VM_IO tells people not to look at these pages
     *  (accesses can have side effects).
     *   VM_PFNMAP tells the core MM that the base pages are just
     *  raw PFN mappings, and do not have a "struct page" associated
     *  with them.
     *   VM_DONTEXPAND
     *      Disable vma merging and expanding with mremap().
     *   VM_DONTDUMP
     *      Omit vma from core dump, even when VM_IO turned off.
     *
     * There's a horrible special case to handle copy-on-write
     * behaviour that some programs depend on. We mark the "original"
     * un-COW'ed pages by matching them up with "vma->vm_pgoff".
     * See vm_normal_page() for details.
     */
    if (is_cow_mapping(vma->vm_flags)) {
        if (addr != vma->vm_start || end != vma->vm_end)
            return -EINVAL;
        vma->vm_pgoff = pfn;
    }

    vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;

    BUG_ON(addr >= end);
    pfn -= addr >> PAGE_SHIFT;
    pgd = pgd_offset(mm, addr);
    flush_cache_range(vma, addr, end);
    do {
        next = pgd_addr_end(addr, end);
        err = remap_p4d_range(mm, pgd, addr, next,
                pfn + (addr >> PAGE_SHIFT), prot);
        if (err)
            return err;
    } while (pgd++, addr = next, addr != end);

    return 0;
}

__wp_page_copy_user()

Linux v6.0 mm/memory.c, SPDX GPL-2.0-only입니다.

static inline bool __wp_page_copy_user(struct page *dst, struct page *src,
                       struct vm_fault *vmf)
{
    bool ret;
    void *kaddr;
    void __user *uaddr;
    bool locked = false;
    struct vm_area_struct *vma = vmf->vma;
    struct mm_struct *mm = vma->vm_mm;
    unsigned long addr = vmf->address;

    if (likely(src)) {
        copy_user_highpage(dst, src, addr, vma);
        return true;
    }

    /*
     * If the source page was a PFN mapping, we don't have
     * a "struct page" for it. We do a best-effort copy by
     * just copying from the original user address. If that
     * fails, we just zero-fill it. Live with it.
     */
    kaddr = kmap_atomic(dst);
    uaddr = (void __user *)(addr & PAGE_MASK);

    /*
     * On architectures with software "accessed" bits, we would
     * take a double page fault, so mark it accessed here.
     */
    if (arch_faults_on_old_pte() && !pte_young(vmf->orig_pte)) {
        pte_t entry;

        vmf->pte = pte_offset_map_lock(mm, vmf->pmd, addr, &vmf->ptl);
        locked = true;
        if (!likely(pte_same(*vmf->pte, vmf->orig_pte))) {
            /*
             * Other thread has already handled the fault
             * and update local tlb only
             */
            update_mmu_tlb(vma, addr, vmf->pte);
            ret = false;
            goto pte_unlock;
        }

        entry = pte_mkyoung(vmf->orig_pte);
        if (ptep_set_access_flags(vma, addr, vmf->pte, entry, 0))
            update_mmu_cache(vma, addr, vmf->pte);
    }

    /*
     * This really shouldn't fail, because the page is there
     * in the page tables. But it might just be unreadable,
     * in which case we just give up and fill the result with
     * zeroes.
     */
    if (__copy_from_user_inatomic(kaddr, uaddr, PAGE_SIZE)) {
        if (locked)
            goto warn;

        /* Re-validate under PTL if the page is still mapped */
        vmf->pte = pte_offset_map_lock(mm, vmf->pmd, addr, &vmf->ptl);
        locked = true;
        if (!likely(pte_same(*vmf->pte, vmf->orig_pte))) {
            /* The PTE changed under us, update local tlb */
            update_mmu_tlb(vma, addr, vmf->pte);
            ret = false;
            goto pte_unlock;
        }

        /*
         * The same page can be mapped back since last copy attempt.
         * Try to copy again under PTL.
         */
        if (__copy_from_user_inatomic(kaddr, uaddr, PAGE_SIZE)) {
            /*
             * Give a warn in case there can be some obscure
             * use-case
             */
warn:
            WARN_ON_ONCE(1);
            clear_page(kaddr);
        }
    }

    ret = true;

pte_unlock:
    if (locked)
        pte_unmap_unlock(vmf->pte, vmf->ptl);
    kunmap_atomic(kaddr);
    flush_dcache_page(dst);

    return ret;
}

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.12. normal-page 판정, remap과 special copy는 Linux v6.18 mm/memory.c, driver interface는 Memory Management APIs를 참고하실 수 있습니다.