Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

Userfaultfd

개요 (Overview)

Userfaultfd는 Linux 커널에서 제공하는 사용자 수준 페이지 폴트 처리 메커니즘입니다. 일반적인 페이지 폴트는 커널이 자동으로 해결하지만, userfaultfd를 사용하면 커널이 폴트를 해결하는 대신 사용자 공간 핸들러에게 위임합니다. 이는 가상 머신(VMM), 실시간 데이터 이동, 메모리 정책 적용 등에서 유용하게 사용됩니다.

사용자 공간은 userfaultfd() 시스템 콜로 파일 디스크립터를 생성한 후, UFFDIO_REGISTER로 모니터링할 메모리 영역을 등록합니다. 이후 해당 영역에서 페이지 폴트가 발생하면 커널은 handle_userfault()를 통해 사용자 핸들러에게 uffd_msg를 전달하고, 사용자는 UFFDIO_COPY, UFFDIO_WRITEPROTECT, UFFDIO_CONTINUE 등의 ioctl로 폴트를 해결합니다. Linux 7.0에서는 UFFDIO_MOVE(zero-copy 페이지 이동)와 UFFDIO_POISON(오염 마커) 기능이 추가되어 더 정교한 메모리 관리가 가능합니다.

일상적인 비유로 보면 userfaultfd는 건물 관리인이 모든 민원을 직접 처리하지 않고, 특정 방에서 문제가 생기면 전담 담당자에게 호출 벨을 울리는 구조와 비슷합니다. 폴트를 낸 스레드는 잠시 멈추고, 사용자 공간 핸들러가 필요한 페이지를 복사하거나 기존 페이지를 연결하거나 쓰기 보호를 해제하면 커널이 다시 스레드를 깨워 같은 주소 접근을 재시도하게 합니다.

소스 파일:

mm/userfaultfd.c                    ← 커널 측 mfill_atomic/move/writeprotect 로직 (2116줄)
fs/userfaultfd.c                    ← 시스템 콜, ioctl, handle_userfault, read/poll (2233줄)
include/linux/userfaultfd_k.h       ← userfaultfd_ctx, mfill_atomic_mode, API 선언 (468줄)
include/uapi/linux/userfaultfd.h    ← UAPI ioctl 정의, uffd_msg, uffdio_* 구조체 (386줄)
include/linux/mm_types.h            ← vm_userfaultfd_ctx (VMA에 등록)

Userfaultfd 호출 흐름

Userfaultfd 호출 흐름

핵심 자료구조 관계도

핵심 자료구조 관계도

빠른 점검 명령

# 1. userfaultfd 커널 지원 확인
grep CONFIG_USERFAULTFD /boot/config-$(uname -r) 2>/dev/null || zgrep CONFIG_USERFAULTFD /proc/config.gz 2>/dev/null

# 2. 현재 시스템의 userfaultfd 관련 프로세스 확인
grep -l userfaultfd /proc/*/fdinfo/* 2>/dev/null | head -5

# 3. userfaultfd 시스템 콜 존재 확인
ausyscall userfaultfd 2>/dev/null || grep -r userfaultfd /usr/include/asm/unistd*.h 2>/dev/null

# 4. /dev/userfaultfd 디바이스 확인
ls -la /dev/userfaultfd 2>/dev/null

# 5. sysctl 설정 확인 (비특권 userfaultfd 허용 여부)
sysctl vm.unprivileged_userfaultfd 2>/dev/null

# 6. perf로 userfaultfd 관련 이벤트 관찰
perf stat -e userfaultfd:uffd_msg -a sleep 1 2>/dev/null || echo "tracepoint not available"

# 7. 커널에서 userfaultfd 관련 심볼 확인
cat /proc/kallsyms | grep -E "handle_userfault|mfill_atomic|move_pages" | head -10

# 8. CONFIG_USERFAULTFD 컴파일 옵션 확인
grep -r "CONFIG_USERFAULTFD" /lib/modules/$(uname -r)/build/.config 2>/dev/null || \
  zcat /proc/config.gz 2>/dev/null | grep CONFIG_USERFAULTFD

# 9. userfaultfd 관련 모듈 로드 상태 확인
lsmod | grep -i userfaultfd 2>/dev/null; echo " Built-in: $(grep CONFIG_USERFAULTFD=y /boot/config-$(uname -r) 2>/dev/null | wc -l)"

# 10. strace로 userfaultfd 시스템 콜 사용 추적
strace -e userfaultfd,ioctl -f <프로세스PID> 2>&1 | grep -E "UFFDIO|userfaultfd" | head -5

# 11. userfaultfd 관련 /proc 인터페이스 확인
cat /proc/vmstat | grep -E "uffd|userfault" 2>/dev/null || echo "no uffd vmstat counters"

# 12. UFFD 기능 플래그 빌드 시 지원 여부 확인
grep -E "UFFD_FEATURE_" include/uapi/linux/userfaultfd.h | head -20

핵심 자료구조

struct userfaultfd_ctx

userfaultfd의 핵심 컨텍스트 구조체입니다. 네 개의 wait queue head를 통해 커널-사용자 간 이벤트 통신을 관리합니다.

// include/linux/userfaultfd_k.h:49-79
struct userfaultfd_ctx {
	/* 대기 중인 (읽히지 않은) 폴트를 위한 waitqueue */
	wait_queue_head_t fault_pending_wqh;
	/* 처리 중인 폴트를 위한 waitqueue */
	wait_queue_head_t fault_wqh;
	/* poll/read를 깨우기 위한 pseudo fd waitqueue */
	wait_queue_head_t fd_wqh;
	/* 이벤트(fork, remap, remove)를 위한 waitqueue */
	wait_queue_head_t event_wqh;
	/* refile 시퀀스 카운트 (fault_pending_wqh lock 보호) */
	seqcount_spinlock_t refile_seq;
	/* pseudo fd 참조 카운트 */
	refcount_t refcount;
	/* userfaultfd 시스템 콜 플래그 (UFFD_USER_MODE_ONLY 등) */
	unsigned int flags;
	/* 사용자 공간이 요청한 기능 (UFFD_FEATURE_*) */
	unsigned int features;
	/* 해제 여부 */
	bool released;
	/* 비협력적 이벤트(mremap 등) 중 mfill/move/wp 차단용 */
	struct rw_semaphore map_changing_lock;
	/* 메모리 매핑 변경 중 플래그 */
	atomic_t mmap_changing;
	/* 이 userfaultfd에 연결된 mm */
	struct mm_struct *mm;
};

struct vm_userfaultfd_ctx

VMA(vm_area_struct)에 userfaultfd를 등록할 때 사용되는 연결 구조체입니다.

// include/linux/mm_types.h:718-722
#ifdef CONFIG_USERFAULTFD
#define NULL_VM_UFFD_CTX ((struct vm_userfaultfd_ctx) { NULL, })
struct vm_userfaultfd_ctx {
	struct userfaultfd_ctx *ctx;  // 실제 userfaultfd_ctx 포인터
};
#endif

