private write는 복사하고 shared write는 backing의 승인을 받습니다

같은 file을 writable로 mapping해도 private와 shared의 첫 write는 다른 대상을 수정합니다. private는 file contents로 초기화한 anonymous target을 만들고, shared는 file backing 자체를 writable하게 합니다. 이를 구별하지 않으면 dirty tracking과 실패 cleanup을 잘못 읽게 됩니다.

PTE가 없을 때의 private copy

이 분기의 anonymous는 새 target의 성격을 가리킵니다. VMA의 file과 vm_ops가 사라진다는 뜻은 아닙니다. source를 얻기 전에 target allocation과 memcg charge를 해 두므로, callback의 ERROR·NOPAGE·RETRY에서도 target을 버리는 경로가 필요합니다. v6.0의 DONE_COW는 callback이 그 target을 포함한 CoW 계약을 처리했다는 별도 결과이므로 일반 copy와 cleanup을 반복하지 않습니다.

do_cow_fault()는 anonymous rmap을 준비하고 cow_page에 target을 allocation합니다. 그다음 backing fault callback으로 source page를 얻습니다. target 전체를 source로 덮어쓸 때에는 zero-fill allocation이 필요 없지만, 사용자에게 공개할 byte를 빠짐없이 초기화해야 한다는 조건은 같습니다.

callback이 copy를 이미 완료한 특별한 결과를 반환하면 같은 copy를 반복하지 않습니다. 일반 경로에서는 source를 target으로 복사하고 uptodate로 표시한 뒤 finish_fault()가 page가 아니라 cow_page를 설치하도록 합니다. hardware poison과 copy failure는 일반 allocation 실패와 구분할 수 있습니다.

source의 folio lock과 reference는 copy 뒤 반환합니다. target은 PTE 경쟁에서 지거나 설치하지 않았을 때 반환합니다. PTE가 성공적으로 소유한 target을 error cleanup에서 다시 free하면 안 됩니다. source와 target의 수명을 따로 생각해야 이 차이가 보입니다.

shared write의 page_mkwrite

v6.0 do_page_mkwrite()는 callback에 WRITE|MKWRITE만 전달하며, callback을 호출한 뒤에는 원래 fault flags를 복원합니다. swapfile backing을 쓰려는 경우에는 callback보다 먼저 SIGBUS로 거부하므로 그 복원 지점까지 가지 않는 분기도 있습니다. callback이 LOCKED를 돌려주지 않았다면 직접 page lock을 얻고 page->mapping이 아직 존재하는지 확인합니다. mapping이 사라졌으면 unlock하고 0을 반환하여 나중에 refault하게 하며, 이를 “writable PTE 설치 성공”이라고 해석하지 않습니다.

do_shared_fault()는 같은 file page를 얻은 뒤 필요하면 page_mkwrite를 호출합니다. filesystem은 이때 block 준비, truncate·writeback 동기화나 write 거부를 수행할 수 있습니다. 단순히 “나중에 writeback하라”는 알림만 주는 hook이 아닙니다.

잠들 수 있는 callback을 PTE spinlock 아래 호출해서는 안 됩니다. callback 뒤에는 필요한 folio lock과 mapping 유효성을 확인합니다. truncate가 그 관계를 끊었다면 지금 얻은 folio를 writable PTE로 강행하지 않고 refault할 수 있습니다. 정상 PTE 설치 뒤에는 dirty 표시, timestamp와 throttling도 이어집니다.

read-only shared PTE도 정상입니다

처음부터 read-only PTE가 필요한지는 vma_wants_writenotify()와 protection 설정에 관계합니다. mmap()의 vma_set_page_prot()뿐 아니라 mprotect()의 protection 조정에서도 이 관계를 유지합니다. 아래 v6.0 wp_page_shared()는 임시 get_page()로 page 수명을 확보합니다. callback이 없는 경우 PTE reuse로 lock을 풀고 난 뒤 page lock을 얻으며, callback이 있는 경우에는 callback의 page lock을 이어받습니다. 두 경우 모두 dirty helper에 진입할 때 필요한 page lock 계약은 같습니다.

