get_user_pages()와 이름이 비슷한 함수들이 많은 이유는 단순한 속도 옵션 때문만은 아닙니다. 현재 mm인지 remote mm인지, caller가 mmap lock을 보유했는지, fault가 그 lock을 내려놓아도 되는지에 따라 반환 계약이 달라집니다.
output array와 반환 개수
입력 start와 nr_pages는 user 주소 범위와 base-page 수를 나타냅니다. pages array에는 성공한 page pointer들이 담기며 caller가 필요한 capacity를 확보해야 합니다. GET 또는 PIN을 얻었다면 나중에 반환할 pointer가 필요합니다. 성공한 부분이 있으면 후속 error보다 positive count가 먼저 반환될 수 있습니다.
이전 GUP interface에는 선택적인 VMA output array도 있었습니다. 그 pointer는 VMA를 보호하는 lock이 유지될 때만 유효하므로 lock을 놓는 retry와 함께 제공할 수 없었습니다. v6.18 공개 get/pin interface에는 같은 vmas 인자가 없습니다. 오래된 prototype을 새 driver에 맞추어 사용해서는 안 됩니다.
locked와 unlocked의 차이
caller가 mmap read lock을 가진 entry와 wrapper가 lock을 얻고 정리하는 unlocked entry를 구별합니다. remote entry는 다른 mm의 참조와 접근 권한까지 caller protocol에 포함해야 합니다. 이름에 remote가 있다고 임의 process memory에 접근할 권한이 생기지 않습니다.
locked output state를 전달하는 경로는 fault가 lock을 놓았다는 사실을 caller에 알립니다. 다시 얻었다가 끝에 놓더라도 caller가 stale VMA state를 사용하지 않도록 “한 번이라도 놓았음”을 전달하는 흐름이 중요합니다. fault 실행이나 retry 대기를 제한하는 NOFAULT·NOWAIT와 lock handoff도 같은 개념이 아닙니다. 이 flags만으로 바깥 wrapper의 mmap lock 획득까지 nonblocking이 되지는 않습니다.
follow flags는 모두 공개 조합형 옵션이 아닙니다
WRITE는 접근 방향, TOUCH는 accessed·dirty 반영, FORCE는 제한된 권한 override, DUMP는 core dump의 hole 처리, HWPOISON은 더 구체적인 오류 전달에 관계합니다. NOFAULT는 필요한 fault를 하지 않으며 NOWAIT는 대기를 제한합니다. LONGTERM은 pin 전용 제약을 더합니다.
MIGRATION·NUMA·SPLIT_PMD 같은 이름은 기존 구현의 내부 walk protocol도 포함합니다. tag마다 허용 집합과 wrapper가 설정하는 bit가 다릅니다. 모든 follow flag를 모든 fast wrapper에 전달할 수 없으며 validator가 거부할 수 있습니다. 특히 FORCE는 shared mapping의 file write permission을 우회하는 만능 스위치가 아닙니다.
long-term은 한번 얻고 끝나지 않을 수 있습니다
slow long-term 경로는 allocation context에 pin 의도를 전달하고 candidate를 조회합니다. 부적합한 folio가 있으면 LRU drain과 isolation을 시도하고, 획득한 pin을 먼저 반환한 뒤 migration을 진행합니다. migration을 성공시키려는데 자신이 보유한 pin으로 이동을 막으면 안 되기 때문입니다.
이후 새 mapping을 다시 조회하여 pin합니다. 이동 실패·isolation 실패에서는 list와 reference를 복구하고 error 또는 진행량을 돌려줍니다. zero page처럼 이동할 필요가 없는 대상과 device-coherent memory처럼 별도 제한이 있는 대상도 구별합니다. “장기 pin이면 처음 얻은 PFN을 영원히 유지한 채 적합성을 검사한다”는 모델은 틀립니다.
Historical API를 선택하는 새 기준표
다음 표는 v6.0의 source 계약을 작업별로 다시 정리한 것입니다. 모든 flag를 모든 wrapper에 전달할 수 있다는 뜻은 아닙니다. pages에는 요청 수만큼 capacity가 필요하지만 유효 결과는 반환된 수만큼입니다. GET/PIN reference는 array에 돌려받아 대응 API로 반환합니다.
| 용도와 entry | 진입 lock | 핵심 차이 | 반환 후 책임 |
|---|---|---|---|
Local get_user_pages |
Caller의 mmap lock | TOUCH, pages가 있으면 GET | 성공한 page만 put; VMA는 lock 안에서만 사용 |
Local pin_user_pages |
Caller의 mmap lock | PIN, pages 필수, GET 금지 | Data 사용 종료와 dirty 처리를 마친 뒤 unpin |
| Remote get/pin | 대상 mm의 lock과 수명 보장 | REMOTE, 선택적 locked handoff | Lock을 놓았으면 옛 VMA 사용 금지 |
| Unlocked get/pin | Caller가 잡지 않고 진입 | 내부 획득 및 retry-aware 해제 | 반환 count의 reference 정리 |
| Fast get/pin | Fast phase에 mmap lock 불필요 | 제한된 flag, 일반 fast는 slow fallback 가능 | Lockless가 atomic-context 사용 허가는 아님 |
| Fast-only get/pin | Fast phase에 mmap lock 불필요 | Slow fallback 없이 short count 가능 | 0 또는 partial count도 정상 계약에 포함 |
전체 local·remote wrapper를 함께 보시면 PIN을 caller가 직접 get API의 flag로 끼워 넣는 방식이 아님을 확인할 수 있습니다. FOLL_LONGTERM은 pin family의 더 제한된 요청입니다. Local pin은 이 version에서 get과 달리 TOUCH를 자동으로 추가하지 않지만 remote 공통 helper는 TOUCH를 추가합니다.
여기서 local pin은 pin_user_pages()입니다. v6.0의 unlocked pin은 get_user_pages_unlocked()를 거치므로 그 안에서 FOLL_TOUCH가 추가됩니다. 같은 helper가 FOLL_LONGTERM을 거절하므로, 이 tag의 pin_user_pages_unlocked()에 LONGTERM을 주어도 -EINVAL입니다. remote pin도 LONGTERM과 non-NULL locked의 조합은 공통 remote helper에서 거절합니다. pin 계열이라는 공통점만으로 모든 entry가 같은 flag 조합을 지원하는 것은 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2245-2254
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas)
{
if (!is_valid_gup_flags(gup_flags))
return -EINVAL;
return __gup_longterm_locked(current->mm, start, nr_pages,
pages, vmas, gup_flags | FOLL_TOUCH);
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2089-2106
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static bool is_valid_gup_flags(unsigned int gup_flags)
{
/*
* FOLL_PIN must only be set internally by the pin_user_pages*() APIs,
* never directly by the caller, so enforce that with an assertion:
*/
if (WARN_ON_ONCE(gup_flags & FOLL_PIN))
return false;
/*
* FOLL_PIN is a prerequisite to FOLL_LONGTERM. Another way of saying
* that is, FOLL_LONGTERM is a specific case, more restrictive case of
* FOLL_PIN.
*/
if (WARN_ON_ONCE(gup_flags & FOLL_LONGTERM))
return false;
return true;
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:3221-3235
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long pin_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas)
{
/* 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 __gup_longterm_locked(current->mm, start, nr_pages,
pages, vmas, gup_flags);
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2198-2208
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long get_user_pages_remote(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
if (!is_valid_gup_flags(gup_flags))
return -EINVAL;
return __get_user_pages_remote(mm, start, nr_pages, gup_flags,
pages, vmas, locked);
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:3186-3201
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long pin_user_pages_remote(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
/* 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 __get_user_pages_remote(mm, start, nr_pages, gup_flags,
pages, vmas, locked);
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2109-2136
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static long __get_user_pages_remote(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
/*
* Parts of FOLL_LONGTERM behavior are incompatible with
* FAULT_FLAG_ALLOW_RETRY because of the FS DAX check requirement on
* vmas. However, this only comes up if locked is set, and there are
* callers that do request FOLL_LONGTERM, but do not set locked. So,
* allow what we can.
*/
if (gup_flags & FOLL_LONGTERM) {
if (WARN_ON_ONCE(locked))
return -EINVAL;
/*
* This will check the vmas (even if our vmas arg is NULL)
* and return -ENOTSUPP if DAX isn't allowed in this case:
*/
return __gup_longterm_locked(mm, start, nr_pages, pages,
vmas, gup_flags | FOLL_TOUCH |
FOLL_REMOTE);
}
return __get_user_pages_locked(mm, start, nr_pages, pages, vmas,
locked,
gup_flags | FOLL_TOUCH | FOLL_REMOTE);
}
Long-term 검사와 재조회는 하나의 loop입니다
__gup_longterm_locked()는 allocation context를 잠시 바꾸고, 얻은 page들이 long-term pin에 적합한지 확인한 뒤 context를 복원합니다. check_and_migrate_movable_pages()의 반환값은 적합한 page 수, 다시 pin할 필요를 뜻하는 0, 또는 error입니다. 이동할 대상은 장기 pin에 부적합한 movable 쪽이며 “모든 non-movable page를 이동”하는 작업이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2063-2087
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static long __gup_longterm_locked(struct mm_struct *mm,
unsigned long start,
unsigned long nr_pages,
struct page **pages,
struct vm_area_struct **vmas,
unsigned int gup_flags)
{
unsigned int flags;
long rc;
if (!(gup_flags & FOLL_LONGTERM))
return __get_user_pages_locked(mm, start, nr_pages, pages, vmas,
NULL, gup_flags);
flags = memalloc_pin_save();
do {
rc = __get_user_pages_locked(mm, start, nr_pages, pages, vmas,
NULL, gup_flags);
if (rc <= 0)
break;
rc = check_and_migrate_movable_pages(rc, pages, gup_flags);
} while (!rc);
memalloc_pin_restore(flags);
return rc;
}
check_and_migrate_movable_pages.c
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1928-2057
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
#ifdef CONFIG_MIGRATION
/*
* Check whether all pages are pinnable, if so return number of pages. If some
* pages are not pinnable, migrate them, and unpin all pages. Return zero if
* pages were migrated, or if some pages were not successfully isolated.
* Return negative error if migration fails.
*/
static long check_and_migrate_movable_pages(unsigned long nr_pages,
struct page **pages,
unsigned int gup_flags)
{
unsigned long isolation_error_count = 0, i;
struct folio *prev_folio = NULL;
LIST_HEAD(movable_page_list);
bool drain_allow = true, coherent_pages = false;
int ret = 0;
for (i = 0; i < nr_pages; i++) {
struct folio *folio = page_folio(pages[i]);
if (folio == prev_folio)
continue;
prev_folio = folio;
/*
* Device coherent pages are managed by a driver and should not
* be pinned indefinitely as it prevents the driver moving the
* page. So when trying to pin with FOLL_LONGTERM instead try
* to migrate the page out of device memory.
*/
if (folio_is_device_coherent(folio)) {
/*
* We always want a new GUP lookup with device coherent
* pages.
*/
pages[i] = 0;
coherent_pages = true;
/*
* Migration will fail if the page is pinned, so convert
* the pin on the source page to a normal reference.
*/
if (gup_flags & FOLL_PIN) {
get_page(&folio->page);
unpin_user_page(&folio->page);
}
ret = migrate_device_coherent_page(&folio->page);
if (ret)
goto unpin_pages;
continue;
}
if (folio_is_longterm_pinnable(folio))
continue;
/*
* Try to move out any movable page before pinning the range.
*/
if (folio_test_hugetlb(folio)) {
if (isolate_hugetlb(&folio->page,
&movable_page_list))
isolation_error_count++;
continue;
}
if (!folio_test_lru(folio) && drain_allow) {
lru_add_drain_all();
drain_allow = false;
}
if (folio_isolate_lru(folio)) {
isolation_error_count++;
continue;
}
list_add_tail(&folio->lru, &movable_page_list);
node_stat_mod_folio(folio,
NR_ISOLATED_ANON + folio_is_file_lru(folio),
folio_nr_pages(folio));
}
if (!list_empty(&movable_page_list) || isolation_error_count ||
coherent_pages)
goto unpin_pages;
/*
* If list is empty, and no isolation errors, means that all pages are
* in the correct zone.
*/
return nr_pages;
unpin_pages:
/*
* pages[i] might be NULL if any device coherent pages were found.
*/
for (i = 0; i < nr_pages; i++) {
if (!pages[i])
continue;
if (gup_flags & FOLL_PIN)
unpin_user_page(pages[i]);
else
put_page(pages[i]);
}
if (!list_empty(&movable_page_list)) {
struct migration_target_control mtc = {
.nid = NUMA_NO_NODE,
.gfp_mask = GFP_USER | __GFP_NOWARN,
};
ret = migrate_pages(&movable_page_list, alloc_migration_target,
NULL, (unsigned long)&mtc, MIGRATE_SYNC,
MR_LONGTERM_PIN, NULL);
if (ret > 0) /* number of pages not migrated */
ret = -ENOMEM;
}
if (ret && !list_empty(&movable_page_list))
putback_movable_pages(&movable_page_list);
return ret;
}
#else
static long check_and_migrate_movable_pages(unsigned long nr_pages,
struct page **pages,
unsigned int gup_flags)
{
return nr_pages;
}
#endif /* CONFIG_MIGRATION */
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: include/linux/mm.h:1565-1568
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static inline bool folio_is_longterm_pinnable(struct folio *folio)
{
return is_longterm_pinnable_page(&folio->page);
}
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: include/linux/mm.h:1538-1563
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
#ifdef CONFIG_MIGRATION
static inline bool is_longterm_pinnable_page(struct page *page)
{
#ifdef CONFIG_CMA
int mt = get_pageblock_migratetype(page);
if (mt == MIGRATE_CMA || mt == MIGRATE_ISOLATE)
return false;
#endif
/* The zero page may always be pinned */
if (is_zero_pfn(page_to_pfn(page)))
return true;
/* Coherent device memory must always allow eviction. */
if (is_device_coherent_page(page))
return false;
/* Otherwise, non-movable zone pages can be pinned. */
return !is_zone_movable_page(page);
}
#else
static inline bool is_longterm_pinnable_page(struct page *page)
{
return true;
}
#endif
/* SPDX-License-Identifier: GPL-2.0 */
/* Upstream Linux v6.0: include/linux/mmzone.h:811-814
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static inline bool is_zone_movable_page(const struct page *page)
{
return page_zonenum(page) == ZONE_MOVABLE;
}
Book에서 빠진 CMA·huge·device-coherent 분기를 전체 source에 남겼습니다. Device-coherent page의 제한은 이 helper에서의 long-term 제약이지 모든 접근을 금지한다는 뜻이 아닙니다. Isolated list를 구성한 뒤에는 이번 호출에서 얻은 pin/reference 전체를 해당 종류에 맞게 내려놓고 이동합니다. 실패하면 남은 list를 putback해야 합니다.
위의 두 placement helper는 v6.0 CONFIG_MIGRATION 분기와 비활성 분기를 함께 실었습니다. 이 설정이 없으면 is_longterm_pinnable_page()는 true, check_and_migrate_movable_pages()는 입력 page 수를 그대로 반환하는 stub입니다. 따라서 migration을 위한 isolation·재조회 loop를 모든 kernel 구성에서 실행한다고 설명할 수 없습니다. CMA 검사는 migration 본체 안에서도 CONFIG_CMA 조건을 따릅니다. 이 차이는 현재 사용 중인 kernel의 configuration을 실제로 확인했다는 뜻이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2892-2913
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int __gup_longterm_unlocked(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages)
{
int ret;
/*
* FIXME: FOLL_LONGTERM does not work with
* get_user_pages_unlocked() (see comments in that function)
*/
if (gup_flags & FOLL_LONGTERM) {
mmap_read_lock(current->mm);
ret = __gup_longterm_locked(current->mm,
start, nr_pages,
pages, NULL, gup_flags);
mmap_read_unlock(current->mm);
} else {
ret = get_user_pages_unlocked(start, nr_pages,
pages, gup_flags);
}
return ret;
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:2272-2294
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long get_user_pages_unlocked(unsigned long start, unsigned long nr_pages,
struct page **pages, unsigned int gup_flags)
{
struct mm_struct *mm = current->mm;
int locked = 1;
long ret;
/*
* FIXME: Current FOLL_LONGTERM behavior is incompatible with
* FAULT_FLAG_ALLOW_RETRY because of the FS DAX check requirement on
* vmas. As there are no users of this flag in this call we simply
* disallow this option for now.
*/
if (WARN_ON_ONCE(gup_flags & FOLL_LONGTERM))
return -EINVAL;
mmap_read_lock(mm);
ret = __get_user_pages_locked(mm, start, nr_pages, pages, NULL,
&locked, gup_flags | FOLL_TOUCH);
if (locked)
mmap_read_unlock(mm);
return ret;
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:3243-3255
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long pin_user_pages_unlocked(unsigned long start, unsigned long nr_pages,
struct page **pages, unsigned int gup_flags)
{
/* 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 get_user_pages_unlocked(start, nr_pages, pages, gup_flags);
}
Unlocked wrapper가 locked를 확인하고서만 unlock하는 이유는 fault가 이미 lock을 놓았을 수 있기 때문입니다. Long-term fallback이 특정 retry interface와 호환되지 않는 이유도 이 historical source의 제약으로 읽겠습니다. 오래된 comment 하나를 근거로 모든 remote GUP가 retry 불가능하다고 확대하지 않습니다.
Follow flag를 상태별로 나누어 읽습니다
WRITE·TOUCH는 접근 방향과 흔적 반영, GET·PIN은 reference 종류, LONGTERM은 placement 제약입니다. DUMP는 불필요한 zero 영역 fault를 막고 HWPOISON은 구체적인 error를 보존합니다. NOFAULT는 fault를 하지 않으며 NOWAIT는 fault retry 대기를 제한합니다. 둘은 동일한 “절대 sleep 없음” 보장이 아닙니다. REMOTE는 대상 mm context를, ANON은 backing 제약을 전달합니다. NUMA·MIGRATION·SPLIT_PMD·TRIED·FAST_ONLY는 walk와 fault protocol에 관계하므로 공개 caller가 임의 조합할 옵션 목록으로 취급하지 않겠습니다.
각 flag가 필요한 이유도 다릅니다. FOLL_MIGRATION은 migration entry를 만났을 때 이동 완료를 기다리고 다시 조회하도록 하며, 없는 page를 새로 할당하라는 뜻은 아닙니다. FOLL_SPLIT_PMD는 PMD의 THP mapping을 나누는 경로를 선택합니다. FOLL_NUMA는 NUMA hinting의 PROTNONE entry를 그냥 통과하지 않고 fault 경로로 보내며, 이 tag의 slow core는 FORCE가 없을 때 이를 더합니다. VMA 자체를 unmap한 뒤 새로 mmap하는 동작과는 다릅니다. FOLL_ANON도 결과 page의 내용만 보고 판단하지 않습니다. VMA가 anonymous인지 검사하므로 private file VMA에서 CoW로 생긴 anonymous page가 있다는 사실만으로 검사를 통과하지는 않습니다.
v6.18과 prototype을 섞지 않습니다
다음은 별도로 고정한 v6.18의 같은 public entry 전체 정의입니다. v6.0에 있던 vmas 인자가 없어졌고 argument validation과 내부 호출도 달라졌습니다. 이것을 v6.0 code의 일부만 바꾼 호환 예제로 사용하지 않습니다. v6.18은 latest라는 뜻도 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.18: mm/gup.c:2644-2654
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages)
{
int locked = 1;
if (!is_valid_gup_args(pages, NULL, &gup_flags, FOLL_TOUCH))
return -EINVAL;
return __get_user_pages_locked(current->mm, start, nr_pages, pages,
&locked, gup_flags);
}
Source: v6.0 mm/gup.c, mm.h, mmzone.h, v6.18 mm/gup.c. Book exact tag는 미확정입니다. 원래 notice와 COPYING, GPL-2.0를 보존했습니다. Compile·runtime 검증은 하지 않았습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1.4–§8.1.5.1. 현재 entry signature·validator·long-term 경로는 Linux v6.18 mm/gup.c, flag 정의는 include/linux/mm.h에 있습니다.