GUP의 page-table walk는 permission과 CoW 상태를 함께 검사합니다

PFN이 present entry에 있다고 곧바로 GUP 결과로 반환할 수는 없습니다. read pin의 unshare, write 접근 권한, NUMA hint와 special mapping은 같은 entry에서도 다른 처리를 요구합니다. follow 경로는 단순한 VA-to-PFN 변환기가 아닙니다.

level별 empty와 bad entry

PGD에서 P4D·PUD·PMD를 거쳐 PTE로 내려갑니다. folded level과 huge leaf는 실제 table 수를 바꾸며, missing table은 fault 필요성을 나타낼 수 있습니다. core dump에서는 아직 없는 anonymous zero 영역을 굳이 fault해 memory와 dump 크기를 늘리지 않도록 별도 조건을 적용합니다.

PTE를 얻으면 lock 아래 present·migration·NUMA PROTNONE 상태를 판단합니다. migration 대기와 새 allocation fault는 다른 동작입니다. vm_normal_page()가 NULL인 special entry도 zero page와 raw PFN을 구별해야 합니다. 유효한 mapping이라는 사실과 page array로 반환할 normal object가 있다는 사실은 다릅니다.

write follow와 강제 접근의 제한

WRITE 요청에서 writable PTE면 가장 단순합니다. read-only entry라면 정상 write fault로 CoW나 notification을 해결해야 할 수 있습니다. FORCE의 특수 허용에는 VMA의 MAYWRITE, private CoW 여부, anonymous exclusive와 soft-dirty 등의 조건이 관계합니다. shared mapping을 무조건 force-write할 수 있는 계약은 아닙니다.

soft-dirty tracking이 write를 trap해야 하는데 이를 단순 refcount 획득으로 우회하면 user-visible tracking이 틀어집니다. 따라서 permission 검사는 VMA와 PTE, tracking state를 함께 봅니다. 오래된 helper의 특정 조건을 현재의 공개 flag 조합으로 그대로 옮기면 안 됩니다.

read pin의 UNSHARE

PIN할 anonymous page가 아직 CoW 공유 관계이면 gup_must_unshare() 계열이 이를 감지합니다. slow core는 UNSHARE fault를 수행한 뒤 다시 follow합니다. anonymous exclusivity를 확보하는 것이 목적이므로 read-only 접근을 writable하게 바꾸는 것은 아닙니다. ZERO와 file backing에는 같은 anonymous unshare 조건을 적용하지 않습니다.

page를 선택한 뒤 try_grab_page()·folio helper는 GET 또는 PIN 방식으로 reference를 얻습니다. large folio는 별도 pincount를 쓰고 order-0은 bias accounting을 사용합니다. TOUCH와 WRITE 조합은 accessed·dirty를 반영할 수 있으며 architecture의 accessibility·cache hook도 따릅니다. 그 뒤 lock을 반환하고 page를 caller에 넘깁니다.

faultin_page의 flags 변환

NOFAULT가 있으면 fault가 필요한 순간 error를 반환합니다. WRITE·REMOTE는 대응 fault flag로, lock을 놓을 수 있는 문맥은 ALLOW_RETRY로 이어집니다. NOWAIT는 lock handoff와 대기 방식에 추가 제약을 주며 KILLABLE 같은 signal 정책도 관계합니다. UNSHARE는 WRITE와 함께 쓰지 않습니다.

handle_mm_fault()의 ERROR는 errno로 변환하고, RETRY·COMPLETED는 lock state와 함께 바깥 loop가 이해할 결과로 바꿉니다. fault가 성공했어도 곧바로 원래 관찰한 PTE pointer가 살아난다는 뜻이 아닙니다. 현재 mapping을 다시 follow하여 실제 object를 얻어야 합니다.

Lookup의 세 결과와 전체 level 정의

다음은 Linux v6.0의 전체 explicit definition입니다. 반환된 page pointer, mapping 부재인 NULL, 특별한 이유를 전하는 error pointer를 구분합니다. Core dump의 no_page_table()은 아직 건드리지 않은 zero 영역을 fault해서 실제 memory를 소비하는 일을 피합니다. 따라서 NULL과 ERR_PTR(-EFAULT)를 같은 “다시 fault하세요”로 취급하면 안 됩니다.

