fast GUP는 먼저 pin하고 나중에 관찰이 맞았는지 확인합니다

fast GUP가 빠른 이유는 VMA lock 검사를 대충 하기 때문이 아닙니다. fault를 수행하지 않는 좁은 경로에서 page-table 상태를 낙관적으로 읽고 reference를 획득한 뒤, 같은 관찰이 아직 유효한지 검증하기 때문입니다. 검증에 실패하면 얻은 reference를 되돌립니다.

fast와 fast-only

일반 fast wrapper는 가능한 prefix를 처리하고 나머지를 slow path에 넘길 수 있습니다. fast-only는 slow fallback을 하지 않으므로 0이나 partial count가 정상적으로 나올 수 있습니다. 모두 base-page 수를 반환하며 반환 개수만큼의 output만 유효합니다.

fast path는 VMA pointer array를 안전하게 넘기는 방식이 아닙니다. current mm의 범위와 허용 flags, address overflow 및 architecture 접근 조건을 먼저 확인합니다. FORCE 같은 flag 이름이 이전 표에 있더라도 현재 wrapper가 허용하는 집합을 validator로 확인해야 합니다.

IRQ disable은 global lock이 아닙니다

lockless table walk는 architecture의 table lifetime protocol에 기대어 진행합니다. x86의 local IRQ 제어는 그 protocol의 일부지만 다른 CPU가 모든 mapping 변경을 멈춘다는 뜻은 아닙니다. table free·RCU·TLB와 맞물린 보장이 있어야 dereference가 가능합니다.

fork의 write-protection 변경과 pin이 어긋나지 않도록 mm의 sequence 상태도 확인합니다. 임계 변경 중이거나 시작과 끝의 sequence가 다르면 optimistic 결과를 반환하지 않습니다. “PTE를 두 번 읽었으니 모든 경쟁이 해결됐다”는 단순한 알고리즘이 아닙니다.

낙관적 reference와 재검사

level별 walker가 ordinary PTE에 도착하면 present와 접근 방향, PROTNONE·special 등 빠른 처리가 불가능한 상태를 확인합니다. PFN에서 page를 얻어 nonzero reference를 확보합니다. 이미 freed된 object를 0에서 부활시키면 안 됩니다.

pin한 뒤 PTE와 필요한 상위 entry가 처음 값과 같은지 비교합니다. 달라졌으면 reference를 반환하고 중단합니다. read pin이 unshare를 요구해도 fast 경로에서 allocation을 강행하지 않고 결과를 되돌려 slow 경로가 처리하게 합니다. reference 획득만으로 그 page가 여전히 요청 VA의 backing이라는 증명이 되지는 않습니다.

folio split도 object identity를 바꿉니다

tail page에서 folio를 얻은 직후 큰 folio가 split될 수 있습니다. try_get_folio()는 reference를 얻고 page가 여전히 같은 folio에 속하는지 재확인합니다. 달라졌으면 잘못된 folio에 더한 reference를 내리고 다시 시도합니다. object pointer와 stable reference가 한 단계에서 완성되지 않는 예입니다.

LONGTERM pin에 부적합한 memory도 fast path에서 직접 migration하지 않습니다. slow fallback이 적합성 검사와 unpin·migration·재조회 작업을 맡을 수 있도록 중단합니다. 성공 prefix와 fallback 결과를 합칠 때에도 error보다 이미 얻은 page count를 정확히 전달해야 합니다.

이러한 제약 때문에 fast GUP 성능은 “page가 RAM에 있는가”만으로 정해지지 않습니다. permission trap, fork sequence, large folio 상태와 pin eligibility가 빠른 경로를 벗어나게 할 수 있습니다. fast-only를 선택한 caller는 partial 결과를 자연스러운 반환으로 설계해야 합니다.

네 public entry와 공통 core의 반환 계약

아래는 official v6.0의 전체 정의입니다. 일반 fast API는 prefix 뒤 slow fallback이 가능하지만 fast-only는 그렇지 않습니다. Fast-only wrapper는 내부 core가 반환한 negative error까지 0으로 바꾸므로, 이를 일반 fast와 같은 errno 계약으로 다루면 안 됩니다. PIN과 GET은 각각 대응 반환 API를 사용합니다.

get_user_pages_fast.c

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

