madvise는 성능 hint일 수도 있고 data를 버리는 명령일 수도 있습니다

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로 읽습니다

do_madvise.c

// 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;
}

madvise_behavior_valid.c

// 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;
	}
}

madvise_need_mmap_write.c

// 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은 “동기화가 없다”가 아닙니다.

madvise_walk_vmas.c

// 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일 수 있습니다.

madvise_vma_behavior.c

// 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;
}

madvise_update_vma.c

// 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를 참고하실 수 있습니다.