follow_page_mask.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:844-882
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *follow_page_mask(struct vm_area_struct *vma,
			      unsigned long address, unsigned int flags,
			      struct follow_page_context *ctx)
{
	pgd_t *pgd;
	struct page *page;
	struct mm_struct *mm = vma->vm_mm;

	ctx->page_mask = 0;

	/* make this handle hugepd */
	page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
	if (!IS_ERR(page)) {
		WARN_ON_ONCE(flags & (FOLL_GET | FOLL_PIN));
		return page;
	}

	pgd = pgd_offset(mm, address);

	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
		return no_page_table(vma, flags);

	if (pgd_huge(*pgd)) {
		page = follow_huge_pgd(mm, address, pgd, flags);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
	if (is_hugepd(__hugepd(pgd_val(*pgd)))) {
		page = follow_huge_pd(vma, address,
				      __hugepd(pgd_val(*pgd)), flags,
				      PGDIR_SHIFT);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}

	return follow_p4d_mask(vma, address, pgd, flags, ctx);
}

no_page_table.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:444-459
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *no_page_table(struct vm_area_struct *vma,
		unsigned int flags)
{
	/*
	 * When core dumping an enormous anonymous area that nobody
	 * has touched so far, we don't want to allocate unnecessary pages or
	 * page tables.  Return error instead of NULL to skip handle_mm_fault,
	 * then get_dump_page() will return NULL to leave a hole in the dump.
	 * But we can only make this optimization where a hole would surely
	 * be zero-filled if handle_mm_fault() actually did handle it.
	 */
	if ((flags & FOLL_DUMP) &&
			(vma_is_anonymous(vma) || !vma->vm_ops->fault))
		return ERR_PTR(-EFAULT);
	return NULL;
}

follow_p4d_mask.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:794-818
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *follow_p4d_mask(struct vm_area_struct *vma,
				    unsigned long address, pgd_t *pgdp,
				    unsigned int flags,
				    struct follow_page_context *ctx)
{
	p4d_t *p4d;
	struct page *page;

	p4d = p4d_offset(pgdp, address);
	if (p4d_none(*p4d))
		return no_page_table(vma, flags);
	BUILD_BUG_ON(p4d_huge(*p4d));
	if (unlikely(p4d_bad(*p4d)))
		return no_page_table(vma, flags);

	if (is_hugepd(__hugepd(p4d_val(*p4d)))) {
		page = follow_huge_pd(vma, address,
				      __hugepd(p4d_val(*p4d)), flags,
				      P4D_SHIFT);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
	return follow_pud_mask(vma, address, p4d, flags, ctx);
}

follow_pud_mask.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:754-792
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *follow_pud_mask(struct vm_area_struct *vma,
				    unsigned long address, p4d_t *p4dp,
				    unsigned int flags,
				    struct follow_page_context *ctx)
{
	pud_t *pud;
	spinlock_t *ptl;
	struct page *page;
	struct mm_struct *mm = vma->vm_mm;