/* Upstream Linux v6.0: mm/gup.c:3077-3091
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
int get_user_pages_fast(unsigned long start, int nr_pages,
			unsigned int gup_flags, struct page **pages)
{
	if (!is_valid_gup_flags(gup_flags))
		return -EINVAL;

	/*
	 * The caller may or may not have explicitly set FOLL_GET; either way is
	 * OK. However, internally (within mm/gup.c), gup fast variants must set
	 * FOLL_GET, because gup fast is always a "pin with a +1 page refcount"
	 * request.
	 */
	gup_flags |= FOLL_GET;
	return internal_get_user_pages_fast(start, nr_pages, gup_flags, pages);
}

get_user_pages_fast_only.c

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

/* Upstream Linux v6.0: mm/gup.c:3032-3058
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
int get_user_pages_fast_only(unsigned long start, int nr_pages,
			     unsigned int gup_flags, struct page **pages)
{
	int nr_pinned;
	/*
	 * Internally (within mm/gup.c), gup fast variants must set FOLL_GET,
	 * because gup fast is always a "pin with a +1 page refcount" request.
	 *
	 * FOLL_FAST_ONLY is required in order to match the API description of
	 * this routine: no fall back to regular ("slow") GUP.
	 */
	gup_flags |= FOLL_GET | FOLL_FAST_ONLY;

	nr_pinned = internal_get_user_pages_fast(start, nr_pages, gup_flags,
						 pages);

	/*
	 * As specified in the API description above, this routine is not
	 * allowed to return negative values. However, the common core
	 * routine internal_get_user_pages_fast() *can* return -errno.
	 * Therefore, correct for that here:
	 */
	if (nr_pinned < 0)
		nr_pinned = 0;

	return nr_pinned;
}

pin_user_pages_fast.c

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

/* Upstream Linux v6.0: mm/gup.c:3110-3122
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
int pin_user_pages_fast(unsigned long start, int nr_pages,
			unsigned int gup_flags, struct page **pages)
{
	/* FOLL_GET and FOLL_PIN are mutually exclusive. */
	if (WARN_ON_ONCE(gup_flags & FOLL_GET))
		return -EINVAL;

	if (WARN_ON_ONCE(!pages))
		return -EINVAL;

	gup_flags |= FOLL_PIN;
	return internal_get_user_pages_fast(start, nr_pages, gup_flags, pages);
}

pin_user_pages_fast_only.c

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

/* Upstream Linux v6.0: mm/gup.c:3131-3161
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
int pin_user_pages_fast_only(unsigned long start, int nr_pages,
			     unsigned int gup_flags, struct page **pages)
{
	int nr_pinned;

	/*
	 * FOLL_GET and FOLL_PIN are mutually exclusive. Note that the API
	 * rules require returning 0, rather than -errno:
	 */
	if (WARN_ON_ONCE(gup_flags & FOLL_GET))
		return 0;

	if (WARN_ON_ONCE(!pages))
		return 0;
	/*
	 * FOLL_FAST_ONLY is required in order to match the API description of
	 * this routine: no fall back to regular ("slow") GUP.
	 */
	gup_flags |= (FOLL_PIN | FOLL_FAST_ONLY);
	nr_pinned = internal_get_user_pages_fast(start, nr_pages, gup_flags,
						 pages);
	/*
	 * This routine is not allowed to return negative values. However,
	 * internal_get_user_pages_fast() *can* return -errno. Therefore,
	 * correct for that here:
	 */
	if (nr_pinned < 0)
		nr_pinned = 0;

	return nr_pinned;
}

internal_get_user_pages_fast.c

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

