# mlock / munlock 📌
관련 소스:mm/mlock.c,mm/swap.c,include/linux/mm.h
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
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),
};
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);
}
// 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 중 동시 마이그레이션 방지용 일시적 플래그// 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 통계 갱신)VM_LOCKED VMA에서 새 folio가 LRU에 들어가기 전에 쓰는 경로입니다. 아직 LRU에 올라오기 전이라 __mlock_new_folio()가 PG_mlocked와 mlock_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);
}
mlock_new()로 표시한 뒤 배치에 넣습니다.swap.c의 folio_add_lru_vma()는 VM_LOCKED VMA면 이 경로로 바로 보냅니다.// 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;
}
mlock_count 중복 mlock 추적 (이미 unevictable이면 카운트만 증가)UNEVICTABLE_PGRESCUED: evictable인데 mlocked가 해제된 케이스// 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 해제// 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;
}
locked_vm 카운트 관리 (RLIMIT_MEMLOCK 체크용)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 상태로 돌립니다.// 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 확인__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 시]
| 비교 항목 | mlock | mlock2 | mlockall |
|---|---|---|---|
| 범위 | 특정 주소 범위 | 특정 주소 범위 | 프로세스 전체 VMA |
| 지연 모드 | 없음 | `MLOCK_ONFAULT` 가능 | `MCL_ONFAULT` 가능 |
| 미래 할당 | 없음 | 없음 | `MCL_FUTURE` 지원 |
| 현재 할당 | 항상 | 항상 | `MCL_CURRENT` (기본값) |
| lock_limit 체크 | `mm->locked_vm + len` | `mm->locked_vm + len` | `total_vm <= lock_limit` |
| 비교 항목 | VM_LOCKED | VM_LOCKONFAULT | VM_LOCKED 해제 |
|---|---|---|---|
| 페이지 고정 시점 | VMA 태그 시 | 페이지 폴트 시 | 해제 시 unevictable 해제 |
| __mm_populate | 필요 | 불필요 (폴트 시) | 불필요 |
| memcg 회수 | 차단 | 차단 | 허용 |
| unevictable LRU | 즉시 이동 | 폴트 시 이동 | active/inactive로 복귀 |
| 비교 항목 | 특수 VMA 처리 | 결과 |
|---|---|---|
| VM_SPECIAL ( vdso, vsyscall 등) | mlock 무시 | `goto out` |
| HugeTLB VMA | mlock 무시 | `goto out` |
| DAX VMA | mlock 무시 | `goto out` |
| secretmem VMA | mlock 무시 | `goto out` |
| VM_DROPPABLE VMA | mlock 무시 | `goto out` |
| VM_LOCKED + VM_LOCKED (이미 고정) | 페이지 워크 없이 플래그만 갱신 | `vm_flags_reset` |
| 비교 항목 | __mlock_folio | __munlock_folio | __mlock_new_folio |
|---|---|---|---|
| LRU 상태 | active/inactive → unevictable | unevictable → active/inactive | LRU에 없음 → unevictable |
| mlock_count | 증가 (이미 unevictable 시) | 감소 (0이 되면 해제) | `!!PG_mlocked` |
| 통계 이벤트 | PGCULLED, PGRESCUED | PGMUNLOCKED, PGSTRANDED | PGCULLED |