struct uffd_msg (read()로 전달되는 메시지)

// include/uapi/linux/userfaultfd.h:108-146
struct uffd_msg {
	__u8	event;

	__u8  reserved1;
	__u16 reserved2;
	__u32 reserved3;

	union {
		struct {
			__u64	flags;
			__u64	address;
			union {
				__u32 ptid;
			} feat;
		} pagefault;

		struct {
			__u32	ufd;
		} fork;

		struct {
			__u64	from;
			__u64	to;
			__u64	len;
		} remap;

		struct {
			__u64	start;
			__u64	end;
		} remove;

		struct {
			/* 사용하지 않는 예약 필드 */
			__u64	reserved1;
			__u64	reserved2;
			__u64	reserved3;
		} reserved;
	} arg;
} __packed;

enum mfill_atomic_mode

mfill_atomic 함수에서 사용하는 작업 모드입니다.

// include/linux/userfaultfd_k.h:87-93
enum mfill_atomic_mode {
	MFILL_ATOMIC_COPY,       // 소스에서 대상으로 페이지 복사
	MFILL_ATOMIC_ZEROPAGE,   // 영(0) 페이지 매핑
	MFILL_ATOMIC_CONTINUE,   // shmem 기존 페이지 연결 (UFFDIO_CONTINUE)
	MFILL_ATOMIC_POISON,     // 오염된 페이지 마커 설치 (UFFDIO_POISON)
	NR_MFILL_ATOMIC_MODES,
};

struct uffdio_copy (UFFDIO_COPY ioctl 인자)

// include/uapi/linux/userfaultfd.h:275-294
struct uffdio_copy {
	__u64 dst;
	__u64 src;
	__u64 len;
#define UFFDIO_COPY_MODE_DONTWAKE		((__u64)1<<0)
	/*
	 * UFFDIO_COPY_MODE_WP는 페이지를 즉시 write-protected 상태로 매핑한다.
	 * 이 모드는 uffdio_register.ioctls에 따라 해당 범위에 write protect
	 * ioctl이 구현되어 있을 때만 사용할 수 있다.
	 */
#define UFFDIO_COPY_MODE_WP			((__u64)1<<1)
	__u64 mode;

	/* "copy"는 ioctl이 쓰며 마지막 8바이트는 copy_from_user가 읽지 않는다. */
	__s64 copy;
};

struct uffdio_move (UFFDIO_MOVE ioctl 인자)

// include/uapi/linux/userfaultfd.h:359-375
struct uffdio_move {
	__u64 dst;
	__u64 src;
	__u64 len;
	/*
	 * 주소 공간에서 메모리를 원자적으로 제거하는 용도로 쓰면
	 * dst 범위의 wake는 필요하지 않다.
	 */
#define UFFDIO_MOVE_MODE_DONTWAKE		((__u64)1<<0)
#define UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES	((__u64)1<<1)
	__u64 mode;
	/* "move"는 ioctl이 쓰며 마지막 8바이트는 copy_from_user가 읽지 않는다. */
	__s64 move;
};

double_pt_lock — 이중 PTL 잠금

두 개의 페이지 테이블 잠금(PTL)을 안전하게 획득하는 헬퍼 함수입니다. 가상 주소 순서로 잠금을 획득하여 lock inversion을 방지합니다.

// mm/userfaultfd.c:1000-1013
void double_pt_lock(spinlock_t *ptl1, spinlock_t *ptl2)
	__acquires(ptl1)
	__acquires(ptl2)
{
	if (ptl1 > ptl2)
		swap(ptl1, ptl2);
	/* 가상 주소 순서로 잠금을 획득하여 lock inversion 방지 */
	spin_lock(ptl1);
	if (ptl1 != ptl2)
		spin_lock_nested(ptl2, SINGLE_DEPTH_NESTING);
	else
		__acquire(ptl2);
}

핵심 함수

handle_userfault() — 페이지 폴트 진입점

페이지 폴트가 발생했을 때 커널이 호출하는 함수입니다. VMA에 등록된 userfaultfd_ctx를 찾아 waitqueue에 대기 항목을 추가하고, 사용자 핸들러가 깨우기를 기다립니다.

// fs/userfaultfd.c:381-558
vm_fault_t handle_userfault(struct vm_fault *vmf, unsigned long reason)
{
	struct vm_area_struct *vma = vmf->vma;
	struct mm_struct *mm = vma->vm_mm;
	struct userfaultfd_ctx *ctx;
	struct userfaultfd_wait_queue uwq;
	vm_fault_t ret = VM_FAULT_SIGBUS;
	bool must_wait;
	unsigned int blocking_state;

	/* 마지막 child pid 갱신과 코어덤프 중에는 userfault 처리를 하지 않는다. */
	if (current->flags & (PF_EXITING|PF_DUMPCORE))
		goto out;

	assert_fault_locked(vmf);

	ctx = vma->vm_userfaultfd_ctx.ctx;
	if (!ctx)
		goto out;

	VM_WARN_ON_ONCE(ctx->mm != mm);

	/* 알 수 없는 플래그는 버그다. */
	VM_WARN_ON_ONCE(reason & ~__VM_UFFD_FLAGS);
	/* 플래그 0개 또는 2개 이상은 버그다. 정확히 1개를 기대한다. */
	VM_WARN_ON_ONCE(!reason || (reason & (reason - 1)));

	if (ctx->features & UFFD_FEATURE_SIGBUS)
		goto out;
	if (!(vmf->flags & FAULT_FLAG_USER) && (ctx->flags & UFFD_USER_MODE_ONLY))
		goto out;

	/*
	 * VM_FAULT_RETRY를 반환할 수 있는지 확인한다.
	 *
	 * VM_UFFD_MISSING 폴트에서는 FAULT_FLAG_ALLOW_RETRY가 필요하다.
	 */
	if (unlikely(!(vmf->flags & FAULT_FLAG_ALLOW_RETRY))) {
		/*
		 * nowait 호출에서 잘못 SIGBUS를 반환하지 않도록
		 * nowait이면 retry가 허용되어야 한다는 불변식을 확인한다.
		 */
		VM_WARN_ON_ONCE(vmf->flags & FAULT_FLAG_RETRY_NOWAIT);
#ifdef CONFIG_DEBUG_VM
		if (printk_ratelimit()) {
			pr_warn("FAULT_FLAG_ALLOW_RETRY missing %x\n",
				vmf->flags);
			dump_stack();
		}
#endif
		goto out;
	}

	/* nowait이면 기다리지 말고 재시도를 요청한다. */
	ret = VM_FAULT_RETRY;
	if (vmf->flags & FAULT_FLAG_RETRY_NOWAIT)
		goto out;

	if (unlikely(READ_ONCE(ctx->released))) {
		/*
		 * 동시 release가 보이면 SIGBUS/NOPAGE 대신 VM_FAULT_RETRY를
		 * 반환하고 fault lock을 선제적으로 해제한다.
		 */
		release_fault_lock(vmf);
		goto out;
	}

	/* mmap_lock을 놓기 전에 참조를 잡는다. */
	userfaultfd_ctx_get(ctx);

	init_waitqueue_func_entry(&uwq.wq, userfaultfd_wake_function);
	uwq.wq.private = current;
	uwq.msg = userfault_msg(vmf->address, vmf->real_address, vmf->flags,
				reason, ctx->features);
	uwq.ctx = ctx;
	uwq.waken = false;

	blocking_state = userfaultfd_get_blocking_state(vmf->flags);

	/*
	 * userfaultfd_huge_must_wait()를 안전하게 호출하기 위해
	 * 여기서 vma lock을 잡는다. sleep 가능한 vma lock은 현재 task
	 * 상태를 바꿀 수 있으므로 set_current_state()보다 먼저 수행한다.
	 */
	if (is_vm_hugetlb_page(vma))
		hugetlb_vma_lock_read(vma);

	spin_lock_irq(&ctx->fault_pending_wqh.lock);
	/* __add_wait_queue 뒤에는 poll/read()를 통해 사용자 공간에 보인다. */
	__add_wait_queue(&ctx->fault_pending_wqh, &uwq.wq);
	/*
	 * __set_current_state 뒤의 smp_mb()는 spin_unlock 뒤의 읽기가
	 * __add_wait_queue의 list_add보다 먼저 실행되지 않게 한다.
	 */
	set_current_state(blocking_state);
	spin_unlock_irq(&ctx->fault_pending_wqh.lock);

	if (is_vm_hugetlb_page(vma)) {
		must_wait = userfaultfd_huge_must_wait(ctx, vmf, reason);
		hugetlb_vma_unlock_read(vma);
	} else {
		must_wait = userfaultfd_must_wait(ctx, vmf, reason);
	}

	release_fault_lock(vmf);

	if (likely(must_wait && !READ_ONCE(ctx->released))) {
		wake_up_poll(&ctx->fd_wqh, EPOLLIN);
		schedule();
	}

	__set_current_state(TASK_RUNNING);

	/*
	 * userfaultfd_ctx_read()가 두 리스트 사이에서 refile하는 동안에도
	 * list_empty_careful()이 stack의 uwq를 안전하게 확인할 수 있다.
	 */
	if (!list_empty_careful(&uwq.wq.entry)) {
		spin_lock_irq(&ctx->fault_pending_wqh.lock);
		/* stack의 uwq는 곧 사라지므로 list_del_init()은 필요 없다. */
		list_del(&uwq.wq.entry);
		spin_unlock_irq(&ctx->fault_pending_wqh.lock);
	}

	/* pseudo fd가 이미 해제되었다면 이 뒤로 ctx가 사라질 수 있다. */
	userfaultfd_ctx_put(ctx);
out:
	return ret;
}