	pud = pud_offset(p4dp, address);
	if (pud_none(*pud))
		return no_page_table(vma, flags);
	if (pud_huge(*pud) && is_vm_hugetlb_page(vma)) {
		page = follow_huge_pud(mm, address, pud, flags);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
	if (is_hugepd(__hugepd(pud_val(*pud)))) {
		page = follow_huge_pd(vma, address,
				      __hugepd(pud_val(*pud)), flags,
				      PUD_SHIFT);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
	if (pud_devmap(*pud)) {
		ptl = pud_lock(mm, pud);
		page = follow_devmap_pud(vma, address, pud, flags, &ctx->pgmap);
		spin_unlock(ptl);
		if (page)
			return page;
	}
	if (unlikely(pud_bad(*pud)))
		return no_page_table(vma, flags);

	return follow_pmd_mask(vma, address, pud, flags, ctx);
}

follow_pmd_mask.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:646-752
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *follow_pmd_mask(struct vm_area_struct *vma,
				    unsigned long address, pud_t *pudp,
				    unsigned int flags,
				    struct follow_page_context *ctx)
{
	pmd_t *pmd, pmdval;
	spinlock_t *ptl;
	struct page *page;
	struct mm_struct *mm = vma->vm_mm;

	pmd = pmd_offset(pudp, address);
	/*
	 * The READ_ONCE() will stabilize the pmdval in a register or
	 * on the stack so that it will stop changing under the code.
	 */
	pmdval = READ_ONCE(*pmd);
	if (pmd_none(pmdval))
		return no_page_table(vma, flags);
	if (pmd_huge(pmdval) && is_vm_hugetlb_page(vma)) {
		page = follow_huge_pmd(mm, address, pmd, flags);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
	if (is_hugepd(__hugepd(pmd_val(pmdval)))) {
		page = follow_huge_pd(vma, address,
				      __hugepd(pmd_val(pmdval)), flags,
				      PMD_SHIFT);
		if (page)
			return page;
		return no_page_table(vma, flags);
	}
retry:
	if (!pmd_present(pmdval)) {
		/*
		 * Should never reach here, if thp migration is not supported;
		 * Otherwise, it must be a thp migration entry.
		 */
		VM_BUG_ON(!thp_migration_supported() ||
				  !is_pmd_migration_entry(pmdval));

		if (likely(!(flags & FOLL_MIGRATION)))
			return no_page_table(vma, flags);

		pmd_migration_entry_wait(mm, pmd);
		pmdval = READ_ONCE(*pmd);
		/*
		 * MADV_DONTNEED may convert the pmd to null because
		 * mmap_lock is held in read mode
		 */
		if (pmd_none(pmdval))
			return no_page_table(vma, flags);
		goto retry;
	}
	if (pmd_devmap(pmdval)) {
		ptl = pmd_lock(mm, pmd);
		page = follow_devmap_pmd(vma, address, pmd, flags, &ctx->pgmap);
		spin_unlock(ptl);
		if (page)
			return page;
	}
	if (likely(!pmd_trans_huge(pmdval)))
		return follow_page_pte(vma, address, pmd, flags, &ctx->pgmap);

	if ((flags & FOLL_NUMA) && pmd_protnone(pmdval))
		return no_page_table(vma, flags);

retry_locked:
	ptl = pmd_lock(mm, pmd);
	if (unlikely(pmd_none(*pmd))) {
		spin_unlock(ptl);
		return no_page_table(vma, flags);
	}
	if (unlikely(!pmd_present(*pmd))) {
		spin_unlock(ptl);
		if (likely(!(flags & FOLL_MIGRATION)))
			return no_page_table(vma, flags);
		pmd_migration_entry_wait(mm, pmd);
		goto retry_locked;
	}
	if (unlikely(!pmd_trans_huge(*pmd))) {
		spin_unlock(ptl);
		return follow_page_pte(vma, address, pmd, flags, &ctx->pgmap);
	}
	if (flags & FOLL_SPLIT_PMD) {
		int ret;
		page = pmd_page(*pmd);
		if (is_huge_zero_page(page)) {
			spin_unlock(ptl);
			ret = 0;
			split_huge_pmd(vma, pmd, address);
			if (pmd_trans_unstable(pmd))
				ret = -EBUSY;
		} else {
			spin_unlock(ptl);
			split_huge_pmd(vma, pmd, address);
			ret = pte_alloc(mm, pmd) ? -ENOMEM : 0;
		}

		return ret ? ERR_PTR(ret) :
			follow_page_pte(vma, address, pmd, flags, &ctx->pgmap);
	}
	page = follow_trans_huge_pmd(vma, address, pmd, flags);
	spin_unlock(ptl);
	ctx->page_mask = HPAGE_PMD_NR - 1;
	return page;
}

Book에서 생략한 HugeTLB·THP·devmap·migration 분기도 남겼습니다. follow_pmd_mask()가 PTE 경로로 갈 때와 huge leaf를 직접 처리할 때는 lock과 page span 계약이 다릅니다. “모든 level은 pointer 하나만 따라가면 된다”는 모델로 이 source를 줄이지 않겠습니다.

PTE lock 안에서 pin과 cleanup이 만납니다

follow_page_pte() 전체에는 lookup, write 가능성 검사, zero/raw PFN 구별, unshare 요청, reference 획득과 실패 cleanup이 이어집니다. Accessibility hook이 pin 뒤에 실패하면 이미 얻은 pin을 반환한 뒤 error로 돌아갑니다. 어느 exit에서도 PTE mapping과 lock을 정리해야 합니다.

follow_page_pte.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:519-644
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static struct page *follow_page_pte(struct vm_area_struct *vma,
		unsigned long address, pmd_t *pmd, unsigned int flags,
		struct dev_pagemap **pgmap)
{
	struct mm_struct *mm = vma->vm_mm;
	struct page *page;
	spinlock_t *ptl;
	pte_t *ptep, pte;
	int ret;

	/* FOLL_GET and FOLL_PIN are mutually exclusive. */
	if (WARN_ON_ONCE((flags & (FOLL_PIN | FOLL_GET)) ==
			 (FOLL_PIN | FOLL_GET)))
		return ERR_PTR(-EINVAL);
retry:
	if (unlikely(pmd_bad(*pmd)))
		return no_page_table(vma, flags);

	ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
	pte = *ptep;
	if (!pte_present(pte)) {
		swp_entry_t entry;
		/*
		 * KSM's break_ksm() relies upon recognizing a ksm page
		 * even while it is being migrated, so for that case we
		 * need migration_entry_wait().
		 */
		if (likely(!(flags & FOLL_MIGRATION)))
			goto no_page;
		if (pte_none(pte))
			goto no_page;
		entry = pte_to_swp_entry(pte);
		if (!is_migration_entry(entry))
			goto no_page;
		pte_unmap_unlock(ptep, ptl);
		migration_entry_wait(mm, pmd, address);
		goto retry;
	}
	if ((flags & FOLL_NUMA) && pte_protnone(pte))
		goto no_page;

	page = vm_normal_page(vma, address, pte);

	/*
	 * We only care about anon pages in can_follow_write_pte() and don't
	 * have to worry about pte_devmap() because they are never anon.
	 */
	if ((flags & FOLL_WRITE) &&
	    !can_follow_write_pte(pte, page, vma, flags)) {
		page = NULL;
		goto out;
	}

	if (!page && pte_devmap(pte) && (flags & (FOLL_GET | FOLL_PIN))) {
		/*
		 * Only return device mapping pages in the FOLL_GET or FOLL_PIN
		 * case since they are only valid while holding the pgmap
		 * reference.
		 */
		*pgmap = get_dev_pagemap(pte_pfn(pte), *pgmap);
		if (*pgmap)
			page = pte_page(pte);
		else
			goto no_page;
	} else if (unlikely(!page)) {
		if (flags & FOLL_DUMP) {
			/* Avoid special (like zero) pages in core dumps */
			page = ERR_PTR(-EFAULT);
			goto out;
		}

		if (is_zero_pfn(pte_pfn(pte))) {
			page = pte_page(pte);
		} else {
			ret = follow_pfn_pte(vma, address, ptep, flags);
			page = ERR_PTR(ret);
			goto out;
		}
	}

	if (!pte_write(pte) && gup_must_unshare(flags, page)) {
		page = ERR_PTR(-EMLINK);
		goto out;
	}

	VM_BUG_ON_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
		       !PageAnonExclusive(page), page);

	/* try_grab_page() does nothing unless FOLL_GET or FOLL_PIN is set. */
	if (unlikely(!try_grab_page(page, flags))) {
		page = ERR_PTR(-ENOMEM);
		goto out;
	}
	/*
	 * We need to make the page accessible if and only if we are going
	 * to access its content (the FOLL_PIN case).  Please see
	 * Documentation/core-api/pin_user_pages.rst for details.
	 */
	if (flags & FOLL_PIN) {
		ret = arch_make_page_accessible(page);
		if (ret) {
			unpin_user_page(page);
			page = ERR_PTR(ret);
			goto out;
		}
	}
	if (flags & FOLL_TOUCH) {
		if ((flags & FOLL_WRITE) &&
		    !pte_dirty(pte) && !PageDirty(page))
			set_page_dirty(page);
		/*
		 * pte_mkyoung() would be more correct here, but atomic care
		 * is needed to avoid losing the dirty bit: it is easier to use
		 * mark_page_accessed().
		 */
		mark_page_accessed(page);
	}
out:
	pte_unmap_unlock(ptep, ptl);
	return page;
no_page:
	pte_unmap_unlock(ptep, ptl);
	if (!pte_none(pte))
		return NULL;
	return no_page_table(vma, flags);
}

can_follow_write_pte.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:482-517
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline bool can_follow_write_pte(pte_t pte, struct page *page,
					struct vm_area_struct *vma,
					unsigned int flags)
{
	/* If the pte is writable, we can write to the page. */
	if (pte_write(pte))
		return true;

	/* Maybe FOLL_FORCE is set to override it? */
	if (!(flags & FOLL_FORCE))
		return false;

	/* But FOLL_FORCE has no effect on shared mappings */
	if (vma->vm_flags & (VM_MAYSHARE | VM_SHARED))
		return false;

	/* ... or read-only private ones */
	if (!(vma->vm_flags & VM_MAYWRITE))
		return false;

	/* ... or already writable ones that just need to take a write fault */
	if (vma->vm_flags & VM_WRITE)
		return false;

	/*
	 * See can_change_pte_writable(): we broke COW and could map the page
	 * writable if we have an exclusive anonymous page ...
	 */
	if (!page || !PageAnon(page) || !PageAnonExclusive(page))
		return false;

	/* ... and a write-fault isn't required for other reasons. */
	if (vma_soft_dirty_enabled(vma) && !pte_soft_dirty(pte))
		return false;
	return !userfaultfd_pte_wp(vma, pte);
}

FORCE의 특수 write 허용에는 exclusive anonymous page가 필요합니다. “not exclusively anonymous”라는 방향으로 읽으면 !PageAnonExclusive(page)에서 false를 반환하는 조건과 반대가 됩니다. MAYWRITE·private 조건에 더해 soft-dirty와 userfaultfd write-protect도 지켜야 합니다. 정상 writable VMA가 read-only PTE를 가진 경우에는 필요한 write fault를 우회하지 않습니다.

Source: Linux v6.0 mm/gup.c. Historical layout reference이며 book exact tag는 미확정입니다. 원래 notice 및 COPYING, GPL-2.0를 보존했습니다. v6.18은 별도 고정 비교 tag이고 compile·runtime 검증은 하지 않았습니다.

Unshare는 read-only PIN에도 필요한 별도 fault입니다

gup_must_unshare()는 WRITE가 없는 PIN에서 nonexclusive anonymous page를 구별합니다. 실제 PTE가 참조하는 subpage를 넘겨야 하며 large folio의 head page를 무조건 대체 입력으로 넣으면 안 됩니다. 반환된 -EMLINK는 file link 개수를 조사하라는 뜻이 아니라 slow core에 UNSHARE fault가 필요함을 알리는 내부 신호입니다.

follow_pfn_pte.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:461-479
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address,
		pte_t *pte, unsigned int flags)
{
	if (flags & FOLL_TOUCH) {
		pte_t entry = *pte;

		if (flags & FOLL_WRITE)
			entry = pte_mkdirty(entry);
		entry = pte_mkyoung(entry);

		if (!pte_same(*pte, entry)) {
			set_pte_at(vma->vm_mm, address, pte, entry);
			update_mmu_cache(vma, address, pte);
		}
	}

	/* Proper page table entry exists, but no corresponding struct page */
	return -EEXIST;
}

gup_must_unshare.c

/* SPDX-License-Identifier: GPL-2.0 */

/* Upstream Linux v6.0: include/linux/mm.h:2985-3005
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline bool gup_must_unshare(unsigned int flags, struct page *page)
{
	/*
	 * FOLL_WRITE is implicitly handled correctly as the page table entry
	 * has to be writable -- and if it references (part of) an anonymous
	 * folio, that part is required to be marked exclusive.
	 */
	if ((flags & (FOLL_WRITE | FOLL_PIN)) != FOLL_PIN)
		return false;
	/*
	 * Note: PageAnon(page) is stable until the page is actually getting
	 * freed.
	 */
	if (!PageAnon(page))
		return false;
	/*
	 * Note that PageKsm() pages cannot be exclusive, and consequently,
	 * cannot get pinned.
	 */
	return !PageAnonExclusive(page);
}

try_grab_page.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:202-229
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
bool __must_check try_grab_page(struct page *page, unsigned int flags)
{
	struct folio *folio = page_folio(page);

	WARN_ON_ONCE((flags & (FOLL_GET | FOLL_PIN)) == (FOLL_GET | FOLL_PIN));
	if (WARN_ON_ONCE(folio_ref_count(folio) <= 0))
		return false;

	if (flags & FOLL_GET)
		folio_ref_inc(folio);
	else if (flags & FOLL_PIN) {
		/*
		 * Similar to try_grab_folio(): be sure to *also*
		 * increment the normal page refcount field at least once,
		 * so that the page really is pinned.
		 */
		if (folio_test_large(folio)) {
			folio_ref_add(folio, 1);
			atomic_add(1, folio_pincount_ptr(folio));
		} else {
			folio_ref_add(folio, GUP_PIN_COUNTING_BIAS);
		}

		node_stat_mod_folio(folio, NR_FOLL_PIN_ACQUIRED, 1);
	}

	return true;
}

follow_pfn_pte()는 PTE를 touch할 수 있어도 반환할 normal struct page가 없다는 뜻으로 -EEXIST를 돌려줍니다. try_grab_page()의 true도 무조건 pin을 증가시켰다는 뜻은 아닙니다. GET/PIN이 없으면 조치가 필요 없어서 true일 수 있습니다. 반대로 PIN을 얻은 뒤 accessibility 검사에 실패한 경로는 unpin으로 되돌려야 합니다.

TOUCH 아래 dirty 반영을 결정하는 flag는 FOLL_WRITE입니다. 이 구간에 FOLL_DIRTY라는 별도 입력 flag가 있는 것처럼 설명하지 않겠습니다. Normal page에서는 mark_page_accessed()를 사용하고 PFN-only helper는 PTE young bit를 다루므로 “TOUCH는 언제나 같은 folio bit 하나를 세운다”로 합칠 수도 없습니다.

faultin_page.c

// SPDX-License-Identifier: GPL-2.0-only

/* Upstream Linux v6.0: mm/gup.c:960-1025
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int faultin_page(struct vm_area_struct *vma,
		unsigned long address, unsigned int *flags, bool unshare,
		int *locked)
{
	unsigned int fault_flags = 0;
	vm_fault_t ret;

	if (*flags & FOLL_NOFAULT)
		return -EFAULT;
	if (*flags & FOLL_WRITE)
		fault_flags |= FAULT_FLAG_WRITE;
	if (*flags & FOLL_REMOTE)
		fault_flags |= FAULT_FLAG_REMOTE;
	if (locked)
		fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
	if (*flags & FOLL_NOWAIT)
		fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
	if (*flags & FOLL_TRIED) {
		/*
		 * Note: FAULT_FLAG_ALLOW_RETRY and FAULT_FLAG_TRIED
		 * can co-exist
		 */
		fault_flags |= FAULT_FLAG_TRIED;
	}
	if (unshare) {
		fault_flags |= FAULT_FLAG_UNSHARE;
		/* FAULT_FLAG_WRITE and FAULT_FLAG_UNSHARE are incompatible */
		VM_BUG_ON(fault_flags & FAULT_FLAG_WRITE);
	}

