지연 시간을 줄이려고 mapping을 미리 준비할 수 있습니다. 그러나 PTE를 채우는 요청, page를 reclaim하지 못하게 하는 요청, device가 쓸 physical object를 붙잡는 요청은 서로 다릅니다. GUP의 helper들이 data access 외에도 사용되는 이유입니다.
population은 범위를 VMA별로 나눕니다
__mm_populate() 계열은 주소 범위를 VMA별로 순회하며 필요한 접근을 fault합니다. 읽을 수 없는 영역과 특수 mapping을 처리하는 조건도 있습니다. 하나의 VMA에서 lock이 내려갔으면 다음 iteration은 현재 주소 공간을 다시 확인해야 합니다.
populate_vma_page_range() 같은 helper는 TOUCH를 포함하고 private writable CoW에는 필요한 write fault를 수행할 수 있습니다. 단순한 read population과 write population은 zero page 공유와 anonymous target allocation에서 결과가 다릅니다. FORCE라는 내부 flag가 보인다고 모든 mapping에서 성공을 강제한다는 뜻은 아닙니다.
MAP_POPULATE의 성공 범위
MAP_POPULATE는 mmap 직후 fault 비용을 앞당길 수 있지만 population 실패가 항상 mmap 실패로 전달되지는 않습니다. 그러므로 나중에 fault가 다시 발생할 수 있습니다. MAP_NONBLOCK과 함께 지정하는 legacy 조합은 기대한 population을 하지 않을 수도 있습니다.
반대로 MADV_POPULATE_READ·WRITE는 명시적 population 결과를 다루는 API입니다. 그래도 원자적인 전부 성공·전부 실패가 아니며 중간 error 전에 일부 page가 준비될 수 있습니다. 이후 reclaim, remap이나 protection 변경이 없다는 보장도 주지 않습니다.
READ와 WRITE를 선택할 때
anonymous read는 zero page에 연결되어 private data allocation을 미룰 수 있습니다. WRITE population은 필요한 CoW를 해소해 실제 target을 준비할 수 있습니다. file read는 cache와 PTE를 준비하고 private write는 source contents를 복사할 수 있어 비용의 성격도 다릅니다.
LRU batch drain을 helper 끝에 수행하는 경우에는 새 folio를 즉시 reclaim tracking에 반영하려는 목적이 있습니다. 이것을 physical residency를 영구 보장하는 pin으로 읽어서는 안 됩니다. pin reference를 caller에게 넘기지 않는 population helper에는 대응 unpin 대상도 없습니다.
무엇을 줄이고 무엇을 보장하지 않는가
prefault는 초기 fault와 일부 I/O·allocation을 앞당길 수 있습니다. mlock은 해당 범위를 resident하게 유지하는 정책이고, PIN은 original VA가 없어져도 physical object 수명을 유지하는 protocol입니다. 셋의 성공 조건과 실패 후 cleanup은 다릅니다.
어떤 API를 선택할지는 “다음 접근의 latency를 줄이려는가”, “reclaim을 막으려는가”, “외부 data 접근 동안 frame 수명을 보존하려는가”로 나누면 명확해집니다. 세 요구가 동시에 있으면 하나의 flag가 모두 대신한다고 가정하지 않고 각각의 계약을 충족해야 합니다.
실제 output이 없는 GUP 호출을 확인합니다
아래 v6.0 __mm_populate()는 caller가 mmap lock을 잡지 않은 채 진입하고 VMA에 원하는 flags는 이미 반영되어 있다는 계약입니다. 순회 도중 lock이 내려가면 VMA pointer를 그대로 다음 iteration에 이어 쓰지 않고 현재 주소에서 다시 lookup합니다. IO·PFNMAP은 건너뛰고 VMA와 요청 범위의 교집합을 처리합니다.
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1652-1703
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
{
struct mm_struct *mm = current->mm;
unsigned long end, nstart, nend;
struct vm_area_struct *vma = NULL;
int locked = 0;
long ret = 0;
end = start + len;
for (nstart = start; nstart < end; nstart = nend) {
/*
* We want to fault in pages for [nstart; end) address range.
* Find first corresponding VMA.
*/
if (!locked) {
locked = 1;
mmap_read_lock(mm);
vma = find_vma(mm, nstart);
} else if (nstart >= vma->vm_end)
vma = vma->vm_next;
if (!vma || vma->vm_start >= end)
break;
/*
* Set [nstart; nend) to intersection of desired address
* range with the first VMA. Also, skip undesirable VMA types.
*/
nend = min(end, vma->vm_end);
if (vma->vm_flags & (VM_IO | VM_PFNMAP))
continue;
if (nstart < vma->vm_start)
nstart = vma->vm_start;
/*
* Now fault in a range of pages. populate_vma_page_range()
* double checks the vma flags, so that it won't mlock pages
* if the vma was already munlocked.
*/
ret = populate_vma_page_range(vma, nstart, nend, &locked);
if (ret < 0) {
if (ignore_errors) {
ret = 0;
continue; /* continue at next VMA */
}
break;
}
nend = nstart + ret * PAGE_SIZE;
ret = 0;
}
if (locked)
mmap_read_unlock(mm);
return ret; /* 0 or negative error code */
}
// SPDX-License-Identifier: GPL-2.0-only
/* Upstream Linux v6.0: mm/gup.c:1535-1580
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
long populate_vma_page_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end, int *locked)
{
struct mm_struct *mm = vma->vm_mm;
unsigned long nr_pages = (end - start) / PAGE_SIZE;
int gup_flags;
long ret;
VM_BUG_ON(!PAGE_ALIGNED(start));
VM_BUG_ON(!PAGE_ALIGNED(end));
VM_BUG_ON_VMA(start < vma->vm_start, vma);
VM_BUG_ON_VMA(end > vma->vm_end, vma);
mmap_assert_locked(mm);
/*
* Rightly or wrongly, the VM_LOCKONFAULT case has never used
* faultin_page() to break COW, so it has no work to do here.
*/
if (vma->vm_flags & VM_LOCKONFAULT)
return nr_pages;
gup_flags = FOLL_TOUCH;
/*
* We want to touch writable mappings with a write fault in order
* to break COW, except for shared mappings because these don't COW
* and we would not want to dirty them for nothing.
*/
if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
gup_flags |= FOLL_WRITE;
/*
* We want mlock to succeed for regions that have any permissions
* other than PROT_NONE.
*/
if (vma_is_accessible(vma))
gup_flags |= FOLL_FORCE;
/*
* We made sure addr is within a VMA, so the following will
* not result in a stack expansion that recurses back here.
*/
ret = __get_user_pages(mm, start, nr_pages, gup_flags,
NULL, NULL, locked);
lru_add_drain();
return ret;
}
하위 helper는 pages = NULL, vmas = NULL로 core를 호출합니다. Caller에게 장기 보유할 pin array를 주는 API가 아니라 fault·touch를 수행하는 용도입니다. Private writable mapping에는 WRITE로 CoW를 준비하되 shared mapping을 불필요하게 dirty하게 만들지는 않습니다. LOCKONFAULT는 여기서 즉시 fault하지 않고 count만 진행하는 별도 정책입니다.
FORCE가 있어도 모든 요청이 항상 성공하지는 않습니다. 접근 불가능한 VMA, allocation 실패, signal, mapping 변경과 fault error가 남아 있습니다. ignore_errors도 성공을 만들어 내는 기능이 아니라 어떤 error를 바깥으로 전달하지 않을지를 정하는 caller policy입니다. 이 차이가 MAP_POPULATE와 명시적인 POPULATE advice의 결과 해석에 연결됩니다.
Source: Linux v6.0 mm/gup.c. Historical reference로 book exact tag는 미확정입니다. 원래 notice와 COPYING, GPL-2.0를 보존했습니다. v6.18은 별도 고정 비교 tag이며 compile·runtime 측정은 하지 않았습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.1.9. Linux v6.18 mm/gup.c, mmap(2), madvise(2)를 참고하실 수 있습니다.