역할: 페이지 폴트를 사용자 공간으로 위임

분기 로직:

1. PF_EXITING | PF_DUMPCORE → 종료/코어덤프 중이면 즉시 VM_FAULT_SIGBUS 반환

2. ctx == NULL → userfaultfd 미등록 VMA → VM_FAULT_SIGBUS

3. UFFD_FEATURE_SIGBUS 설정됨 → 일반 SIGBUS 경로로 복귀

4. !FAULT_FLAG_USER && UFFD_USER_MODE_ONLY → 커널 모드 폴트 무시

5. !FAULT_FLAG_ALLOW_RETRY → 재시도 불가 → VM_FAULT_SIGBUS

6. FAULT_FLAG_RETRY_NOWAIT → 즉시 VM_FAULT_RETRY 반환

7. ctx->released → userfaultfd 해제 중 → VM_FAULT_RETRY

8. 정상 → fault_pending_wqh에 추가 → schedule() → 깨어남

mfill_atomic() — 메모리 채움 핵심 루프

UFFDIO_COPY, UFFDIO_ZEROPAGE, UFFDIO_CONTINUE, UFFDIO_POISON의 공통 처리 함수입니다.

// mm/userfaultfd.c:704-866
static __always_inline ssize_t mfill_atomic(struct userfaultfd_ctx *ctx,
					    unsigned long dst_start,
					    unsigned long src_start,
					    unsigned long len,
					    uffd_flags_t flags)
{
	struct mm_struct *dst_mm = ctx->mm;
	struct vm_area_struct *dst_vma;
	ssize_t err;
	pmd_t *dst_pmd;
	unsigned long src_addr, dst_addr;
	long copied;
	struct folio *folio;

	/* 명령 인자를 정규화한다. */
	VM_WARN_ON_ONCE(dst_start & ~PAGE_MASK);
	VM_WARN_ON_ONCE(len & ~PAGE_MASK);

	/* 주소 범위가 wrap되거나 길이가 0인지 확인한다. */
	VM_WARN_ON_ONCE(src_start + len <= src_start);
	VM_WARN_ON_ONCE(dst_start + len <= dst_start);

	src_addr = src_start;
	dst_addr = dst_start;
	copied = 0;
	folio = NULL;
retry:
	/*
	 * dst 범위가 하나의 기존 VMA 안에 완전히 들어가고,
	 * 해당 VMA가 유효한지 확인한다.
	 */
	dst_vma = uffd_mfill_lock(dst_mm, dst_start, len);
	if (IS_ERR(dst_vma)) {
		err = PTR_ERR(dst_vma);
		goto out;
	}

	/*
	 * mremap 같은 비협력 매핑 변경이 병렬로 진행 중이면
	 * 사용자가 나중에 다시 시도하도록 -EAGAIN을 반환한다.
	 */
	down_read(&ctx->map_changing_lock);
	err = -EAGAIN;
	if (atomic_read(&ctx->mmap_changing))
		goto out_unlock;

	err = -EINVAL;
	/*
	 * MAP_ANONYMOUS|MAP_SHARED mmap에서는 shmem_zero_setup이 vm_ops를
	 * 덮어쓰므로 vma_is_anonymous는 false를 반환해야 한다.
	 */
	if (WARN_ON_ONCE(vma_is_anonymous(dst_vma) &&
	    dst_vma->vm_flags & VM_SHARED))
		goto out_unlock;

	/*
	 * dst_vma를 확인한 뒤 mode를 검증한다. WP로 등록되지 않은
	 * userfaultfd에는 wrprotect copy를 허용하지 않는다.
	 */
	if ((flags & MFILL_ATOMIC_WP) && !(dst_vma->vm_flags & VM_UFFD_WP))
		goto out_unlock;

	/* HUGETLB VMA이면 전용 루틴으로 넘긴다. */
	if (is_vm_hugetlb_page(dst_vma))
		return  mfill_atomic_hugetlb(ctx, dst_vma, dst_start,
					     src_start, len, flags);

	if (!vma_is_anonymous(dst_vma) && !vma_is_shmem(dst_vma))
		goto out_unlock;
	if (!vma_is_shmem(dst_vma) &&
	    uffd_flags_mode_is(flags, MFILL_ATOMIC_CONTINUE))
		goto out_unlock;

	while (src_addr < src_start + len) {
		pmd_t dst_pmdval;

		VM_WARN_ON_ONCE(dst_addr >= dst_start + len);

		dst_pmd = mm_alloc_pmd(dst_mm, dst_addr);
		if (unlikely(!dst_pmd)) {
			err = -ENOMEM;
			break;
		}

		dst_pmdval = pmdp_get_lockless(dst_pmd);
		if (unlikely(pmd_none(dst_pmdval)) &&
		    unlikely(__pte_alloc(dst_mm, dst_pmd))) {
			err = -ENOMEM;
			break;
		}
		dst_pmdval = pmdp_get_lockless(dst_pmd);
		/*
		 * dst_pmd가 THP이면 덮어쓰지 않고 엄격하게 실패한다.
		 * __pte_alloc() 뒤에 PMD가 THP에서 none으로 바뀐 경우도 포함한다.
		 */
		if (unlikely(!pmd_present(dst_pmdval) ||
				pmd_trans_huge(dst_pmdval))) {
			err = -EEXIST;
			break;
		}
		if (unlikely(pmd_bad(dst_pmdval))) {
			err = -EFAULT;
			break;
		}
		/*
		 * shmem 매핑에서는 khugepaged가 아래에서 page table을 제거할 수 있고,
		 * pte_offset_map_lock()이 그 상황을 처리한다.
		 */

		err = mfill_atomic_pte(dst_pmd, dst_vma, dst_addr,
				       src_addr, flags, &folio);
		cond_resched();

		if (unlikely(err == -ENOENT)) {
			void *kaddr;

			up_read(&ctx->map_changing_lock);
			uffd_mfill_unlock(dst_vma);
			VM_WARN_ON_ONCE(!folio);

			kaddr = kmap_local_folio(folio, 0);
			err = copy_from_user(kaddr,
					     (const void __user *) src_addr,
					     PAGE_SIZE);
			kunmap_local(kaddr);
			if (unlikely(err)) {
				err = -EFAULT;
				goto out;
			}
			flush_dcache_folio(folio);
			goto retry;
		} else
			VM_WARN_ON_ONCE(folio);

		if (!err) {
			dst_addr += PAGE_SIZE;
			src_addr += PAGE_SIZE;
			copied += PAGE_SIZE;

			if (fatal_signal_pending(current))
				err = -EINTR;
		}
		if (err)
			break;
	}

out_unlock:
	up_read(&ctx->map_changing_lock);
	uffd_mfill_unlock(dst_vma);
out:
	if (folio)
		folio_put(folio);
	VM_WARN_ON_ONCE(copied < 0);
	VM_WARN_ON_ONCE(err > 0);
	VM_WARN_ON_ONCE(!copied && !err);
	return copied ? copied : err;
}

