The Unevictable LRU and mlock

Most pages in the system are eligible for reclaim: under memory pressure the kernel can write a dirty page back, drop a clean cached one, or swap an anonymous one out. A minority of pages must never be reclaimed — pages an application has mlock()ed into RAM, pages backed by ramfs (which has no backing store to evict to), SHM_LOCKed shared memory, and similar. Linux gives these pages their own LRU list, LRU_UNEVICTABLE, and a page flag, PG_unevictable, so that the reclaim scanner (vmscan) never wastes time examining them. Before this infrastructure existed, the scanner would repeatedly walk such pages, find them unreclaimable, and put them back — on a 128 GB box with tens of millions of locked pages this could pin every CPU in vmscan “for hours or days on end, with the system completely unresponsive” (Documentation/mm/unevictable-lru.rst). Segregating them off the evictable lists turns that wasted scanning into an O(1) skip.

This note explains what makes a page unevictable, the LRU_UNEVICTABLE list and the PG_mlocked / PG_unevictable flags that drive it, the mlock / mlock2 / mlockall / MAP_LOCKED interfaces that user space uses to pin pages, MLOCK_ONFAULT, the RLIMIT_MEMLOCK limit and CAP_IPC_LOCK privilege, the non-mlock sources of unevictable pages, and how to observe it all through /proc/meminfo. It is the companion to The LRU Lists (which covers the evictable active/inactive lists) and Memory Reclaim Overview (the scanner that consumes them). Version claims are pinned to Linux 6.12 LTS (released 2024-11-17).


Mental Model — A Sin Bin the Scanner Skips

flowchart LR
  subgraph EVICT["Evictable LRU lists (per node, per memcg)"]
    AF["Active file"]
    IF["Inactive file"]
    AA["Active anon"]
    IA["Inactive anon"]
  end
  UNEVICT["LRU_UNEVICTABLE list<br/>PG_unevictable set"]
  SCAN["vmscan / kswapd<br/>scans evictable lists only"]
  MLOCK["mlock() / mlockall()<br/>MAP_LOCKED / SHM_LOCK / ramfs"]

  MLOCK -->|"set PG_mlocked + PG_unevictable"| UNEVICT
  AF & IF & AA & IA -->|"folio_evictable() == false"| UNEVICT
  UNEVICT -->|"munlock / unmap / drop ramfs"| IA
  SCAN --> AF
  SCAN --> IF
  SCAN --> AA
  SCAN --> IA
  SCAN -.->|"NEVER walks"| UNEVICT

The unevictable LRU as a sin bin. What it shows: the kernel keeps several evictable LRU lists (active/inactive × file/anon) that the reclaim scanner walks looking for victims. Pages that cannot be reclaimed are moved to a separate LRU_UNEVICTABLE list and stamped PG_unevictable; the scanner never walks that list. Pages enter it when locked (mlock, MAP_LOCKED, SHM_LOCK, ramfs), and leave it back to the inactive list when the lock is dropped or the page is unmapped. The insight to take: “unevictable” is implemented as an LRU list, not as a flag the scanner checks per page — so the cost of having millions of locked pages is the cost of not looking at them, which is zero. Keeping them on an LRU-shaped structure (rather than a plain set) means the same isolation/migration/accounting code paths work on them unchanged.


What Makes a Page Unevictable

The kernel’s folio_evictable() predicate (in mm/internal.h) decides whether a folio may be reclaimed. A folio is unevictable when either its address space carries the AS_UNEVICTABLE flag (the whole mapping is unreclaimable) or folio_test_mlocked() is true (the folio is locked into at least one VMA) (unevictable-lru doc). The documentation enumerates the classes of unevictable pages:

  1. ramfs-owned pages. ramfs is a filesystem with no backing store — its pages exist only in RAM, so there is nowhere to evict them to. Its address space is marked with mapping_set_unevictable().
  2. tmpfs mounted with the noswap option. Like ramfs, such pages have nowhere to go.
  3. SHM_LOCKed System V shared memory. shmctl(id, SHM_LOCK, …) locks a shared-memory segment into RAM (shmctl(2)); the segment’s pages become unevictable until SHM_UNLOCK.
  4. VM_LOCKED VMAs — the big one. Any VMA flagged VM_LOCKED because the application called mlock(), mlock2(), mlockall(), or mapped with mmap(... MAP_LOCKED) has its pages pinned.

