Ryotta's Linux 7.0 MM

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

# mlock / munlock 📌

관련 소스: mm/mlock.c, mm/swap.c, include/linux/mm.h
mlock 호출 흐름
mlock 자료구조 관계도

개요 (Overview)

mlock은 사용자 공간의 메모리 페이지를 물리 메모리에 고정하여 커널의 페이지 회수(kswapd, direct reclaim)나 스왑 아웃으로부터 보호하는 시스템 호출입니다. 실시간 시스템, 저지연(latency) 애플리케이션, 암호화 키 등에서 중요한 보호 메커니즘으로, mlock, mlock2, mlockall과 해제 함수 munlock, munlockall을 제공합니다.

커널 내부적으로 mlock된 페이지는 folio에 PG_mlocked 플래그를 설정하고, LRU(active/inactive)에서 unevictable 목록으로 이동시킵니다. folio의 mlock_count 필드는 같은 folio가 여러 VMA에서 mlock될 수 있는 경우를 추적합니다. VM_LOCKED 플래그는 VMA에, VM_LOCKONFAULT(mlock2)은 페이지 폴트 시점에 고정하는 지연 모드를 나타냅니다.

이 과정은 도서관에서 자주 쓰는 책에 반납 금지 표식을 붙여 두는 것과 비슷합니다. 책을 아예 치우는 것이 아니라, 회수 담당자가 먼저 가져가 버리지 못하게 표시만 남기고 필요할 때는 그대로 꺼내 읽게 하는 방식입니다.

// mm/mlock.c:1-29 — 헤더 및 기본 구조
#include <linux/capability.h>
#include <linux/mman.h>
#include <linux/mm.h>
#include <linux/sched/user.h>
#include <linux/swap.h>
#include <linux/pagewalk.h>
#include <linux/mempolicy.h>
#include <linux/rmap.h>

빠른 점검 명령

# 현재 프로세스의 locked 메모리와 기본 상태 확인
grep -E '^VmLck|^VmRSS|^VmSize' /proc/self/status

# 시스템 전체 unevictable / mlocked 통계
grep -E '^Unevictable:|^Mlocked:' /proc/meminfo

# VMA 단위 lock 플래그 확인
grep -E 'VmFlags:.*\blo\b' /proc/<PID>/smaps

# RLIMIT_MEMLOCK 한도 확인
ulimit -l

# mlock / munlock / rescued / stranded 통계
grep -E 'nr_mlock|unevictable_pgs_(mlocked|munlocked|rescued|stranded)' /proc/vmstat

# 프로세스 요약 통계에서 Locked 확인
grep -E '^Locked:|^Rss:' /proc/<PID>/smaps_rollup

# mlock 대상과 함께 자주 보는 per-process 항목
grep -E '^VmLck|^VmPin' /proc/<PID>/status

# lock 한도와 실제 NUMA 배치 같이 보기
cat /proc/<PID>/numa_maps | head -20

핵심 자료구조

struct mlock_fbatch

Per-CPU 배치 구조체로, mlock/munlock 연산을 모았다가 한 번에 처리하여 lock 오버헤드를 줄입니다.

// mm/mlock.c:31-38 — mlock_fbatch 정의
struct mlock_fbatch {
	local_lock_t lock;
	struct folio_batch fbatch;
};

static DEFINE_PER_CPU(struct mlock_fbatch, mlock_fbatch) = {
	.lock = INIT_LOCAL_LOCK(lock),
};

folio 포인터 플래그 인코딩

mlock_fbatch 내에서 folio 포인터의 하위 비트에 작업 유형을 인코딩합니다. LRU_FOLIO(0x1)는 기존 LRU의 folio, NEW_FOLIO(0x2)는 새로 할당된 folio를 나타냅니다. 둘 다 아니면 munlock입니다.