역할: 지정된 주소 범위의 페이지 테이블에 새 PTE를 설치

분기 로직:

1. mmap_changing 확인 → mremap 등 병렬 변경 시 -EAGAIN

2. is_vm_hugetlb_page(dst_vma)mfill_atomic_hugetlb() 경로로 분기

3. !vma_is_anonymous && !vma_is_shmem → 지원 불가 -EINVAL

4. MFILL_ATOMIC_CONTINUE && !vma_is_shmem → shmem만 가능 -EINVAL

5. 페이지별 PMD 할당 → mfill_atomic_pte() 호출

6. -ENOENT 반환 → mmap_lock 해제 후 재시도 (folio를 사용자 공간에서 복사)

mfill_atomic_install_pte() — PTE 설치

실제 페이지 테이블 항목을 설치하는 최종 단계 함수입니다.

// mm/userfaultfd.c:168-239
int mfill_atomic_install_pte(pmd_t *dst_pmd,
			     struct vm_area_struct *dst_vma,
			     unsigned long dst_addr, struct page *page,
			     bool newly_allocated, uffd_flags_t flags)
{
	int ret;
	struct mm_struct *dst_mm = dst_vma->vm_mm;
	pte_t _dst_pte, *dst_pte;
	bool writable = dst_vma->vm_flags & VM_WRITE;
	bool vm_shared = dst_vma->vm_flags & VM_SHARED;
	spinlock_t *ptl;
	struct folio *folio = page_folio(page);
	bool page_in_cache = folio_mapping(folio);
	pte_t dst_ptep;

	_dst_pte = mk_pte(page, dst_vma->vm_page_prot);
	_dst_pte = pte_mkdirty(_dst_pte);
	if (page_in_cache && !vm_shared)
		writable = false;
	if (writable)
		_dst_pte = pte_mkwrite(_dst_pte, dst_vma);
	if (flags & MFILL_ATOMIC_WP)
		_dst_pte = pte_mkuffd_wp(_dst_pte);

	ret = -EAGAIN;
	dst_pte = pte_offset_map_lock(dst_mm, dst_pmd, dst_addr, &ptl);
	if (!dst_pte)
		goto out;

	if (mfill_file_over_size(dst_vma, dst_addr)) {
		ret = -EFAULT;
		goto out_unlock;
	}

	ret = -EEXIST;

	dst_ptep = ptep_get(dst_pte);

	/*
	 * UFFD PTE marker는 덮어쓸 수 있다. MISSING|WP가 함께 등록된 뒤
	 * 페이지 캐시 page가 없는 none PTE를 먼저 wr-protect하고 접근하는
	 * 경우를 고려한다.
	 */
	if (!pte_none(dst_ptep) && !pte_is_uffd_marker(dst_ptep))
		goto out_unlock;

	if (page_in_cache) {
		/* 보통 cache page는 이미 LRU에 추가되어 있다. */
		if (newly_allocated)
			folio_add_lru(folio);
		folio_add_file_rmap_pte(folio, page, dst_vma);
	} else {
		folio_add_new_anon_rmap(folio, dst_vma, dst_addr, RMAP_EXCLUSIVE);
		folio_add_lru_vma(folio, dst_vma);
	}

	/*
	 * mm_counter()가 mapping을 확인하므로 rmap 뒤에 실행되어야 한다.
	 * mapping은 __page_set_anon_rmap()에서 설정된다.
	 */
	inc_mm_counter(dst_mm, mm_counter(folio));

	set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte);

	/* 이전에는 non-present였으므로 invalidate가 필요 없다. */
	update_mmu_cache(dst_vma, dst_addr, dst_pte);
	ret = 0;
out_unlock:
	pte_unmap_unlock(dst_pte, ptl);
out:
	return ret;
}

역할: PTE를 구성하고 페이지 테이블에 기록

분기 로직:

1. mfill_file_over_size() → 파일 범위 초과 -EFAULT

2. !pte_none(dst_ptep) && !pte_is_uffd_marker() → 이미 매핑됨 -EEXIST

3. page_in_cache → 파일 기반 페이지 → folio_add_file_rmap_pte()

4. !page_in_cache → 익명 페이지 → folio_add_new_anon_rmap()

5. MFILL_ATOMIC_WP 플래그 → pte_mkuffd_wp()로 쓰기 보호 PTE 설치