	ret = handle_mm_fault(vma, address, fault_flags, NULL);

	if (ret & VM_FAULT_COMPLETED) {
		/*
		 * With FAULT_FLAG_RETRY_NOWAIT we'll never release the
		 * mmap lock in the page fault handler. Sanity check this.
		 */
		WARN_ON_ONCE(fault_flags & FAULT_FLAG_RETRY_NOWAIT);
		if (locked)
			*locked = 0;
		/*
		 * We should do the same as VM_FAULT_RETRY, but let's not
		 * return -EBUSY since that's not reflecting the reality of
		 * what has happened - we've just fully completed a page
		 * fault, with the mmap lock released.  Use -EAGAIN to show
		 * that we want to take the mmap lock _again_.
		 */
		return -EAGAIN;
	}

	if (ret & VM_FAULT_ERROR) {
		int err = vm_fault_to_errno(ret, *flags);

		if (err)
			return err;
		BUG();
	}

	if (ret & VM_FAULT_RETRY) {
		if (locked && !(fault_flags & FAULT_FLAG_RETRY_NOWAIT))
			*locked = 0;
		return -EBUSY;
	}

	return 0;
}

faultin_page()의 NOFAULT는 -EFAULT로 필요한 fault를 거절합니다. UNSHARE와 WRITE는 함께 설정할 수 없습니다. VM_FAULT_COMPLETED는 fault가 끝났더라도 mmap lock을 놓았음을 -EAGAIN으로 전달하고, RETRY는 -EBUSY와 lock state를 전달합니다. 이 errno들은 바깥 GUP loop의 protocol이며 user syscall의 최종 error와 항상 일대일 대응하지 않습니다. Source는 위와 같은 v6.0 mm/gup.c, mm.h입니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1.6–§8.1.7. 현재 follow와 fault 변환은 Linux v6.18 mm/gup.c, anonymous unshare는 mm/memory.c를 참고하실 수 있습니다.