/* Upstream Linux v6.0: mm/gup.c:2964-3010
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int internal_get_user_pages_fast(unsigned long start,
					unsigned long nr_pages,
					unsigned int gup_flags,
					struct page **pages)
{
	unsigned long len, end;
	unsigned long nr_pinned;
	int ret;

	if (WARN_ON_ONCE(gup_flags & ~(FOLL_WRITE | FOLL_LONGTERM |
				       FOLL_FORCE | FOLL_PIN | FOLL_GET |
				       FOLL_FAST_ONLY | FOLL_NOFAULT)))
		return -EINVAL;

	if (gup_flags & FOLL_PIN)
		mm_set_has_pinned_flag(&current->mm->flags);

	if (!(gup_flags & FOLL_FAST_ONLY))
		might_lock_read(&current->mm->mmap_lock);

	start = untagged_addr(start) & PAGE_MASK;
	len = nr_pages << PAGE_SHIFT;
	if (check_add_overflow(start, len, &end))
		return 0;
	if (unlikely(!access_ok((void __user *)start, len)))
		return -EFAULT;

	nr_pinned = lockless_pages_from_mm(start, end, gup_flags, pages);
	if (nr_pinned == nr_pages || gup_flags & FOLL_FAST_ONLY)
		return nr_pinned;

	/* Slow path: try to get the remaining pages with get_user_pages */
	start += nr_pinned << PAGE_SHIFT;
	pages += nr_pinned;
	ret = __gup_longterm_unlocked(start, nr_pages - nr_pinned, gup_flags,
				      pages);
	if (ret < 0) {
		/*
		 * The caller has to unpin the pages we already pinned so
		 * returning -errno is not an option
		 */
		if (nr_pinned)
			return nr_pinned;
		return ret;
	}
	return ret + nr_pinned;
}

공통 core의 허용 mask는 WRITE | LONGTERM | FORCE | PIN | GET | FAST_ONLY | NOFAULT입니다. 이 중 PIN/GET은 wrapper가 계약에 따라 설정합니다. v6.0 get_user_pages_fast()는 is_valid_gup_flags()로 caller의 PIN·LONGTERM을 거절하지만, get_user_pages_fast_only()는 같은 validator를 호출하지 않습니다. 따라서 모든 get entry가 잘못된 조합을 똑같이 검출한다고 가정하면 안 됩니다. 검사가 빠졌다는 사실이 LONGTERM을 GET과 함께 쓰거나 public get API에 PIN을 직접 넘겨도 된다는 지원 계약을 만들지는 않습니다. TOUCH·DUMP·NOWAIT·HWPOISON·NUMA·MIGRATION·TRIED·REMOTE·ANON·SPLIT_PMD는 이 fast core의 mask에 없습니다. FORCE가 mask에 있어도 lockless phase가 필요한 write fault를 대신 수행하지는 않습니다.

GET을 설정하는 fast wrapper에도 결과를 받을 pages array가 필요합니다. 이 tag에서 pin wrapper는 NULL array를 검사하지만 get wrapper가 같은 검사를 해 준다고 기대해서는 안 됩니다. caller는 요청한 base-page 수만큼 공간을 준비하고 반환 count까지만 유효한 pointer로 사용해야 합니다.

Slow fallback은 성공한 prefix만큼 VA와 output pointer를 옮긴 뒤 __gup_longterm_unlocked()를 부릅니다. 이 이름을 locked variant로 바꾸어 읽으면 진입 lock의 주인이 바뀝니다. Prefix가 남아 있으면 뒤의 실패를 negative error 하나로 덮지 않고 그 개수를 반환합니다.

Sequence 검증 실패는 실제 unpin을 요구합니다

lockless_pages_from_mm.c

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

/* Upstream Linux v6.0: mm/gup.c:2915-2962
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static unsigned long lockless_pages_from_mm(unsigned long start,
					    unsigned long end,
					    unsigned int gup_flags,
					    struct page **pages)
{
	unsigned long flags;
	int nr_pinned = 0;
	unsigned seq;

	if (!IS_ENABLED(CONFIG_HAVE_FAST_GUP) ||
	    !gup_fast_permitted(start, end))
		return 0;

	if (gup_flags & FOLL_PIN) {
		seq = raw_read_seqcount(&current->mm->write_protect_seq);
		if (seq & 1)
			return 0;
	}

	/*
	 * Disable interrupts. The nested form is used, in order to allow full,
	 * general purpose use of this routine.
	 *
	 * With interrupts disabled, we block page table pages from being freed
	 * from under us. See struct mmu_table_batch comments in
	 * include/asm-generic/tlb.h for more details.
	 *
	 * We do not adopt an rcu_read_lock() here as we also want to block IPIs
	 * that come from THPs splitting.
	 */
	local_irq_save(flags);
	gup_pgd_range(start, end, gup_flags, pages, &nr_pinned);
	local_irq_restore(flags);

	/*
	 * When pinning pages for DMA there could be a concurrent write protect
	 * from fork() via copy_page_range(), in this case always fail fast GUP.
	 */
	if (gup_flags & FOLL_PIN) {
		if (read_seqcount_retry(&current->mm->write_protect_seq, seq)) {
			unpin_user_pages_lockless(pages, nr_pinned);
			return 0;
		} else {
			sanity_check_pinned_pages(pages, nr_pinned);
		}
	}
	return nr_pinned;
}