mwriteprotect_range() — 쓰기 보호 범위 설정

UFFDIO_WRITEPROTECT ioctl로 지정된 주소 범위의 PTE를 쓰기 보호하거나 해제합니다.

// mm/userfaultfd.c:936-997
int mwriteprotect_range(struct userfaultfd_ctx *ctx, unsigned long start,
			unsigned long len, bool enable_wp)
{
	struct mm_struct *dst_mm = ctx->mm;
	unsigned long end = start + len;
	unsigned long _start, _end;
	struct vm_area_struct *dst_vma;
	unsigned long page_mask;
	long err;
	VMA_ITERATOR(vmi, dst_mm, start);

	/* 명령 인자를 정규화한다. */
	VM_WARN_ON_ONCE(start & ~PAGE_MASK);
	VM_WARN_ON_ONCE(len & ~PAGE_MASK);

	/* 주소 범위가 wrap되거나 길이가 0인지 확인한다. */
	VM_WARN_ON_ONCE(start + len <= start);

	mmap_read_lock(dst_mm);

	/*
	 * mremap 같은 비협력 매핑 변경이 병렬로 진행 중이면
	 * 사용자가 나중에 다시 시도하도록 -EAGAIN을 반환한다.
	 */
	down_read(&ctx->map_changing_lock);
	err = -EAGAIN;
	if (atomic_read(&ctx->mmap_changing))
		goto out_unlock;

	err = -ENOENT;
	for_each_vma_range(vmi, dst_vma, end) {

		if (!userfaultfd_wp(dst_vma)) {
			err = -ENOENT;
			break;
		}

		if (is_vm_hugetlb_page(dst_vma)) {
			err = -EINVAL;
			page_mask = vma_kernel_pagesize(dst_vma) - 1;
			if ((start & page_mask) || (len & page_mask))
				break;
		}

		_start = max(dst_vma->vm_start, start);
		_end = min(dst_vma->vm_end, end);

		err = uffd_wp_range(dst_vma, _start, _end - _start, enable_wp);

		/* 성공이면 0, 실패하면 음수 값을 반환한다. */
		if (err < 0)
			break;
		err = 0;
	}
out_unlock:
	up_read(&ctx->map_changing_lock);
	mmap_read_unlock(dst_mm);
	return err;
}

역할: 페이지 테이블의 쓰기 권한 일괄 변경

분기 로직:

1. mmap_changing 확인 → -EAGAIN

2. for_each_vma_range()로 VMA 순회

3. !userfaultfd_wp(dst_vma) → WP 미등록 VMA -ENOENT

4. is_vm_hugetlb_page() → huge page 정렬 검증

5. change_protection() 호출 → 실제 PTE 변경

move_pages() — 페이지 이동 핵심 함수

UFFDIO_MOVE ioctl로 익명 페이지를 한 주소에서 다른 주소로 제로카피 이동합니다.

// mm/userfaultfd.c:1766-1939
ssize_t move_pages(struct userfaultfd_ctx *ctx, unsigned long dst_start,
		   unsigned long src_start, unsigned long len, __u64 mode)
{
	struct mm_struct *mm = ctx->mm;
	struct vm_area_struct *src_vma, *dst_vma;
	unsigned long src_addr, dst_addr, src_end;
	pmd_t *src_pmd, *dst_pmd;
	long err = -EINVAL;
	ssize_t moved = 0;

	/* 명령 인자를 정규화한다. */
	VM_WARN_ON_ONCE(src_start & ~PAGE_MASK);
	VM_WARN_ON_ONCE(dst_start & ~PAGE_MASK);
	VM_WARN_ON_ONCE(len & ~PAGE_MASK);

	/* 주소 범위가 wrap되거나 길이가 0인지 확인한다. */
	VM_WARN_ON_ONCE(src_start + len < src_start);
	VM_WARN_ON_ONCE(dst_start + len < dst_start);

	err = uffd_move_lock(mm, dst_start, src_start, &dst_vma, &src_vma);
	if (err)
		goto out;

	/* map_changing_lock을 잡은 뒤 다시 확인한다. */
	err = -EAGAIN;
	down_read(&ctx->map_changing_lock);
	if (likely(atomic_read(&ctx->mmap_changing)))
		goto out_unlock;
	/*
	 * src/dst remap 범위가 공유 VMA가 아니며 각각 하나의 기존 VMA 안에
	 * 완전히 들어가는지 확인한다.
	 */
	err = -EINVAL;
	if (src_vma->vm_flags & VM_SHARED)
		goto out_unlock;
	if (src_start + len > src_vma->vm_end)
		goto out_unlock;

	if (dst_vma->vm_flags & VM_SHARED)
		goto out_unlock;
	if (dst_start + len > dst_vma->vm_end)
		goto out_unlock;

	err = validate_move_areas(ctx, src_vma, dst_vma);
	if (err)
		goto out_unlock;

	for (src_addr = src_start, dst_addr = dst_start, src_end = src_start + len;
	     src_addr < src_end;) {
		spinlock_t *ptl;
		pmd_t dst_pmdval;
		unsigned long step_size;

		/*
		 * 익명 영역에는 transparent huge PUD가 없기 때문에 아래 로직이
		 * 동작한다. file-backed 지원이 추가되면 그 경우도 처리해야 한다.
		 */
		src_pmd = mm_find_pmd(mm, src_addr);
		if (unlikely(!src_pmd)) {
			if (!(mode & UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES)) {
				err = -ENOENT;
				break;
			}
			src_pmd = mm_alloc_pmd(mm, src_addr);
			if (unlikely(!src_pmd)) {
				err = -ENOMEM;
				break;
			}
		}
		dst_pmd = mm_alloc_pmd(mm, dst_addr);
		if (unlikely(!dst_pmd)) {
			err = -ENOMEM;
			break;
		}

		dst_pmdval = pmdp_get_lockless(dst_pmd);
		/*
		 * dst_pmd가 THP로 매핑되어 있으면 덮어쓰지 않고 엄격하게 실패한다.
		 * 이 확인 뒤 dst_pmd가 THP로 바뀌면 huge/pte 경로가 각각 재시도나
		 * 실패로 감지한다.
		 */
		if (unlikely(pmd_trans_huge(dst_pmdval))) {
			err = -EEXIST;
			break;
		}

		ptl = pmd_trans_huge_lock(src_pmd, src_vma);
		if (ptl) {
			/* PMD를 split하지 않고 옮길 수 있는지 확인한다. */
			if (move_splits_huge_pmd(dst_addr, src_addr, src_start + len) ||
			    !pmd_none(dst_pmdval)) {
				/* migration entry일 수 있다. */
				if (pmd_present(*src_pmd)) {
					struct folio *folio = pmd_folio(*src_pmd);

					if (!is_huge_zero_folio(folio) &&
					    !PageAnonExclusive(&folio->page)) {
						spin_unlock(ptl);
						err = -EBUSY;
						break;
					}
				}

				spin_unlock(ptl);
				split_huge_pmd(src_vma, src_pmd, src_addr);
				/* folio는 move_pages_pte()에서 split된다. */
				continue;
			}

			err = move_pages_huge_pmd(mm, dst_pmd, src_pmd,
						  dst_pmdval, dst_vma, src_vma,
						  dst_addr, src_addr);
			step_size = HPAGE_PMD_SIZE;
		} else {
			long ret;

			if (pmd_none(*src_pmd)) {
				if (!(mode & UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES)) {
					err = -ENOENT;
					break;
				}
				if (unlikely(__pte_alloc(mm, src_pmd))) {
					err = -ENOMEM;
					break;
				}
			}

			if (unlikely(pte_alloc(mm, dst_pmd))) {
				err = -ENOMEM;
				break;
			}

			ret = move_pages_ptes(mm, dst_pmd, src_pmd,
					      dst_vma, src_vma, dst_addr,
					      src_addr, src_end - src_addr, mode);
			if (ret < 0)
				err = ret;
			else
				step_size = ret;
		}

		cond_resched();

		if (fatal_signal_pending(current)) {
			/* 기존 에러를 덮어쓰지 않는다. */
			if (!err || err == -EAGAIN)
				err = -EINTR;
			break;
		}

		if (err) {
			if (err == -EAGAIN)
				continue;
			break;
		}

		/* 다음 페이지로 진행한다. */
		dst_addr += step_size;
		src_addr += step_size;
		moved += step_size;
	}

out_unlock:
	up_read(&ctx->map_changing_lock);
	uffd_move_unlock(dst_vma, src_vma);
out:
	VM_WARN_ON_ONCE(moved < 0);
	VM_WARN_ON_ONCE(err > 0);
	VM_WARN_ON_ONCE(!moved && !err);
	return moved ? moved : err;
}