이미 read하여 present PTE가 있다면 write는 do_wp_page()를 거칩니다. shared mapping의 read-only protection은 CoW가 아니라 write notification을 위한 것일 수 있습니다. wp_page_shared()는 source reference를 추가한 뒤 lock을 놓아 callback을 호출하고 finish_mkwrite_fault()에서 lock을 재획득해 entry를 다시 비교합니다.

folio가 없는 shared PFN은 wp_pfn_shared()와 pfn_mkwrite를 사용합니다. callback이 없으면 허용된 PTE의 권한을 갱신하는 경로가 가능하지만 일반 file의 dirty page와 동일한 page reference 계약을 적용하지 않습니다.

첫 write의 객체 관계입니다. private는 새 anonymous target을, shared는 같은 file folio를 연결합니다. lock의 정확한 시간 순서와 실패 경로는 생략했습니다.
첫 write의 객체 관계입니다. private는 새 anonymous target을, shared는 같은 file folio를 연결합니다. lock의 정확한 시간 순서와 실패 경로는 생략했습니다.

dirty 처리에도 lock handoff가 있습니다

v6.0에서는 page_rmapping()으로 얻은 address_space를 page unlock 전에 local variable에 보관합니다. unlock 뒤 truncate가 page->mapping을 지우더라도 VMA의 file reference가 address_space 수명을 유지하기 때문입니다. driver page처럼 mapping이 NULL인 경우에는 그 pointer로 throttling을 진행하지 않습니다. set_page_dirty()의 반환값은 이번에 새로 dirty가 됐는지에 관계하며, 이미 dirty였더라도 page_mkwrite가 있는 경로는 throttling 조건에 들어갈 수 있습니다.

maybe_unlock_mmap_for_io()는 기존 file pin이 있으면 재사용합니다. 새로 pin하고 lock을 놓는 것은 ALLOW_RETRY가 있고 TRIED는 없으며 RETRY_NOWAIT도 없는 첫 시도입니다. 이 경로는 “I/O를 금지”하는 설정이 아니라 이 helper가 lock을 놓고 기다릴지 정하는 계약입니다. 뒤이어 balance_dirty_pages_ratelimited()가 writeback 속도에 맞춰 생산자를 지연시킬 수 있지만, 반환 시 file의 모든 dirty byte가 storage에 동기화됐다는 뜻은 아닙니다.

truncate 경고는 shared에도 유효합니다. shared write가 backing에 반영된다고 해서 다른 주체의 O_TRUNC나 ftruncate()로 제거한 data가 보존되는 것은 아닙니다. 특히 제거된 file 범위의 private anonymous CoW contents도 invalidation에 의해 잃을 수 있으므로, 어느 쪽도 truncate에 견디는 snapshot이라는 보장은 없습니다.

fault_dirty_shared_page()는 folio가 안정된 동안 dirty 상태를 기록합니다. unlock 뒤에 page->mapping을 다시 읽어 이전 값과 같다고 가정해서는 안 됩니다. 반면 unlock 전에 저장한 local mapping은 앞서 설명한 VMA의 file reference가 수명을 유지하므로, 이 함수의 throttling에 계속 사용할 수 있습니다. 이 pointer와 unlock 뒤 달라질 수 있는 page field를 구별하셔야 합니다. page_mkwrite가 없는 경로에는 file time 갱신이 필요할 수 있습니다.

writeback throttling 때문에 대기해야 하면 file reference를 붙잡고 mmap lock을 먼저 놓아 contention을 줄일 수 있습니다. 이 경로의 COMPLETED 결과는 caller가 또 unlock하지 않도록 알려 줍니다. dirty가 되었다는 사실, writable PTE가 생겼다는 사실, storage에 영속화됐다는 사실은 여전히 서로 다릅니다.