PIN 경로는 fork write-protect sequence가 odd이면 시작하지 않습니다. Walk 뒤 sequence가 달라졌다면 지금까지 얻은 optimistic pin을 모두 반환하고 0으로 돌아갑니다. Local IRQ disable은 table-free와 THP split의 architecture protocol에 참여하는 수단이지 다른 CPU의 PTE 변경을 금지하는 global lock이 아닙니다.

아래 level walker는 v6.0의 CONFIG_HAVE_FAST_GUP 구현입니다. 이 설정이 없으면 lockless phase는 0을 반환하고 일반 fast API는 slow fallback을 사용할 수 있지만 fast-only는 그 경로가 없습니다. PTE walker의 본체에는 CONFIG_ARCH_HAS_PTE_SPECIAL 조건도 있습니다. Special PTE를 구별할 수 없는 구성의 PTE stub는 즉시 0을 반환하며, 이것이 HugeTLB·THP leaf까지 모두 처리할 수 없다는 뜻은 아닙니다. 따라서 x86-64에서 읽은 walk를 모든 architecture·configuration의 구현으로 일반화하지 않습니다.

gup_pgd_range.c

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

/* Upstream Linux v6.0: mm/gup.c:2849-2873
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static void gup_pgd_range(unsigned long addr, unsigned long end,
		unsigned int flags, struct page **pages, int *nr)
{
	unsigned long next;
	pgd_t *pgdp;

	pgdp = pgd_offset(current->mm, addr);
	do {
		pgd_t pgd = READ_ONCE(*pgdp);

		next = pgd_addr_end(addr, end);
		if (pgd_none(pgd))
			return;
		if (unlikely(pgd_huge(pgd))) {
			if (!gup_huge_pgd(pgd, pgdp, addr, next, flags,
					  pages, nr))
				return;
		} else if (unlikely(is_hugepd(__hugepd(pgd_val(pgd))))) {
			if (!gup_huge_pd(__hugepd(pgd_val(pgd)), addr,
					 PGDIR_SHIFT, next, flags, pages, nr))
				return;
		} else if (!gup_p4d_range(pgdp, pgd, addr, next, flags, pages, nr))
			return;
	} while (pgdp++, addr = next, addr != end);
}

gup_p4d_range.c

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

/* Upstream Linux v6.0: mm/gup.c:2824-2847
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int gup_p4d_range(pgd_t *pgdp, pgd_t pgd, unsigned long addr, unsigned long end,
			 unsigned int flags, struct page **pages, int *nr)
{
	unsigned long next;
	p4d_t *p4dp;

	p4dp = p4d_offset_lockless(pgdp, pgd, addr);
	do {
		p4d_t p4d = READ_ONCE(*p4dp);

		next = p4d_addr_end(addr, end);
		if (p4d_none(p4d))
			return 0;
		BUILD_BUG_ON(p4d_huge(p4d));
		if (unlikely(is_hugepd(__hugepd(p4d_val(p4d))))) {
			if (!gup_huge_pd(__hugepd(p4d_val(p4d)), addr,
					 P4D_SHIFT, next, flags, pages, nr))
				return 0;
		} else if (!gup_pud_range(p4dp, p4d, addr, next, flags, pages, nr))
			return 0;
	} while (p4dp++, addr = next, addr != end);

	return 1;
}

gup_pud_range.c

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

/* Upstream Linux v6.0: mm/gup.c:2796-2822
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int gup_pud_range(p4d_t *p4dp, p4d_t p4d, unsigned long addr, unsigned long end,
			 unsigned int flags, struct page **pages, int *nr)
{
	unsigned long next;
	pud_t *pudp;

	pudp = pud_offset_lockless(p4dp, p4d, addr);
	do {
		pud_t pud = READ_ONCE(*pudp);

		next = pud_addr_end(addr, end);
		if (unlikely(!pud_present(pud)))
			return 0;
		if (unlikely(pud_huge(pud))) {
			if (!gup_huge_pud(pud, pudp, addr, next, flags,
					  pages, nr))
				return 0;
		} else if (unlikely(is_hugepd(__hugepd(pud_val(pud))))) {
			if (!gup_huge_pd(__hugepd(pud_val(pud)), addr,
					 PUD_SHIFT, next, flags, pages, nr))
				return 0;
		} else if (!gup_pmd_range(pudp, pud, addr, next, flags, pages, nr))
			return 0;
	} while (pudp++, addr = next, addr != end);

	return 1;
}

gup_pmd_range.c

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

/* Upstream Linux v6.0: mm/gup.c:2753-2794
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int gup_pmd_range(pud_t *pudp, pud_t pud, unsigned long addr, unsigned long end,
		unsigned int flags, struct page **pages, int *nr)
{
	unsigned long next;
	pmd_t *pmdp;

	pmdp = pmd_offset_lockless(pudp, pud, addr);
	do {
		pmd_t pmd = READ_ONCE(*pmdp);

		next = pmd_addr_end(addr, end);
		if (!pmd_present(pmd))
			return 0;

		if (unlikely(pmd_trans_huge(pmd) || pmd_huge(pmd) ||
			     pmd_devmap(pmd))) {
			/*
			 * NUMA hinting faults need to be handled in the GUP
			 * slowpath for accounting purposes and so that they
			 * can be serialised against THP migration.
			 */
			if (pmd_protnone(pmd))
				return 0;

			if (!gup_huge_pmd(pmd, pmdp, addr, next, flags,
				pages, nr))
				return 0;

		} else if (unlikely(is_hugepd(__hugepd(pmd_val(pmd))))) {
			/*
			 * architecture have different format for hugetlbfs
			 * pmd format and THP pmd format
			 */
			if (!gup_huge_pd(__hugepd(pmd_val(pmd)), addr,
					 PMD_SHIFT, next, flags, pages, nr))
				return 0;
		} else if (!gup_pte_range(pmd, pmdp, addr, next, flags, pages, nr))
			return 0;
	} while (pmdp++, addr = next, addr != end);

	return 1;
}