역할: 익명 페이지를 제로카피로 이동 (VMA 생성 없이)

전제 조건: src_vma와 dst_vma 모두 익명, 쓰기 가능, 동일 접근 권한

이 경로는 reverse mapping과 DMA pin 상태에 민감하다.

move_pages_ptes() — PTE 수준 페이지 이동

일반 PTE 단위로 페이지를 이동하는 핵심 함수입니다.

// mm/userfaultfd.c:1247-1327
static long move_pages_ptes(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
			    struct vm_area_struct *dst_vma,
			    struct vm_area_struct *src_vma,
			    unsigned long dst_addr, unsigned long src_addr,
			    unsigned long len, __u64 mode)
{
	struct swap_info_struct *si = NULL;
	pte_t orig_src_pte, orig_dst_pte;
	pte_t src_folio_pte;
	spinlock_t *src_ptl, *dst_ptl;
	pte_t *src_pte = NULL;
	pte_t *dst_pte = NULL;
	pmd_t dummy_pmdval;
	pmd_t dst_pmdval;
	struct folio *src_folio = NULL;
	struct mmu_notifier_range range;
	long ret = 0;

	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
				src_addr, src_addr + len);
	mmu_notifier_invalidate_range_start(&range);
retry:
	/*
	 * dst_pte가 수정될 것이므로 maywrite 버전을 사용한다. dst_pte는 none이어야
	 * 하고 뒤의 pte_same()만으로는 dst_pte page가 병렬로 해제되는 것을 막을 수
	 * 없으므로 dst_pmdval도 얻어 나중에 pmd_same()으로 다시 확인한다.
	 */
	dst_pte = pte_offset_map_rw_nolock(mm, dst_pmd, dst_addr, &dst_pmdval,
					   &dst_ptl);

	/* 아래에서 huge pmd가 생기면 재시도한다. */
	if (unlikely(!dst_pte)) {
		ret = -EAGAIN;
		goto out;
	}

	/*
	 * dst_pte와 달리 뒤의 pte_same()으로 src_pte page 안정성을 확인할 수
	 * 있으므로 pmdval을 얻을 필요가 없고 dummy 변수만 넘긴다.
	 */
	src_pte = pte_offset_map_rw_nolock(mm, src_pmd, src_addr, &dummy_pmdval,
					   &src_ptl);

	/*
	 * read mmap_lock을 잡고 있으므로 MADV_DONTNEED가 transparent huge page를
	 * zap하거나 transparent huge page fault가 새 THP를 만들 수 있다.
	 */
	if (unlikely(!src_pte)) {
		ret = -EAGAIN;
		goto out;
	}

	/* 작업 전 기본 검사를 수행한다. */
	if (pmd_none(*dst_pmd) || pmd_none(*src_pmd) ||
	    pmd_trans_huge(*dst_pmd) || pmd_trans_huge(*src_pmd)) {
		ret = -EINVAL;
		goto out;
	}

	spin_lock(dst_ptl);
	orig_dst_pte = ptep_get(dst_pte);
	spin_unlock(dst_ptl);
	if (!pte_none(orig_dst_pte)) {
		ret = -EEXIST;
		goto out;
	}

	spin_lock(src_ptl);
	orig_src_pte = ptep_get(src_pte);
	spin_unlock(src_ptl);
	if (pte_none(orig_src_pte)) {
		if (!(mode & UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES))
			ret = -ENOENT;
		else /* hole은 옮길 내용이 없다. */
			ret = PAGE_SIZE;
		goto out;
	}
