madvise()라는 이름 때문에 모든 요청을 “해도 되고 안 해도 되는 최적화 조언”으로 생각하기 쉽습니다. 그러나 DONTNEED나 REMOVE는 다음 read의 contents를 바꿀 수 있습니다. advice마다 보존하는 대상과 범위가 다르므로 먼저 data semantics를 확인해야 합니다.
advice는 하나의 동작 값입니다
address는 page-aligned여야 하고 length는 page 단위로 다룹니다. advice는 일반적인 OR 조합형 flags가 아니라 한 동작 값입니다. kernel은 지원하는 값과 configuration을 확인하고 range overflow·empty range를 검사합니다. remote process_madvise는 허용 advice와 권한 검사가 별도입니다.
RANDOM·SEQUENTIAL·NORMAL은 access pattern을 전달하고 VMA의 read-ahead 정책에 영향을 줍니다. WILLNEED는 backing에 따라 file readahead 또는 swap-in 등 서로 다른 준비를 요청합니다. 이것이 모든 requested page가 반환 시 resident하고 계속 남는다는 보장은 아닙니다.
DONTNEED, FREE, REMOVE
private anonymous DONTNEED는 PTE 관계와 reference를 정리하여 contents를 버릴 수 있습니다. VMA는 남아 다음 read가 zero-fill로 해결될 수 있습니다. file mapping은 backing을 다시 fault하므로 dirty data를 즉시 disk에 쓰는 sync 요청이 아닙니다. shared shmem의 data까지 private anonymous처럼 모두 사라진다고 일반화하면 안 됩니다.
FREE는 private anonymous의 lazy-free 정책입니다. reclaim 전에는 기존 byte를 읽을 수 있고 reclaim 뒤에는 zero-fill이 될 수 있으며 재쓰기는 다시 보존할 data를 만듭니다. REMOVE는 지원 filesystem의 shared writable range에 hole punching을 요청합니다. mapping permission과 filesystem 지원, locked-range 제약을 만족해야 하며 file data 자체의 의미를 바꿉니다.
같은 byte를 담았던 서로 독립적인 세 buffer를 생각해 보겠습니다. Private anonymous DONTNEED는 다음 fault에서 zero-fill이 될 수 있고, FREE는 reclaim 전 기존 byte를 보다가 나중에 사라질 수 있습니다. Shared writable file의 REMOVE는 mapping만이 아니라 backing file의 hole을 만듭니다. 세 요청은 서로 대체할 수 없습니다.
일반 DONTNEED의 locked-page 제한과 DONTNEED_LOCKED 같은 별도 advice를 구별합니다. 어떤 discard도 device가 사용하는 pin을 임의로 해제하는 API는 아닙니다. 성공 뒤 RSS 감소량을 data 변경량이나 즉시 free한 physical page 수로 치환하지 않습니다.
fork, dump와 reclaim 정책
DONTFORK·DOFORK는 child mapping 상속을 제어하고, private anonymous의 WIPEONFORK·KEEPONFORK는 child가 보게 될 초기 contents 정책을 바꿉니다. DONTDUMP·DODUMP는 core dump 포함 여부이며 접근 permission 자체를 지우지 않습니다. 이들에는 VM_IO·VM_SPECIAL 등 backing별 제약이 있습니다.
COLD는 사용 이력을 낮추어 reclaim 후보로 만들고 PAGEOUT은 더 직접적인 회수를 시도합니다. MERGEABLE·UNMERGEABLE은 KSM, HUGEPAGE·NOHUGEPAGE는 THP 정책과 configuration에 관계합니다. HWPOISON·SOFT_OFFLINE은 failure-management 기능이며 일반 성능 튜닝을 위해 실행할 동작이 아닙니다. POPULATE_READ·WRITE는 필요한 fault와 CoW를 앞당기지만 pin이나 mlock의 수명 보장은 제공하지 않습니다.
range 전체가 원자적으로 변경되지는 않습니다
VMA 순회는 범위 안의 각 VMA에 해당 부분만 clamp하여 visit합니다. hole을 만나면 ENOMEM을 기록하면서도 뒤의 유효 VMA에 작업할 수 있는 경로가 있습니다. callback이 merge해 이전·다음 관계를 바꾸면 iterator도 새 끝에 맞추어야 합니다. error가 났다고 앞부분까지 모두 rollback됐다고 가정할 수 없습니다.
flags나 anonymous name을 바꾸는 advice는 값이 실제로 다른지 먼저 확인합니다. 변경이 필요하면 merge를 시도하고 안 되면 부분 VMA를 split합니다. data-page 작업과 metadata 변경은 요구하는 lock이 다릅니다. v6.18의 lock 선택은 단순한 mmap read/write 두 종류보다 세분화되어 있으므로 현재 get_lock_mode·madvise_lock 계약을 보아야 합니다.
동작별 보존 대상을 먼저 정합니다
int madvise(void *addr, size_t length, int advice);
이 prototype은 user API를 설명하기 위한 선언이며 kernel 함수 body가 아닙니다. User library interface는 실패 시 -1과 errno를 사용하는 반면, 아래 kernel 내부 helper는 negative errno를 반환합니다. 다음은 v6.0에서 다루는 behavior를 목적별로 다시 정리한 표입니다. OR할 flag 조합 목록이 아니며 backing·configuration별 추가 검사가 남아 있습니다.
| Behavior | 바꾸려는 대상 | 잊지 말아야 할 제한 |
|---|---|---|
| NORMAL / RANDOM / SEQUENTIAL | Access-pattern metadata | RANDOM은 RAND_READ, SEQUENTIAL은 SEQ_READ이며 서로 해제합니다 |
| WILLNEED | File readahead 또는 swap-in 준비 | 모든 page의 영구 residency나 population 성공 보장이 아닙니다 |
| DONTNEED / DONTNEED_LOCKED | 현재 translation과 해당 reference | Private anonymous contents를 버릴 수 있고 locked 허용이 다릅니다 |
| FREE | Private anonymous의 lazy-free 상태 | Reclaim 전후 data 의미가 달라지며 재쓰기와 경쟁합니다 |
| REMOVE | Shared writable file backing의 hole | Filesystem의 hole-punch 지원과 권한이 필요합니다 |
| DONTFORK / DOFORK | Child의 VMA 상속 | DOFORK에는 IO mapping 제약이 있습니다 |
| WIPEONFORK / KEEPONFORK | Child contents 정책 | WIPE는 private anonymous만 허용하며 parent를 지우는 명령이 아닙니다 |
| DONTDUMP / DODUMP | Core dump 포함 여부 | DODUMP의 special mapping 제한을 확인합니다 |
| COLD / PAGEOUT | Reclaim 우선순위와 회수 시도 | 즉시 모든 physical memory가 반환된다는 보장은 아닙니다 |
| POPULATE_READ / POPULATE_WRITE | 명시적인 prefault | WRITE는 필요한 CoW를 준비하지만 pin·mlock을 대신하지 않습니다 |
| MERGEABLE / UNMERGEABLE | KSM 정책 | 해당 configuration과 VMA 조건이 필요합니다 |
| HUGEPAGE / NOHUGEPAGE | THP 정책 | Policy 요청과 실제 huge allocation 성공은 다릅니다 |
| HWPOISON / SOFT_OFFLINE | Memory failure 관리 | 일반 성능 실습에서 실행할 advice가 아닙니다 |
특히 SEQUENTIAL이 VM_RAND_READ를 세운다는 설명은 source의 실제 분기와 반대입니다. 또한 DONTNEED·REMOVE처럼 data 의미를 바꾸는 요청까지 “kernel이 무시해도 되는 hint”로 통일하지 않겠습니다.
Validation, lock 선택과 순회를 전체 source로 읽습니다
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:1371-1422
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int behavior)
{
unsigned long end;
int error;
int write;
size_t len;
struct blk_plug plug;
start = untagged_addr(start);
if (!madvise_behavior_valid(behavior))
return -EINVAL;
if (!PAGE_ALIGNED(start))
return -EINVAL;
len = PAGE_ALIGN(len_in);
/* Check to see whether len was rounded up from small -ve to zero */
if (len_in && !len)
return -EINVAL;
end = start + len;
if (end < start)
return -EINVAL;
if (end == start)
return 0;
#ifdef CONFIG_MEMORY_FAILURE
if (behavior == MADV_HWPOISON || behavior == MADV_SOFT_OFFLINE)
return madvise_inject_error(behavior, start, start + len_in);
#endif
write = madvise_need_mmap_write(behavior);
if (write) {
if (mmap_write_lock_killable(mm))
return -EINTR;
} else {
mmap_read_lock(mm);
}
blk_start_plug(&plug);
error = madvise_walk_vmas(mm, start, end, behavior,
madvise_vma_behavior);
blk_finish_plug(&plug);
if (write)
mmap_write_unlock(mm);
else
mmap_read_unlock(mm);
return error;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:1131-1170
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static bool
madvise_behavior_valid(int behavior)
{
switch (behavior) {
case MADV_DOFORK:
case MADV_DONTFORK:
case MADV_NORMAL:
case MADV_SEQUENTIAL:
case MADV_RANDOM:
case MADV_REMOVE:
case MADV_WILLNEED:
case MADV_DONTNEED:
case MADV_DONTNEED_LOCKED:
case MADV_FREE:
case MADV_COLD:
case MADV_PAGEOUT:
case MADV_POPULATE_READ:
case MADV_POPULATE_WRITE:
#ifdef CONFIG_KSM
case MADV_MERGEABLE:
case MADV_UNMERGEABLE:
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
case MADV_HUGEPAGE:
case MADV_NOHUGEPAGE:
#endif
case MADV_DONTDUMP:
case MADV_DODUMP:
case MADV_WIPEONFORK:
case MADV_KEEPONFORK:
#ifdef CONFIG_MEMORY_FAILURE
case MADV_SOFT_OFFLINE:
case MADV_HWPOISON:
#endif
return true;
default:
return false;
}
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:50-67
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int madvise_need_mmap_write(int behavior)
{
switch (behavior) {
case MADV_REMOVE:
case MADV_WILLNEED:
case MADV_DONTNEED:
case MADV_DONTNEED_LOCKED:
case MADV_COLD:
case MADV_PAGEOUT:
case MADV_FREE:
case MADV_POPULATE_READ:
case MADV_POPULATE_WRITE:
return 0;
default:
/* be safe, default to 1. list exceptions explicitly */
return 1;
}
}
madvise_behavior_valid()가 true여도 특정 VMA에서 요청을 수행할 수 있다는 보장은 아닙니다. 먼저 구현·configuration이 지원하는 behavior인지 확인한 것입니다. do_madvise()는 alignment, round-up wrap과 end overflow를 검사합니다. v6.0에서는 VMA flags를 바꿀 수 있는 behavior에 write lock을 기본값으로 택하고, data 작업을 수행하는 특정 behavior는 read lock 경로를 사용합니다. Read lock은 “동기화가 없다”가 아닙니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:1193-1250
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static
int madvise_walk_vmas(struct mm_struct *mm, unsigned long start,
unsigned long end, unsigned long arg,
int (*visit)(struct vm_area_struct *vma,
struct vm_area_struct **prev, unsigned long start,
unsigned long end, unsigned long arg))
{
struct vm_area_struct *vma;
struct vm_area_struct *prev;
unsigned long tmp;
int unmapped_error = 0;
/*
* If the interval [start,end) covers some unmapped address
* ranges, just ignore them, but return -ENOMEM at the end.
* - different from the way of handling in mlock etc.
*/
vma = find_vma_prev(mm, start, &prev);
if (vma && start > vma->vm_start)
prev = vma;
for (;;) {
int error;
/* Still start < end. */
if (!vma)
return -ENOMEM;
/* Here start < (end|vma->vm_end). */
if (start < vma->vm_start) {
unmapped_error = -ENOMEM;
start = vma->vm_start;
if (start >= end)
break;
}
/* Here vma->vm_start <= start < (end|vma->vm_end) */
tmp = vma->vm_end;
if (end < tmp)
tmp = end;
/* Here vma->vm_start <= start < tmp <= (end|vma->vm_end). */
error = visit(vma, &prev, start, tmp, arg);
if (error)
return error;
start = tmp;
if (prev && start < prev->vm_end)
start = prev->vm_end;
if (start >= end)
break;
if (prev)
vma = prev->vm_next;
else /* madvise_remove dropped mmap_lock */
vma = find_vma(mm, start);
}
return unmapped_error;
}
Walker는 hole에서 ENOMEM을 기억하면서 뒤의 유효 범위에 계속 작업할 수 있습니다. 그러나 callback 자체가 error를 반환하면 그 지점에서 끝납니다. 따라서 “어떤 error든 전체 뒤쪽까지 모두 처리한다”도, “error면 앞부분은 모두 rollback된다”도 옳지 않습니다. prev = NULL은 helper가 lock을 놓아 다음 VMA를 다시 찾아야 하는 protocol일 수 있습니다.
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:992-1079
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int madvise_vma_behavior(struct vm_area_struct *vma,
struct vm_area_struct **prev,
unsigned long start, unsigned long end,
unsigned long behavior)
{
int error;
struct anon_vma_name *anon_name;
unsigned long new_flags = vma->vm_flags;
switch (behavior) {
case MADV_REMOVE:
return madvise_remove(vma, prev, start, end);
case MADV_WILLNEED:
return madvise_willneed(vma, prev, start, end);
case MADV_COLD:
return madvise_cold(vma, prev, start, end);
case MADV_PAGEOUT:
return madvise_pageout(vma, prev, start, end);
case MADV_FREE:
case MADV_DONTNEED:
case MADV_DONTNEED_LOCKED:
return madvise_dontneed_free(vma, prev, start, end, behavior);
case MADV_POPULATE_READ:
case MADV_POPULATE_WRITE:
return madvise_populate(vma, prev, start, end, behavior);
case MADV_NORMAL:
new_flags = new_flags & ~VM_RAND_READ & ~VM_SEQ_READ;
break;
case MADV_SEQUENTIAL:
new_flags = (new_flags & ~VM_RAND_READ) | VM_SEQ_READ;
break;
case MADV_RANDOM:
new_flags = (new_flags & ~VM_SEQ_READ) | VM_RAND_READ;
break;
case MADV_DONTFORK:
new_flags |= VM_DONTCOPY;
break;
case MADV_DOFORK:
if (vma->vm_flags & VM_IO)
return -EINVAL;
new_flags &= ~VM_DONTCOPY;
break;
case MADV_WIPEONFORK:
/* MADV_WIPEONFORK is only supported on anonymous memory. */
if (vma->vm_file || vma->vm_flags & VM_SHARED)
return -EINVAL;
new_flags |= VM_WIPEONFORK;
break;
case MADV_KEEPONFORK:
new_flags &= ~VM_WIPEONFORK;
break;
case MADV_DONTDUMP:
new_flags |= VM_DONTDUMP;
break;
case MADV_DODUMP:
if (!is_vm_hugetlb_page(vma) && new_flags & VM_SPECIAL)
return -EINVAL;
new_flags &= ~VM_DONTDUMP;
break;
case MADV_MERGEABLE:
case MADV_UNMERGEABLE:
error = ksm_madvise(vma, start, end, behavior, &new_flags);
if (error)
goto out;
break;
case MADV_HUGEPAGE:
case MADV_NOHUGEPAGE:
error = hugepage_madvise(vma, &new_flags, behavior);
if (error)
goto out;
break;
}
anon_name = anon_vma_name(vma);
anon_vma_name_get(anon_name);
error = madvise_update_vma(vma, prev, start, end, new_flags,
anon_name);
anon_vma_name_put(anon_name);
out:
/*
* madvise() returns EAGAIN if kernel resources, such as
* slab, are temporarily unavailable.
*/
if (error == -ENOMEM)
error = -EAGAIN;
return error;
}
// SPDX-License-Identifier: GPL-2.0
/*
* linux/mm/madvise.c
*
* Copyright (C) 1999 Linus Torvalds
* Copyright (C) 2002 Christoph Hellwig
*/
/* Upstream Linux v6.0: mm/madvise.c:139-192
* Exact source lines; kernel context, not a standalone program or book restoration.
*/
static int madvise_update_vma(struct vm_area_struct *vma,
struct vm_area_struct **prev, unsigned long start,
unsigned long end, unsigned long new_flags,
struct anon_vma_name *anon_name)
{
struct mm_struct *mm = vma->vm_mm;
int error;
pgoff_t pgoff;
if (new_flags == vma->vm_flags && anon_vma_name_eq(anon_vma_name(vma), anon_name)) {
*prev = vma;
return 0;
}
pgoff = vma->vm_pgoff + ((start - vma->vm_start) >> PAGE_SHIFT);
*prev = vma_merge(mm, *prev, start, end, new_flags, vma->anon_vma,
vma->vm_file, pgoff, vma_policy(vma),
vma->vm_userfaultfd_ctx, anon_name);
if (*prev) {
vma = *prev;
goto success;
}
*prev = vma;
if (start != vma->vm_start) {
if (unlikely(mm->map_count >= sysctl_max_map_count))
return -ENOMEM;
error = __split_vma(mm, vma, start, 1);
if (error)
return error;
}
if (end != vma->vm_end) {
if (unlikely(mm->map_count >= sysctl_max_map_count))
return -ENOMEM;
error = __split_vma(mm, vma, end, 0);
if (error)
return error;
}
success:
/*
* vm_flags is protected by the mmap_lock held in write mode.
*/
vma->vm_flags = new_flags;
if (!vma->vm_file) {
error = replace_anon_vma_name(vma, anon_name);
if (error)
return error;
}
return 0;
}
Data 작업을 전담 helper로 넘기는 branch와 new_flags를 만들고 VMA를 갱신하는 branch를 나누어 읽습니다. Anonymous name의 reference는 기존 VMA가 merge로 사라질 수 있기 때문에 별도로 올렸다 내립니다. madvise_update_vma()는 같은 flags·name이면 빠져나오고, logical pgoff를 계산해 merge를 시도한 뒤 필요한 경계를 split합니다. 두 번째 split이나 name 교체가 실패했을 때 첫 단계까지 저절로 되돌려진다고 가정하지 않습니다.
반환 error도 발생 경로를 구별해야 합니다. v6.0 madvise_vma_behavior()의 공통 out 경로는 metadata 갱신이나 KSM·THP helper에서 받은 -ENOMEM을 -EAGAIN으로 바꿉니다. 그러나 앞에서 곧바로 return하는 data helper의 error나 walker가 hole 때문에 기억한 -ENOMEM까지 이 변환을 거치는 것은 아닙니다. 따라서 ENOMEM을 모두 “잠시 후 재시도하면 되는 자원 부족”으로 해석하거나 EAGAIN 하나로 범위 전체의 처리 여부를 판단해서는 안 됩니다.
Source: Linux v6.0 mm/madvise.c. 전체 explicit definition과 원래 notice, COPYING, GPL-2.0를 보존했습니다. v6.0은 historical reference이고 book exact tag는 미확정입니다. v6.18은 별도 비교 tag이며 latest가 아닙니다. Compile·runtime 검증은 하지 않았습니다.
참고 자료
Lorenzo Stoakes, The Linux Memory Manager, Chapter 8 §8.2.4. madvise(2), Linux v6.18 mm/madvise.c를 참고하실 수 있습니다.