gup_pte_range.c

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

/* Upstream Linux v6.0: mm/gup.c:2367-2451
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static int gup_pte_range(pmd_t pmd, pmd_t *pmdp, unsigned long addr,
			 unsigned long end, unsigned int flags,
			 struct page **pages, int *nr)
{
	struct dev_pagemap *pgmap = NULL;
	int nr_start = *nr, ret = 0;
	pte_t *ptep, *ptem;

	ptem = ptep = pte_offset_map(&pmd, addr);
	do {
		pte_t pte = ptep_get_lockless(ptep);
		struct page *page;
		struct folio *folio;

		/*
		 * Similar to the PMD case below, NUMA hinting must take slow
		 * path using the pte_protnone check.
		 */
		if (pte_protnone(pte))
			goto pte_unmap;

		if (!pte_access_permitted(pte, flags & FOLL_WRITE))
			goto pte_unmap;

		if (pte_devmap(pte)) {
			if (unlikely(flags & FOLL_LONGTERM))
				goto pte_unmap;

			pgmap = get_dev_pagemap(pte_pfn(pte), pgmap);
			if (unlikely(!pgmap)) {
				undo_dev_pagemap(nr, nr_start, flags, pages);
				goto pte_unmap;
			}
		} else if (pte_special(pte))
			goto pte_unmap;

		VM_BUG_ON(!pfn_valid(pte_pfn(pte)));
		page = pte_page(pte);

		folio = try_grab_folio(page, 1, flags);
		if (!folio)
			goto pte_unmap;

		if (unlikely(page_is_secretmem(page))) {
			gup_put_folio(folio, 1, flags);
			goto pte_unmap;
		}

		if (unlikely(pmd_val(pmd) != pmd_val(*pmdp)) ||
		    unlikely(pte_val(pte) != pte_val(*ptep))) {
			gup_put_folio(folio, 1, flags);
			goto pte_unmap;
		}

		if (!pte_write(pte) && gup_must_unshare(flags, page)) {
			gup_put_folio(folio, 1, flags);
			goto pte_unmap;
		}

		/*
		 * 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) {
				gup_put_folio(folio, 1, flags);
				goto pte_unmap;
			}
		}
		folio_set_referenced(folio);
		pages[*nr] = page;
		(*nr)++;
	} while (ptep++, addr += PAGE_SIZE, addr != end);

	ret = 1;

pte_unmap:
	if (pgmap)
		put_dev_pagemap(pgmap);
	pte_unmap(ptem);
	return ret;
}

전체 PTE 함수에는 special·device·secret-memory·accessibility 분기와 undo도 보존했습니다. Ref를 먼저 얻고 entry가 같은지 재검사하며, PMD collapse 경쟁에서는 parent PMD도 확인합니다. 실패 branch마다 방금 얻은 ref를 내리는지와 이미 성공한 prefix를 어느 경우에 되돌리는지 함께 보셔야 합니다. pte_unmap()은 이 API protocol의 일부이므로 특정 historical x86 구현에서 가볍다는 이유로 지우지 않겠습니다.

try_grab_folio.c

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

/* Upstream Linux v6.0: mm/gup.c:124-168
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
struct folio *try_grab_folio(struct page *page, int refs, unsigned int flags)
{
	if (flags & FOLL_GET)
		return try_get_folio(page, refs);
	else if (flags & FOLL_PIN) {
		struct folio *folio;

		/*
		 * Can't do FOLL_LONGTERM + FOLL_PIN gup fast path if not in a
		 * right zone, so fail and let the caller fall back to the slow
		 * path.
		 */
		if (unlikely((flags & FOLL_LONGTERM) &&
			     !is_longterm_pinnable_page(page)))
			return NULL;

		/*
		 * CAUTION: Don't use compound_head() on the page before this
		 * point, the result won't be stable.
		 */
		folio = try_get_folio(page, refs);
		if (!folio)
			return NULL;

		/*
		 * When pinning a large folio, use an exact count to track it.
		 *
		 * However, be sure to *also* increment the normal folio
		 * refcount field at least once, so that the folio really
		 * is pinned.  That's why the refcount from the earlier
		 * try_get_folio() is left intact.
		 */
		if (folio_test_large(folio))
			atomic_add(refs, folio_pincount_ptr(folio));
		else
			folio_ref_add(folio,
					refs * (GUP_PIN_COUNTING_BIAS - 1));
		node_stat_mod_folio(folio, NR_FOLL_PIN_ACQUIRED, refs);

		return folio;
	}

	WARN_ON_ONCE(1);
	return NULL;
}