// mm/userfaultfd.c:1335-1489
if (pte_present(orig_src_pte)) {
	if (is_zero_pfn(pte_pfn(orig_src_pte))) {
		ret = move_zeropage_pte(mm, dst_vma, src_vma,
				       dst_addr, src_addr, dst_pte, src_pte,
				       orig_dst_pte, orig_src_pte,
				       dst_pmd, dst_pmdval, dst_ptl, src_ptl);
		goto out;
	}

	/*
	 * source folio를 pin하고 lock한다. RCU read section 안에서는 block할 수
	 * 없으므로 경합이 있으면 pte mapping을 해제하고 lock을 얻은 뒤 재시도한다.
	 */
	if (!src_folio) {
		struct folio *folio;
		bool locked;

		/* lock을 잡은 상태에서 page를 pin하여 아래에서 해제되지 않게 한다. */
		spin_lock(src_ptl);
		if (!pte_same(orig_src_pte, ptep_get(src_pte))) {
			spin_unlock(src_ptl);
			ret = -EAGAIN;
			goto out;
		}

		folio = vm_normal_folio(src_vma, src_addr, orig_src_pte);
		if (!folio || !PageAnonExclusive(&folio->page)) {
			spin_unlock(src_ptl);
			ret = -EBUSY;
			goto out;
		}

		locked = folio_trylock(folio);
		/*
		 * large folio는 refcount가 증가하면 split_folio()가 나중에 실패하고
		 * 재시도할 수 있으므로 folio lock 대기를 피한다.
		 */
		if (!locked && folio_test_large(folio)) {
			spin_unlock(src_ptl);
			ret = -EAGAIN;
			goto out;
		}

		folio_get(folio);
		src_folio = folio;
		src_folio_pte = orig_src_pte;
		spin_unlock(src_ptl);

		if (!locked) {
			pte_unmap(src_pte);
			pte_unmap(dst_pte);
			src_pte = dst_pte = NULL;
			/* 이제 block하고 기다릴 수 있다. */
			folio_lock(src_folio);
			goto retry;
		}

		if (WARN_ON_ONCE(!folio_test_anon(src_folio))) {
			ret = -EBUSY;
			goto out;
		}
	}

	/* 여기까지 오면 src_folio lock을 잡은 상태다. */
	if (folio_test_large(src_folio)) {
		/* split_folio()는 block할 수 있다. */
		pte_unmap(src_pte);
		pte_unmap(dst_pte);
		src_pte = dst_pte = NULL;
		ret = split_folio(src_folio);
		if (ret)
			goto out;
		/* split 뒤에는 folio를 다시 얻어야 한다. */
		folio_unlock(src_folio);
		folio_put(src_folio);
		src_folio = NULL;
		goto retry;
	}

	ret = move_present_ptes(mm, dst_vma, src_vma,
				dst_addr, src_addr, dst_pte, src_pte,
				orig_dst_pte, orig_src_pte, dst_pmd,
				dst_pmdval, dst_ptl, src_ptl, &src_folio,
				len);
} else { /* !pte_present() 경로 */
	struct folio *folio = NULL;
	const softleaf_t entry = softleaf_from_pte(orig_src_pte);

	if (softleaf_is_migration(entry)) {
		pte_unmap(src_pte);
		pte_unmap(dst_pte);
		src_pte = dst_pte = NULL;
		migration_entry_wait(mm, src_pmd, src_addr);

		ret = -EAGAIN;
		goto out;
	} else if (!softleaf_is_swap(entry)) {
		ret = -EFAULT;
		goto out;
	}

	if (!pte_swp_exclusive(orig_src_pte)) {
		ret = -EBUSY;
		goto out;
	}

	si = get_swap_device(entry);
	if (unlikely(!si)) {
		ret = -EAGAIN;
		goto out;
	}
	/*
	 * swapcache 존재 여부를 확인한다. 있으면 PTE가 swap entry여도 folio의
	 * index와 mapping을 갱신해야 한다. swap entry가 exclusive이므로 이 과정에서
	 * anon_vma lock은 잡지 않는다.
	 */
	if (!src_folio)
		folio = swap_cache_get_folio(entry);
	if (folio) {
		if (folio_test_large(folio)) {
			ret = -EBUSY;
			folio_put(folio);
			goto out;
		}
		src_folio = folio;
		src_folio_pte = orig_src_pte;
		if (!folio_trylock(src_folio)) {
			pte_unmap(src_pte);
			pte_unmap(dst_pte);
			src_pte = dst_pte = NULL;
			put_swap_device(si);
			si = NULL;
			/* 이제 block하고 기다릴 수 있다. */
			folio_lock(src_folio);
			goto retry;
		}
	}
	ret = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
			orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
			dst_ptl, src_ptl, src_folio, si, entry);
}

역할: 소스 PTE를 대상 PTE로 이동 (제로카피)

분기 로직:

1. pte_none(orig_src_pte)UFFDIO_MOVE_MODE_ALLOW_SRC_HOLES에 따라 -ENOENT 또는 PAGE_SIZE 반환

2. is_zero_pfn(pte_pfn(orig_src_pte))move_zeropage_pte() 호출

3. pte_present(orig_src_pte) → folio 잠금 → move_present_ptes() 호출

4. softleaf_is_migration(entry)migration_entry_wait()-EAGAIN

5. softleaf_is_swap(entry)move_swap_pte() 호출

check_ptes_for_batched_move() — 배치 이동 검증

연속된 PTE가 배치 이동 가능한지 검증하는 함수입니다.

// mm/userfaultfd.c:1043-1066
static struct folio *check_ptes_for_batched_move(struct vm_area_struct *src_vma,
						 unsigned long src_addr,
						 pte_t *src_pte, pte_t *dst_pte)
{
	pte_t orig_dst_pte, orig_src_pte;
	struct folio *folio;

	orig_dst_pte = ptep_get(dst_pte);
	if (!pte_none(orig_dst_pte))
		return NULL;

	orig_src_pte = ptep_get(src_pte);
	if (!pte_present(orig_src_pte) || is_zero_pfn(pte_pfn(orig_src_pte)))
		return NULL;

	folio = vm_normal_folio(src_vma, src_addr, orig_src_pte);
	if (!folio || !folio_trylock(folio))
		return NULL;
	if (!PageAnonExclusive(&folio->page) || folio_test_large(folio)) {
		folio_unlock(folio);
		return NULL;
	}
	return folio;
}

역할: 배치 이동 가능 여부 판단 (연속 PTE를 한번에 이동)

반환: 이동 가능한 folio 포인터 또는 NULL

validate_move_areas() — 이동 영역 검증

move_pages() 호출 시 소스/대상 VMA의 유효성을 검증합니다.

// mm/userfaultfd.c:1536-1570
static int validate_move_areas(struct userfaultfd_ctx *ctx,
			       struct vm_area_struct *src_vma,
			       struct vm_area_struct *dst_vma)
{
	/* 두 VMA의 접근 권한과 protection이 같을 때만 이동을 허용한다. */
	if ((src_vma->vm_flags & VM_ACCESS_FLAGS) != (dst_vma->vm_flags & VM_ACCESS_FLAGS) ||
	    pgprot_val(src_vma->vm_page_prot) != pgprot_val(dst_vma->vm_page_prot))
		return -EINVAL;

	/* 두 VMA가 모두 mlocked이거나 모두 mlocked가 아닐 때만 허용한다. */
	if ((src_vma->vm_flags & VM_LOCKED) != (dst_vma->vm_flags & VM_LOCKED))
		return -EINVAL;

	/*
	 * 지금은 단순하게 writable VMA 사이의 이동만 허용한다.
	 * 접근 플래그가 같으므로 source만 확인해도 충분하다.
	 */
	if (!(src_vma->vm_flags & VM_WRITE))
		return -EINVAL;

	/* VMA 플래그가 이동 가능한 내용을 나타내는지 확인한다. */
	if (!vma_move_compatible(src_vma) || !vma_move_compatible(dst_vma))
		return -EINVAL;

	/* dst_vma가 현재 조작 중인 uffd에 등록되어 있는지 보장한다. */
	if (!dst_vma->vm_userfaultfd_ctx.ctx ||
	    dst_vma->vm_userfaultfd_ctx.ctx != ctx)
		return -EINVAL;

	/* 익명 VMA 사이의 이동만 허용한다. */
	if (!vma_is_anonymous(src_vma) || !vma_is_anonymous(dst_vma))
		return -EINVAL;

	return 0;
}

역할: 이동 전 VMA 호환성 검증

반환: 성공 0, 실패 -EINVAL


호출 흐름

페이지 폴트 처리 흐름

프로세스 스레드
  │ 가상 메모리 접근
  ▼