Additional pinned-memory users (the i915 GPU driver, hugetlbfs allocations, secretmem / memfd_secret pages) also land here. The /proc/meminfo description for the Unevictable field captures the breadth: “Memory allocated for userspace which cannot be reclaimed, such as mlocked pages, ramfs backing pages, secret memfd pages etc.” (Documentation/filesystems/proc.rst).

The Two Flags: PG_mlocked and PG_unevictable

These flags do related but distinct jobs, and conflating them is a common error.

  • PG_mlocked marks a folio that is mapped into at least one VM_LOCKED VMA. It is set by folio_set_mlocked() when the page is faulted into (or found in) such a VMA, and cleared by folio_clear_mlocked(). It is the cause signal — “some VMA has locked this.”
  • PG_unevictable marks a folio that is currently managed on the LRU_UNEVICTABLE list. It is the placement signal. The unevictable-lru doc notes it is “analogous in function” and mutually exclusive with PG_active: a folio is either on an evictable list (possibly active) or on the unevictable list, never both.

The AS_UNEVICTABLE address-space flag is the third piece — it makes an entire mapping unevictable (the ramfs/SHM_LOCK case) without needing a per-page PG_mlocked. It is manipulated through mapping_set_unevictable(), mapping_clear_unevictable(), and tested via mapping_unevictable().

The mlock Family of System Calls

User space pins pages with a small family of calls, all documented in mlock(2):

  • mlock(addr, len) locks the pages covering [addr, addr+len). On success, “all pages that contain a part of the specified address range are guaranteed to be resident in RAM when the call returns successfully; the pages are guaranteed to stay in RAM until later unlocked.” Locking and unlocking operate in whole-page units; Linux rounds addr down to the page boundary.
  • mlock2(addr, len, flags) is mlock plus a flags argument. The one flag is MLOCK_ONFAULT: “Lock pages that are currently resident and mark the entire range so that the remaining nonresident pages are locked when they are populated by a page fault.” This avoids the cost of pre-faulting the whole range up front — useful for a large mapping where you want locking semantics but expect to touch only part of it.
  • mlockall(flags) locks the process’s entire address space. MCL_CURRENT locks all currently-mapped pages; MCL_FUTURE locks pages that become mapped later (so future mmap/brk/stack growth is locked on fault); MCL_ONFAULT (since Linux 4.4) applies the on-fault semantics to current and/or future mappings and must be combined with MCL_CURRENT or MCL_FUTURE.
  • munlock / munlockall reverse the above.

Two semantic subtleties matter. First, locks do not stack: “pages which have been locked several times by calls to mlock(), mlock2(), or mlockall() will be unlocked by a single call to munlock() for the corresponding range or by munlockall().” Second, locks are not inherited across fork(2) and are dropped on execve(2); the man page warns against fork() after locking because copy-on-write would otherwise interact awkwardly with the residency guarantee. Locks vanish automatically if the range is munmap()ed.

MAP_LOCKED (an mmap(2) flag) locks pages at map time, equivalent to mlocking the region — though the man page notes MAP_LOCKED is weaker than mlock in corner cases (it does not guarantee the whole region is populated if some pages fail to fault in).

What mlock does not guarantee

mlock guarantees residency (no page fault, no swap) — it does not guarantee the page stays at the same physical frame. The kernel may still migrate an mlocked page to another frame for compaction, NUMA balancing, or memory hotplug; residency is preserved, the physical address is not. This distinction is why mlocked pages are still kept on an LRU-shaped list that supports isolation for migration (below), rather than being removed from mm/’s reach entirely.

How a Page Moves Onto and Off the Unevictable List