try_get_folio.c

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

/* Upstream Linux v6.0: mm/gup.c:69-96
 * Exact source lines; kernel context, not a standalone program or book restoration.
 */
static inline struct folio *try_get_folio(struct page *page, int refs)
{
	struct folio *folio;

retry:
	folio = page_folio(page);
	if (WARN_ON_ONCE(folio_ref_count(folio) < 0))
		return NULL;
	if (unlikely(!folio_ref_try_add_rcu(folio, refs)))
		return NULL;

	/*
	 * At this point we have a stable reference to the folio; but it
	 * could be that between calling page_folio() and the refcount
	 * increment, the folio was split, in which case we'd end up
	 * holding a reference on a folio that has nothing to do with the page
	 * we were given anymore.
	 * So now that the folio is stable, recheck that the page still
	 * belongs to this folio.
	 */
	if (unlikely(page_folio(page) != folio)) {
		if (!put_devmap_managed_page_refs(&folio->page, refs))
			folio_put_refs(folio, refs);
		goto retry;
	}

	return folio;
}

try_get_folio()는 nonzero reference를 얻은 다음 subpage의 folio identity가 그대로인지 확인합니다. Split로 달라졌다면 얻은 쪽을 put하고 재시도합니다. PIN의 order-0 bias에는 이 preliminary reference 1이 이미 들어 있어 BIAS - 1을 추가합니다. Long-term 부적합 page는 여기서 직접 migration하지 않습니다.

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

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1.8. 현재 optimistic walk·reference undo와 wrapper는 Linux v6.18 mm/gup.c, table lifetime 계약은 include/asm-generic/tlb.h를 참고하실 수 있습니다.