handle_userfault(vmf, reason)          ← fs/userfaultfd.c:381
  │ ctx = vma->vm_userfaultfd_ctx.ctx
  │ userfaultfd_ctx_get(ctx)
  │ __add_wait_queue(fault_pending_wqh)
  │ set_current_state(TASK_INTERRUPTIBLE)
  ▼
userfaultfd_must_wait() / userfaultfd_huge_must_wait()
  │ 조건 재확인 (PTE가 여전히 none인지 등)
  ▼
release_fault_lock(vmf)
  │
  ├─ must_wait && !released → wake_up_poll(fd_wqh) + schedule()
  │                                │
  │                                ▼
  │                         (사용자 핸들러가 read()로 수신)
  │                                │
  │                                ▼
  │                         userfaultfd_ctx_read()        ← fs/userfaultfd.c:992
  │                           │ fault_pending_wqh에서 fault_wqh로 refile
  │                           │ uffd_msg 반환
  │                                │
  │                                ▼
  │                         사용자 ioctl(UFFDIO_COPY 등)
  │                                │
  │                                ▼
  │                         mfill_atomic_copy()            ← mm/userfaultfd.c:868
  │                           │ mfill_atomic()
  │                           │   ├─ uffd_mfill_lock()
  │                           │   ├─ mm_alloc_pmd() + __pte_alloc()
  │                           │   ├─ mfill_atomic_pte()
  │                           │   │   ├─ mfill_atomic_pte_copy()
  │                           │   │   ├─ mfill_atomic_pte_zeropage()
  │                           │   │   ├─ mfill_atomic_pte_continue()
  │                           │   │   └─ mfill_atomic_pte_poison()
  │                           │   └─ mfill_atomic_install_pte()
  │                           │       └─ set_pte_at()
  │
  └─ !must_wait → VM_FAULT_RETRY

move_pages() — 페이지 이동 핵심 함수 흐름

userfaultfd_move()                     ← fs/userfaultfd.c
  ▼
move_pages(ctx, dst_start, src_start, len, flags)  ← mm/userfaultfd.c:1766
  │ uffd_move_lock() → VMA 2개 잠금
  │ validate_move_areas() → 접근 권한/보호 비트 검증
  ▼
move_pages_ptes()                      ← mm/userfaultfd.c:1247
  │ pte_offset_map_rw_nolock(dst/src)
  │
  ├─ pte_present(orig_src_pte)
  │   ├─ is_zero_pfn → move_zeropage_pte()
  │   └─ folio lock → move_present_ptes()
  │       │ 배치 이동 (연속 PTE를 한번에 이동)
  │       │ check_ptes_for_batched_move() → 배치 가능 여부 확인
  │       │ double_pt_lock() → 이중 PTL 잠금
  │       │ ptep_get_and_clear → folio_move_anon_rmap → set_pte_at
  │       │ double_pt_unlock()
  │
  └─ !pte_present (swap entry)
      └─ move_swap_pte()
          │ swap cache 확인 → swap entry 이동
          │ double_pt_lock → ptep_get_and_clear → set_pte_at → double_pt_unlock

userfaultfd_register_range() — 범위 등록 흐름

userfaultfd_register()                 ← fs/userfaultfd.c
  ▼
userfaultfd_register_range()           ← mm/userfaultfd.c:2007
  │ for_each_vma_range(vmi, vma, end)
  │   ├─ 이미 등록됨 (ctx == ctx && flags == flags) → skip
  │   ├─ vma_modify_flags_uffd() → VMA 분할/병합
  │   └─ userfaultfd_set_ctx() → vm_userfaultfd_ctx 설정
  │       └─ userfaultfd_set_vm_flags() → VM_UFFD_* 플래그 업데이트
  │           └─ (공유 매핑 시) vma_set_page_prot() → writenotify 활성화

조건별 비교

Userfaultfd 등록 모드 비교

모드VM 플래그폴트 시 동작주요 사용처
`MISSING``VM_UFFD_MISSING`PTE가 none인 영역에서 폴트 발생 시 사용자에게 알림VMM (KVM/QEMU), CR/CRIU
`WP``VM_UFFD_WP`쓰기 시 PTE WP 비트로 쓰기 보호 폴트 발생KSM, 메모리 정책, live migration
`MINOR``VM_UFFD_MINOR`shmem/hugetlb의 기존 페이지 매핑 시 미니어 폴트파일 매핑 기반 VMM

mfill_atomic 모드 비교

모드ioctlsrc_addr 사용대상 VMA설명
`COPY``UFFDIO_COPY`O (사용자 소스)anonymous, shmem소스에서 대상으로 페이지 복사
`ZEROPAGE``UFFDIO_ZEROPAGE`Xanonymous영(0) 페이지 매핑
`CONTINUE``UFFDIO_CONTINUE`Xshmem only기존 shmem 페이지를 PTE에 연결
`POISON``UFFDIO_POISON`Xanonymous, shmemPTE 마커를 POISONED로 설정

lock 전략 비교 (PER_VMA_LOCK)

상황mmap_lockvma lock설명
`CONFIG_PER_VMA_LOCK` ON`mmap_read_lock` 최소화`lock_vma_under_rcu()` 시도RCU 기반 락으로 성능 향상
PER_VMA_LOCK OFF 또는 실패`mmap_read_lock` 사용VMA 시작 시 읽기 잠금기존 방식
`uffd_move_lock()`dst/src VMA 잠금`vma_start_read_locked()`이중 잠금으로 교차 접근 방지

UFFDIO_MOVE 처리 경로 비교

소스 PTE 상태플래그처리 함수반환값
`pte_none``ALLOW_SRC_HOLES` 없음`-ENOENT`에러
`pte_none``ALLOW_SRC_HOLES` 있음`PAGE_SIZE` (건너뜀)성공
`is_zero_pfn`-`move_zeropage_pte()``PAGE_SIZE`
`pte_present` (일반)-`move_present_ptes()`이동된 바이트
`pte_present` (large folio)-`split_folio()` → 재시도`-EAGAIN`
swap + migration-`migration_entry_wait()` → 재시도`-EAGAIN`
swap + exclusive-`move_swap_pte()``PAGE_SIZE`
swap + non-exclusive-`-EBUSY`에러

move_present_ptes() 배치 이동 조건

조건결과설명
`folio_test_large(src_folio)``-EBUSY`large folio는 배치 이동 불가
`folio_maybe_dma_pinned(src_folio)``-EBUSY`DMA 핀된 folio는 이동 불가
`!PageAnonExclusive(&src_folio->page)``-EBUSY`공유 익명 페이지 이동 불가
`check_ptes_for_batched_move()` 실패배치 중단다음 PTE에서 개별 이동
`folio_maybe_dma_pinned()` (이동 중)`-EBUSY` + 복원이동 중 DMA 핀 발생

관련 문서

  • 메모리 관리 개요
  • VMA / mmap
  • vmalloc
  • Swap / zswap
  • Huge Pages / THP
  • Folio / Page Cache
  • Memory Cgroup
  • Reverse Mapping
  • GUP (Get User Pages)
  • Migration