// mm/mlock.c:167-177 — folio 포인터 하위 비트 인코딩
#define LRU_FOLIO 0x1
#define NEW_FOLIO 0x2
static inline struct folio *mlock_lru(struct folio *folio)
{
	return (struct folio *)((unsigned long)folio + LRU_FOLIO);
}

static inline struct folio *mlock_new(struct folio *folio)
{
	return (struct folio *)((unsigned long)folio + NEW_FOLIO);
}

vm_area_struct 주요 플래그

// include/linux/mm.h:420-575 — VM_LOCKED 관련 플래그
#define VM_LOCKED	INIT_VM_FLAG(LOCKED)
#define VM_LOCKONFAULT	INIT_VM_FLAG(LOCKONFAULT)
#define VM_LOCKED_MASK	(VM_LOCKED | VM_LOCKONFAULT)
  • VM_LOCKED: VMA의 모든 페이지를 물리 메모리에 고정
  • VM_LOCKONFAULT: 페이지 폴트 시점에만 고정 (지연 모드)
  • VM_IO: mlock_vma_pages_range 중 동시 마이그레이션 방지용 일시적 플래그
  • 핵심 함수

    mlock_folio — 기존 LRU folio 고정

    // mm/mlock.c:242-261 — mlock_folio()
    void mlock_folio(struct folio *folio)
    {
    	struct folio_batch *fbatch;
    
    	local_lock(&mlock_fbatch.lock);
    	fbatch = this_cpu_ptr(&mlock_fbatch.fbatch);
    
    	if (!folio_test_set_mlocked(folio)) {
    		int nr_pages = folio_nr_pages(folio);
    		zone_stat_mod_folio(folio, NR_MLOCK, nr_pages);
    		__count_vm_events(UNEVICTABLE_PGMLOCKED, nr_pages);
    	}
    
    	folio_get(folio);
    	if (!folio_batch_add(fbatch, mlock_lru(folio)) ||
    	    !folio_may_be_lru_cached(folio) || lru_cache_disabled())
    		mlock_folio_batch(fbatch);
    	local_unlock(&mlock_fbatch.lock);
    }
  • folio_test_set_mlocked: atomic set & test로 처음 mlock인지 확인 (NR_MLOCK 통계 갱신)
  • 배치가 가득 차거나 LRU 캐시 비활성화 시 즉시 drain
  • mlock_new_folio — 새로 할당된 folio 고정

    VM_LOCKED VMA에서 새 folio가 LRU에 들어가기 전에 쓰는 경로입니다. 아직 LRU에 올라오기 전이라 __mlock_new_folio()PG_mlockedmlock_count를 먼저 세팅하고 unevictable 상태로 넣습니다.

    // mm/mlock.c:263-284 — mlock_new_folio()
    void mlock_new_folio(struct folio *folio)
    {
    	struct folio_batch *fbatch;
    	int nr_pages = folio_nr_pages(folio);
    
    	local_lock(&mlock_fbatch.lock);
    	fbatch = this_cpu_ptr(&mlock_fbatch.fbatch);
    	folio_set_mlocked(folio);
    
    	zone_stat_mod_folio(folio, NR_MLOCK, nr_pages);
    	__count_vm_events(UNEVICTABLE_PGMLOCKED, nr_pages);
    
    	folio_get(folio);
    	if (!folio_batch_add(fbatch, mlock_new(folio)) ||
    	    !folio_may_be_lru_cached(folio) || lru_cache_disabled())
    		mlock_folio_batch(fbatch);
    	local_unlock(&mlock_fbatch.lock);
    }
  • 새로 할당된 folio는 mlock_new()로 표시한 뒤 배치에 넣습니다.
  • swap.cfolio_add_lru_vma()VM_LOCKED VMA면 이 경로로 바로 보냅니다.
  • __mlock_folio — LRU에서 unevictable로 이동

    // mm/mlock.c:61-101 — __mlock_folio()
    static struct lruvec *__mlock_folio(struct folio *folio, struct lruvec *lruvec)
    {
    	if (!folio_test_clear_lru(folio))
    		return lruvec;
    
    	lruvec = folio_lruvec_relock_irq(folio, lruvec);
    
    	if (unlikely(folio_evictable(folio))) {
    		if (folio_test_unevictable(folio)) {
    			lruvec_del_folio(lruvec, folio);
    			folio_clear_unevictable(folio);
    			lruvec_add_folio(lruvec, folio);
    			__count_vm_events(UNEVICTABLE_PGRESCUED,
    					  folio_nr_pages(folio));
    		}
    		goto out;
    	}
    
    	if (folio_test_unevictable(folio)) {
    		if (folio_test_mlocked(folio))
    			folio->mlock_count++;
    		goto out;
    	}
    
    	lruvec_del_folio(lruvec, folio);
    	folio_clear_active(folio);
    	folio_set_unevictable(folio);
    	folio->mlock_count = !!folio_test_mlocked(folio);
    	lruvec_add_folio(lruvec, folio);
    	__count_vm_events(UNEVICTABLE_PGCULLED, folio_nr_pages(folio));
    out:
    	folio_set_lru(folio);
    	return lruvec;
    }
  • active/inactive 목록에서 unevictable 목록으로 이동
  • mlock_count 중복 mlock 추적 (이미 unevictable이면 카운트만 증가)
  • UNEVICTABLE_PGRESCUED: evictable인데 mlocked가 해제된 케이스
  • __munlock_folio — munlock 처리

    // mm/mlock.c:122-162 — __munlock_folio()
    static struct lruvec *__munlock_folio(struct folio *folio, struct lruvec *lruvec)
    {
    	int nr_pages = folio_nr_pages(folio);
    	bool isolated = false;
    
    	if (!folio_test_clear_lru(folio))
    		goto munlock;
    
    	isolated = true;
    	lruvec = folio_lruvec_relock_irq(folio, lruvec);
    
    	if (folio_test_unevictable(folio)) {
    		if (folio->mlock_count)
    			folio->mlock_count--;
    		if (folio->mlock_count)
    			goto out;
    	}
    
    munlock:
    	if (folio_test_clear_mlocked(folio)) {
    		__zone_stat_mod_folio(folio, NR_MLOCK, -nr_pages);
    		if (isolated || !folio_test_unevictable(folio))
    			__count_vm_events(UNEVICTABLE_PGMUNLOCKED, nr_pages);
    		else
    			__count_vm_events(UNEVICTABLE_PGSTRANDED, nr_pages);
    	}
    
    	if (isolated && folio_test_unevictable(folio) && folio_evictable(folio)) {
    		lruvec_del_folio(lruvec, folio);
    		folio_clear_unevictable(folio);
    		lruvec_add_folio(lruvec, folio);
    		__count_vm_events(UNEVICTABLE_PGRESCUED, nr_pages);
    	}
    out:
    	if (isolated)
    		folio_set_lru(folio);
    	return lruvec;
    }
  • mlock_count 감소 후 0이 되면 실제 mlock 해제
  • UNEVICTABLE_PGSTRANDED: LRU에서 격리되지 않은 상태에서 mlocked 해제
  • mlock_fixup — VMA 단위 mlock/munlock 처리

    // mm/mlock.c:466-512 — mlock_fixup()
    static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_struct *vma,
    	       struct vm_area_struct **prev, unsigned long start,
    	       unsigned long end, vm_flags_t newflags)
    {
    	struct mm_struct *mm = vma->vm_mm;
    	int nr_pages;
    	int ret = 0;
    	vm_flags_t oldflags = vma->vm_flags;
    
    	if (newflags == oldflags || (oldflags & VM_SPECIAL) ||
    	    is_vm_hugetlb_page(vma) || vma == get_gate_vma(current->mm) ||
    	    vma_is_dax(vma) || vma_is_secretmem(vma) || (oldflags & VM_DROPPABLE))
    		goto out;
    
    	vma = vma_modify_flags(vmi, *prev, vma, start, end, &newflags);
    	if (IS_ERR(vma)) {
    		ret = PTR_ERR(vma);
    		goto out;
    	}
    
    	nr_pages = (end - start) >> PAGE_SHIFT;
    	if (!(newflags & VM_LOCKED))
    		nr_pages = -nr_pages;
    	else if (oldflags & VM_LOCKED)
    		nr_pages = 0;
    	mm->locked_vm += nr_pages;
    
    	if ((newflags & VM_LOCKED) && (oldflags & VM_LOCKED)) {
    		vma_start_write(vma);
    		vm_flags_reset(vma, newflags);
    	} else {
    		mlock_vma_pages_range(vma, start, end, newflags);
    	}
    out:
    	*prev = vma;
    	return ret;
    }
  • VM_SPECIAL, HugeTLB, DAX, secretmem, VM_DROPPABLE VMA는 mlock 무시
  • locked_vm 카운트 관리 (RLIMIT_MEMLOCK 체크용)
  • 기존에 이미 VM_LOCKED였으면 페이지 워크 없이 플래그만 갱신
  • mlock_vma_pages_range — 페이지 워크와 임시 VM_IO 처리

    mlock_fixup()가 VMA 플래그를 바꾼 뒤 실제 페이지를 순회하는 경로입니다. VM_LOCKED를 켠 상태에서 다른 경로가 같은 folio를 다시 세지 않도록, 순회 동안만 VM_IO를 덧붙였다가 끝나면 제거합니다.

    // mm/mlock.c:423-455 — mlock_vma_pages_range()
    static void mlock_vma_pages_range(struct vm_area_struct *vma,
    	unsigned long start, unsigned long end, vm_flags_t newflags)
    {
    	static const struct mm_walk_ops mlock_walk_ops = {
    		.pmd_entry = mlock_pte_range,
    		.walk_lock = PGWALK_WRLOCK_VERIFY,
    	};
    
    	if (newflags & VM_LOCKED)
    		newflags |= VM_IO;
    	vma_start_write(vma);
    	vm_flags_reset_once(vma, newflags);
    
    	lru_add_drain();
    	walk_page_range(vma->vm_mm, start, end, &mlock_walk_ops, NULL);
    	lru_add_drain();
    
    	if (newflags & VM_IO) {
    		newflags &= ~VM_IO;
    		vm_flags_reset_once(vma, newflags);
    	}
    }
  • VM_IO는 여기서만 잠깐 붙는 보조 플래그입니다.
  • walk_page_range()mlock_pte_range()를 부르며 THP와 PTE 경로를 나눕니다.
  • 끝나면 VM_IO를 지워 원래 VMA 상태로 돌립니다.
  • do_mlock — 시스템 호출 진입점

    // mm/mlock.c:612-657 — do_mlock()
    static __must_check int do_mlock(unsigned long start, size_t len, vm_flags_t flags)
    {
    	unsigned long locked;
    	unsigned long lock_limit;
    	int error = -ENOMEM;
    
    	start = untagged_addr(start);
    	if (!can_do_mlock())
    		return -EPERM;
    
    	len = PAGE_ALIGN(len + (offset_in_page(start)));
    	start &= PAGE_MASK;
    
    	lock_limit = rlimit(RLIMIT_MEMLOCK);
    	lock_limit >>= PAGE_SHIFT;
    	locked = len >> PAGE_SHIFT;
    
    	if (mmap_write_lock_killable(current->mm))
    		return -EINTR;
    
    	locked += current->mm->locked_vm;
    	if ((locked > lock_limit) && (!capable(CAP_IPC_LOCK))) {
    		locked -= count_mm_mlocked_page_nr(current->mm, start, len);
    	}
    
    	if ((locked <= lock_limit) || capable(CAP_IPC_LOCK))
    		error = apply_vma_lock_flags(start, len, flags);
    
    	mmap_write_unlock(current->mm);
    	if (error)
    		return error;
    
    	error = __mm_populate(start, len, 0);
    	if (error)
    		return __mlock_posix_error_return(error);
    	return 0;
    }
  • can_do_mlock: RLIMIT_MEMLOCK != 0 또는 CAP_IPC_LOCK capability 확인
  • 기존 mlocked 영역 중복 체크 후 lock_limit 재조정
  • __mm_populate: 실제 페이지를 물리 메모리에 올림 (demand paging 해소)
  • 호출 흐름

    mlock(start, len)
      → do_mlock(start, len, VM_LOCKED)
        → can_do_mlock()                 [권한 체크]
        → mmap_write_lock_killable()     [mmap_lock 획득]
        → apply_vma_lock_flags(start, len, flags)
          → for_each_vma_range()
            → mlock_fixup()
              → vma_modify_flags()       [VMA 분할/병합]
              → mm->locked_vm 갱신
              → mlock_vma_pages_range()
                → walk_page_range(mlock_pte_range)
                  → mlock_pte_range()
                    → [THP] pmd_folio() → mlock_folio()
                    → [PTE] vm_normal_folio() → mlock_folio() / munlock_folio()
        → mmap_write_unlock()
        → __mm_populate()                [페이지 실제 할당]
    
    새로 생긴 folio는 `mm/swap.c:523-530`의 `folio_add_lru_vma()`에서 `VM_LOCKED` VMA인지 보고 `mlock_new_folio()`로 들어가며, 그 밖의 folio는 일반 LRU 경로를 탑니다.
    
    munlock(start, len)
      → apply_vma_lock_flags(start, len, 0)
        → mlock_fixup() → mlock_vma_pages_range()
          → munlock_folio()
    
    mlockall(flags)
      → do_mlockall → apply_mlockall_flags()
        → mm->def_flags |= VM_LOCKED     [MCL_FUTURE 시]
        → for_each_vma() → mlock_fixup()
        → mm_populate()                   [MCL_CURRENT 시]

    조건별 비교

    비교 항목mlockmlock2mlockall
    범위특정 주소 범위특정 주소 범위프로세스 전체 VMA
    지연 모드없음`MLOCK_ONFAULT` 가능`MCL_ONFAULT` 가능
    미래 할당없음없음`MCL_FUTURE` 지원
    현재 할당항상항상`MCL_CURRENT` (기본값)
    lock_limit 체크`mm->locked_vm + len``mm->locked_vm + len``total_vm <= lock_limit`
    비교 항목VM_LOCKEDVM_LOCKONFAULTVM_LOCKED 해제
    페이지 고정 시점VMA 태그 시페이지 폴트 시해제 시 unevictable 해제
    __mm_populate필요불필요 (폴트 시)불필요
    memcg 회수차단차단허용
    unevictable LRU즉시 이동폴트 시 이동active/inactive로 복귀
    비교 항목특수 VMA 처리결과
    VM_SPECIAL ( vdso, vsyscall 등)mlock 무시`goto out`
    HugeTLB VMAmlock 무시`goto out`
    DAX VMAmlock 무시`goto out`
    secretmem VMAmlock 무시`goto out`
    VM_DROPPABLE VMAmlock 무시`goto out`
    VM_LOCKED + VM_LOCKED (이미 고정)페이지 워크 없이 플래그만 갱신`vm_flags_reset`
    비교 항목__mlock_folio__munlock_folio__mlock_new_folio
    LRU 상태active/inactive → unevictableunevictable → active/inactiveLRU에 없음 → unevictable
    mlock_count증가 (이미 unevictable 시)감소 (0이 되면 해제)`!!PG_mlocked`
    통계 이벤트PGCULLED, PGRESCUEDPGMUNLOCKED, PGSTRANDEDPGCULLED

    관련 문서

  • 메모리 관리 개요
  • VMA / mmap
  • 페이지 회수
  • Folio / Page Cache
  • Memory Cgroup
  • mprotect
  • madvise