The mechanism lives in mm/mlock.c (v6.12), and the doc walks it (unevictable-lru):

Locking. mlock() / mlock2() / mlockall() invoke mlock_fixup() for each affected VMA, setting VM_LOCKED. For pages already present, mlock_folio() (via mlock_pte_range()) sets PG_mlocked; __mlock_folio() then “sets PG_unevictable, initializes mlock_count and moves the page to unevictable state.” Pages not yet present are faulted in via __mm_populate() / get_user_pages() so the residency guarantee holds on return.

Unlocking. munlock() / munlockall() also go through mlock_fixup(). munlock_folio()__munlock_folio() decrements mlock_count; “when that reaches 0 it clears the mlocked flag and clears the unevictable flag, moving the folio from unevictable state to the inactive LRU.”

mlock_count — the key 5.18 change. A page can be mapped into several VM_LOCKED VMAs at once (e.g. shared memory locked by multiple processes). The kernel must keep the page unevictable until the last lock is dropped, so it needs a count of how many VM_LOCKED VMAs map it. Before 5.18 the kernel determined this by walking the reverse map (rmap) on every munlock — expensive. Since Linux 5.18 an mlock_count is stored in the folio’s LRU list-link field (reusing the space because an unevictable folio is not being threaded for LRU ordering anyway), tracking “the number of VM_LOCKED VMAs mapping the page” directly and without preventing migration (unevictable-lru doc).

The scanner’s safety net. Pages can still end up on an evictable list by mistake (e.g. a page becomes locked after it was already on the inactive list). The reclaim functions shrink_active_list(), shrink_inactive_list(), and shrink_folio_list() cull such pages when they encounter them — diverting them to the unevictable list via folio_putback_lru() (the inverse of folio_isolate_lru()). Conversely, when folio_referenced() or try_to_unmap() discover a page still mapped into a VM_LOCKED VMA, they call mlock_vma_folio() to re-mark it. So the unevictable list is eventually consistent: a transiently misplaced locked page gets caught and moved on the next scan rather than being wrongly evicted.

VMAs That Are Skipped, and the Huge-Page Twist

mlock_fixup() deliberately ignores certain VMAs because their pages are already pinned by other means and never sit on the LRU lists. In v6.12 mm/mlock.c the skip condition reads:

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;   /* leave VM_LOCKED unset; skip */

VM_SPECIAL is a mask that bundles the “inherently pinned / not on the LRU” flags — VM_IO and VM_PFNMAP (device memory behind these mappings is inherently pinned), VM_DONTEXPAND (VDSO, relay channels — “inherently unevictable and not managed on the LRU lists”), and VM_MIXEDMAP. Hugetlbfs pages (is_vm_hugetlb_page) are already pinned, and the gate VMA, DAX, secretmem, and droppable VMAs are likewise excluded. Because VM_LOCKED is never set on them, they are correspondingly skipped on munlock too.

Transparent huge pages (THP) add a wrinkle. A THP is one entry on the LRU, so the kernel “can only make unevictable an entire compound page, not individual subpages.” If mlock covers only part of a huge page (the PMD straddles the edge of a VM_LOCKED VMA), the kernel splits the PMD into a PTE table and keeps the now-PTE-mapped page on the evictable list, letting vmscan split it and reclaim the subpages that fall outside the locked region.

Uncertain

Verify: the precise THP split-on-partial-mlock behavior in 6.12 specifically — i.e. that a PMD straddling a VM_LOCKED boundary is split into a PTE table and the page kept on the evictable list. Reason: this is quoted from Documentation/mm/unevictable-lru.rst (which tracks current behavior but is not auto-pinned to a release) rather than read line-by-line from v6.12 mm/. The VMA-skip condition above was verified directly against v6.12 mm/mlock.c. To resolve: read mlock_pte_range() / the PMD-handling path in the v6.12 source. uncertain

RLIMIT_MEMLOCK and CAP_IPC_LOCK