private file의 CoW target도 file truncate로 무효화될 수 있습니다. VMA의 file rmap 연결이 남아 있기 때문입니다. 따라서 private mapping을 외부 수정과 무관한 영구 snapshot으로 사용하는 설계는 backing 수명과 truncate 규칙을 별도로 고려해야 합니다.

Linux v6.0의 private와 shared write 전체 정의

missing PTE와 present write-protected PTE가 어디서 합류하는지, callback과 dirty 처리에서 lock과 reference가 어떻게 인계되는지 비교합니다. wp_page_reuse()의 전체 정의는 CoW reuse 글에서 이어서 읽으실 수 있습니다.

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

do_cow_fault()

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

static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret;

    if (unlikely(anon_vma_prepare(vma)))
        return VM_FAULT_OOM;

    vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
    if (!vmf->cow_page)
        return VM_FAULT_OOM;

    if (mem_cgroup_charge(page_folio(vmf->cow_page), vma->vm_mm,
                GFP_KERNEL)) {
        put_page(vmf->cow_page);
        return VM_FAULT_OOM;
    }
    cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);

    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        goto uncharge_out;
    if (ret & VM_FAULT_DONE_COW)
        return ret;

    copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
    __SetPageUptodate(vmf->cow_page);

    ret |= finish_fault(vmf);
    unlock_page(vmf->page);
    put_page(vmf->page);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        goto uncharge_out;
    return ret;
uncharge_out:
    put_page(vmf->cow_page);
    return ret;
}

do_shared_fault()

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

static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret, tmp;

    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        return ret;

    /*
     * Check if the backing address space wants to know that the page is
     * about to become writable
     */
    if (vma->vm_ops->page_mkwrite) {
        unlock_page(vmf->page);
        tmp = do_page_mkwrite(vmf);
        if (unlikely(!tmp ||
                (tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
            put_page(vmf->page);
            return tmp;
        }
    }

    ret |= finish_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
                    VM_FAULT_RETRY))) {
        unlock_page(vmf->page);
        put_page(vmf->page);
        return ret;
    }

    ret |= fault_dirty_shared_page(vmf);
    return ret;
}

do_page_mkwrite()

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

static vm_fault_t do_page_mkwrite(struct vm_fault *vmf)
{
    vm_fault_t ret;
    struct page *page = vmf->page;
    unsigned int old_flags = vmf->flags;

    vmf->flags = FAULT_FLAG_WRITE|FAULT_FLAG_MKWRITE;

    if (vmf->vma->vm_file &&
        IS_SWAPFILE(vmf->vma->vm_file->f_mapping->host))
        return VM_FAULT_SIGBUS;

    ret = vmf->vma->vm_ops->page_mkwrite(vmf);
    /* Restore original flags so that caller is not surprised */
    vmf->flags = old_flags;
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))
        return ret;
    if (unlikely(!(ret & VM_FAULT_LOCKED))) {
        lock_page(page);
        if (!page->mapping) {
            unlock_page(page);
            return 0; /* retry */
        }
        ret |= VM_FAULT_LOCKED;
    } else
        VM_BUG_ON_PAGE(!PageLocked(page), page);
    return ret;
}

wp_pfn_shared()

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

static vm_fault_t wp_pfn_shared(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;

    if (vma->vm_ops && vma->vm_ops->pfn_mkwrite) {
        vm_fault_t ret;

        pte_unmap_unlock(vmf->pte, vmf->ptl);
        vmf->flags |= FAULT_FLAG_MKWRITE;
        ret = vma->vm_ops->pfn_mkwrite(vmf);
        if (ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))
            return ret;
        return finish_mkwrite_fault(vmf);
    }
    wp_page_reuse(vmf);
    return VM_FAULT_WRITE;
}