Locking memory is privileged because it removes pages from the reclaimable pool — an unprivileged process that could lock arbitrary amounts of RAM could starve the rest of the system. Two gates apply (mlock(2)):

  • RLIMIT_MEMLOCK is the soft resource limit (in bytes, rounded to pages) on how much memory an unprivileged process may lock. Exceeding it makes mlock fail with ENOMEM. Since Linux 2.6.9 a process with CAP_IPC_LOCK has no such limit. The limit is per-process and visible via ulimit -l / getrlimit(RLIMIT_MEMLOCK).
  • CAP_IPC_LOCK is the capability that lifts the limit (and is required at all for some locking operations). Without it and over the limit, mlock returns EPERM or ENOMEM depending on the case.

The error codes are worth knowing: EAGAIN (some/all of the range could not be locked), ENOMEM (range has unmapped pages, would exceed an address-space limit, or exceeds RLIMIT_MEMLOCK), EPERM (lacks CAP_IPC_LOCK where required), and EINVAL (bad flags, or MCL_ONFAULT without MCL_CURRENT/MCL_FUTURE, or address overflow).

In container and Kubernetes contexts RLIMIT_MEMLOCK is a recurring footgun: workloads that legitimately need to lock memory (databases preventing key material from swapping, RDMA/DPDK pinning buffers, eBPF maps) hit the default limit and need it raised or CAP_IPC_LOCK granted.

Configuration and Observation

A typical use — lock a buffer of secret key material so it never swaps to disk:

#include <sys/mman.h>
 
unsigned char *key = aligned_alloc(sysconf(_SC_PAGESIZE), 4096);
if (mlock(key, 4096) != 0)          /* pin into RAM; whole pages */
        perror("mlock");            /* likely ENOMEM (RLIMIT_MEMLOCK) or EPERM */
/* ... use key; it will never be written to swap ... */
munlock(key, 4096);                 /* single munlock drops the lock — no stacking */
explicit_bzero(key, 4096);
free(key);

Lock the whole process and everything it maps in future — common for real-time and latency-sensitive daemons that cannot tolerate a page fault mid-operation:

/* lock current pages AND every future mapping; on-fault to avoid pre-faulting all */
if (mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) != 0)
        perror("mlockall");

Observe system-wide locked memory in /proc/meminfo (Documentation/filesystems/proc.rst):

Unevictable:     1048576 kB   # all unreclaimable userspace: mlock + ramfs + secretmem + ...
Mlocked:          524288 kB   # specifically memory locked with mlock()

UnevictableMlocked always, because Mlocked is the mlock-specific subset of the broader unevictable total. Per-process, /proc/<pid>/status shows VmLck (“locked memory size”) — the amount that process has pinned.

Failure Modes and Common Misunderstandings

  • mlock pins the page at a fixed physical address.” No — it guarantees residency, not a stable physical frame. Page Migration can still relocate the page (for compaction or NUMA), which is exactly why the doc stresses that mlock_count “without preventing page migration.” Code that needs a stable physical address (DMA) must use [[get_user_pages and Page Pinning|get_user_pages/pin_user_pages]], not mlock.
  • Locks don’t stack — a single munlock unlocks. Calling mlock three times then munlock once fully unlocks; programmers who expect reference-counted lock/unlock pairs get surprised. (The kernel-internal mlock_count does count VM_LOCKED VMAs, but that is per-mapping, not per-mlock-call.)
  • RLIMIT_MEMLOCK ambush. The default soft limit is small (often 64 KiB historically, larger on modern systemd defaults). Databases, RDMA, DPDK, and eBPF all routinely exceed it and fail with ENOMEM/EPERM until the limit is raised — a frequent first-deploy failure.
  • fork() after mlock. Locks are not inherited and COW interacts poorly; the man page explicitly advises against it. The child does not get the lock.
  • The historical livelock this fixed. The entire feature exists because, pre-unevictable-LRU, the scanner spent unbounded time re-scanning unreclaimable pages on big memory machines. If you see kswapd/direct-reclaim CPU that scales with locked memory size, you are looking at the problem this list was built to eliminate.

See Also