finish_mkwrite_fault()

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

vm_fault_t finish_mkwrite_fault(struct vm_fault *vmf)
{
    WARN_ON_ONCE(!(vmf->vma->vm_flags & VM_SHARED));
    vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd, vmf->address,
                       &vmf->ptl);
    /*
     * We might have raced with another page fault while we released the
     * pte_offset_map_lock.
     */
    if (!pte_same(*vmf->pte, vmf->orig_pte)) {
        update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return VM_FAULT_NOPAGE;
    }
    wp_page_reuse(vmf);
    return 0;
}

wp_page_shared()

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

static vm_fault_t wp_page_shared(struct vm_fault *vmf)
    __releases(vmf->ptl)
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret = VM_FAULT_WRITE;

    get_page(vmf->page);

    if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
        vm_fault_t tmp;

        pte_unmap_unlock(vmf->pte, vmf->ptl);
        tmp = do_page_mkwrite(vmf);
        if (unlikely(!tmp || (tmp &
                      (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
            put_page(vmf->page);
            return tmp;
        }
        tmp = finish_mkwrite_fault(vmf);
        if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
            unlock_page(vmf->page);
            put_page(vmf->page);
            return tmp;
        }
    } else {
        wp_page_reuse(vmf);
        lock_page(vmf->page);
    }
    ret |= fault_dirty_shared_page(vmf);
    put_page(vmf->page);

    return ret;
}

fault_dirty_shared_page()

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

static vm_fault_t fault_dirty_shared_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct address_space *mapping;
    struct page *page = vmf->page;
    bool dirtied;
    bool page_mkwrite = vma->vm_ops && vma->vm_ops->page_mkwrite;

    dirtied = set_page_dirty(page);
    VM_BUG_ON_PAGE(PageAnon(page), page);
    /*
     * Take a local copy of the address_space - page.mapping may be zeroed
     * by truncate after unlock_page().   The address_space itself remains
     * pinned by vma->vm_file's reference.  We rely on unlock_page()'s
     * release semantics to prevent the compiler from undoing this copying.
     */
    mapping = page_rmapping(page);
    unlock_page(page);

    if (!page_mkwrite)
        file_update_time(vma->vm_file);

    /*
     * Throttle page dirtying rate down to writeback speed.
     *
     * mapping may be NULL here because some device drivers do not
     * set page.mapping but still dirty their pages
     *
     * Drop the mmap_lock before waiting on IO, if we can. The file
     * is pinning the mapping, as per above.
     */
    if ((dirtied || page_mkwrite) && mapping) {
        struct file *fpin;

        fpin = maybe_unlock_mmap_for_io(vmf, NULL);
        balance_dirty_pages_ratelimited(mapping);
        if (fpin) {
            fput(fpin);
            return VM_FAULT_COMPLETED;
        }
    }

    return 0;
}

maybe_unlock_mmap_for_io()

Linux v6.0 mm/internal.h, SPDX GPL-2.0-or-later입니다.

static inline struct file *maybe_unlock_mmap_for_io(struct vm_fault *vmf,
                            struct file *fpin)
{
    int flags = vmf->flags;

    if (fpin)
        return fpin;

    /*
     * FAULT_FLAG_RETRY_NOWAIT means we don't want to wait on page locks or
     * anything, so we only pin the file and drop the mmap_lock if only
     * FAULT_FLAG_ALLOW_RETRY is set, while this is the first attempt.
     */
    if (fault_flag_allow_retry_first(flags) &&
        !(flags & FAULT_FLAG_RETRY_NOWAIT)) {
        fpin = get_file(vmf->vma->vm_file);
        mmap_read_unlock(vmf->vma->vm_mm);
    }
    return fpin;
}

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 6 §6.6–§6.9.3. 현재 구현은 Linux v6.18 mm/memory.c, interface 의미는 mmap(2)를 참고하실 수 있습니다.