mlockall() and munlockall() Lock the Entire Process Address Space

 

mlockall() and munlockall()
Chapter 50 · File 2 of 3 — Lock the Entire Process Address Space

mlock() vs mlockall() — When to Use Which

In the previous file, we saw mlock() which locks a specific address range. You have to know exactly which buffers you want to lock and call mlock() on each one.

mlockall() takes a different approach — it locks everything in the process’s virtual address space in one call. This is more convenient when:

  • You want to guarantee that all process memory is always in RAM
  • Your real-time code uses many different buffers and it is impractical to track each one
  • You want to lock even future allocations automatically (using MCL_FUTURE)
  • You are writing a daemon or service that must have zero page-fault latency

The mlockall() and munlockall() API

#include <sys/mman.h>

int mlockall(int flags);
int munlockall(void);

/* Both return 0 on success, or -1 on error */

mlockall(flags) — Locks some or all of the pages in the calling process’s virtual address space. The flags argument controls which pages are locked.

munlockall() — Removes all memory locks established by the calling process. Also cancels any pending MCL_FUTURE lock so future allocations will no longer be automatically locked.

mlockall() Flags — MCL_CURRENT and MCL_FUTURE

Flag What It Locks When Pages Are Loaded Typical Use
MCL_CURRENT All pages currently mapped — text, data, stack, heap, all mappings Immediately — all resident before mlockall() returns Lock everything right now
MCL_FUTURE All pages mapped after this call — new mmap(), heap growth, stack growth On allocation — each new page is locked as it is mapped Ensure future allocations are also locked
Both combined All current + all future pages Current pages immediately, future pages on allocation Full real-time process lock

What MCL_CURRENT Locks — Process Address Space Breakdown

Process Virtual Address Space (top = high addr)
Stack (grows downward) ✓ Locked by MCL_CURRENT
mmap() regions (shared libs, file maps) ✓ Locked by MCL_CURRENT
Heap (malloc/new — grows upward) ✓ Locked by MCL_CURRENT
BSS (uninitialized global variables) ✓ Locked by MCL_CURRENT
Data segment (initialized globals) ✓ Locked by MCL_CURRENT
Text segment (program code) ✓ Locked by MCL_CURRENT
Future: new mmap(), heap growth, stack growth ✓ Locked by MCL_FUTURE only

Example 1: Basic mlockall() — Lock Everything Now

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    /* Lock ALL currently mapped pages into RAM.
     * After this returns, no page in this process will cause a page fault. */
    if (mlockall(MCL_CURRENT) == -1) {
        perror("mlockall MCL_CURRENT");
        /* Likely cause: ENOMEM (not enough RAM or RLIMIT_MEMLOCK too low)
         *               EPERM  (need CAP_IPC_LOCK on older kernels)         */
        return 1;
    }
    printf("All current pages locked in RAM\n");

    /* From here on, no page fault can occur for existing data */
    char buf[1024];
    memset(buf, 'A', sizeof(buf));   /* No page fault */
    printf("buf filled: %c\n", buf[0]);

    /* Cleanup */
    munlockall();
    printf("All locks removed\n");
    return 0;
}

Example 2: MCL_FUTURE — Auto-Lock Future Allocations

With MCL_FUTURE, every new page mapped after the call — including malloc(), mmap(), and stack growth — is automatically locked. This is the most comprehensive protection.

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    /* Lock current + all future pages */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall");
        return 1;
    }
    printf("All pages locked (current and future)\n");

    /* This malloc() will automatically be locked — no extra call needed */
    char *heap_buf = malloc(64 * 1024);   /* 64 KB — auto-locked */
    if (!heap_buf) {
        /* malloc CAN fail here if RLIMIT_MEMLOCK is exhausted
         * because MCL_FUTURE means every new page must be locked,
         * and if the limit is hit, the allocation fails */
        perror("malloc failed — RLIMIT_MEMLOCK may be exhausted");
        munlockall();
        return 1;
    }
    memset(heap_buf, 0, 64 * 1024);
    printf("64KB heap buffer auto-locked (no mlock() call needed)\n");

    /* mmap() also auto-locked with MCL_FUTURE */
    void *mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
    } else {
        printf("mmap region also auto-locked\n");
        munmap(mapped, 4096);
    }

    free(heap_buf);
    munlockall();   /* Remove all locks + cancel MCL_FUTURE */
    printf("Cleanup done\n");
    return 0;
}

⚠️ MCL_FUTURE Risk: malloc() and stack growth may fail!

When MCL_FUTURE is active, every new page the process maps must be locked. If the system runs out of RAM or the process hits its RLIMIT_MEMLOCK limit, the kernel cannot honor the lock — so the allocation itself fails. This means:

  • malloc() may return NULL
  • mmap() may return MAP_FAILED
  • Stack growth may trigger SIGSEGV (signal 11) instead of growing normally

Always set RLIMIT_MEMLOCK to a sufficient value and check return values carefully when using MCL_FUTURE.

Example 3: Real-Time Daemon Startup Pattern

This is the standard way real-time Linux applications (JACK audio, Xenomai, robotics daemons) initialize themselves to avoid all page faults during operation:

/* Standard real-time initialization sequence */

#include <sys/mman.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* Pre-fault the stack by writing to each page */
static void prefault_stack(size_t stack_size)
{
    volatile char stack_buf[stack_size];
    /* Touch every page of the stack to bring it into RAM */
    for (size_t i = 0; i < stack_size; i += 4096)
        stack_buf[i] = 0;
}

/* Pre-fault a heap region */
static void prefault_heap(size_t heap_size)
{
    char *buf = malloc(heap_size);
    if (buf) {
        memset(buf, 0, heap_size);   /* Touch every page */
        free(buf);
    }
}

int init_realtime(void)
{
    struct rlimit rl;

    /* Step 1: Raise RLIMIT_MEMLOCK to unlimited (requires root) */
    rl.rlim_cur = RLIM_INFINITY;
    rl.rlim_max = RLIM_INFINITY;
    if (setrlimit(RLIMIT_MEMLOCK, &rl) == -1) {
        perror("setrlimit RLIMIT_MEMLOCK — run as root");
        return -1;
    }

    /* Step 2: Lock all current + future pages */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall");
        return -1;
    }

    /* Step 3: Pre-fault the stack (force stack pages into RAM now) */
    prefault_stack(8 * 1024 * 1024);   /* Pre-fault 8MB of stack */

    /* Step 4: Pre-fault expected heap usage */
    prefault_heap(64 * 1024 * 1024);   /* Pre-fault 64MB of heap */

    printf("Real-time init complete — no page faults expected\n");
    return 0;
}

int main(void)
{
    if (init_realtime() != 0) {
        fprintf(stderr, "Real-time init failed\n");
        return 1;
    }

    /* Real-time loop — zero page fault latency */
    for (int i = 0; i < 5; i++) {
        /* Simulate real-time task */
        printf("RT cycle %d — guaranteed no page fault\n", i);
        usleep(1000);   /* 1ms period */
    }

    munlockall();
    return 0;
}

Example 4: Verify mlockall() Effect via /proc/self/status

#include <sys/mman.h>
#include <stdio.h>
#include <string.h>

/* Print VmRSS (resident) and VmLck (locked) from /proc/self/status */
static void print_vm_stats(const char *label)
{
    FILE *fp = fopen("/proc/self/status", "r");
    if (!fp) return;

    char line[256];
    char vmrss[64] = "?", vmlck[64] = "?";

    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "VmRSS:", 6) == 0)
            sscanf(line + 6, "%s", vmrss);
        if (strncmp(line, "VmLck:", 6) == 0)
            sscanf(line + 6, "%s", vmlck);
    }
    fclose(fp);

    printf("[%s] VmRSS: %s kB  VmLck: %s kB\n", label, vmrss, vmlck);
}

int main(void)
{
    print_vm_stats("Before mlockall");

    if (mlockall(MCL_CURRENT) == -1) {
        perror("mlockall");
        return 1;
    }

    print_vm_stats("After  mlockall");
    /* VmLck should now equal (or be close to) VmRSS */

    munlockall();
    print_vm_stats("After  munlockall");
    /* VmLck drops back to 0 */

    return 0;
}

/* Sample output:
 * [Before mlockall] VmRSS: 1280 kB  VmLck:    0 kB
 * [After  mlockall] VmRSS: 1280 kB  VmLck: 1280 kB
 * [After  munlockall] VmRSS: 1280 kB  VmLck:    0 kB
 */

munlockall() — Remove All Locks

munlockall() does two things in one call:

Action Effect
Removes all existing memory locks All currently locked pages become eligible for eviction
Cancels MCL_FUTURE effect Future allocations will no longer be auto-locked
Kernel version note: Before Linux 2.6.9, calling munlockall() required CAP_IPC_LOCK privilege. Since Linux 2.6.9 this requirement was removed — any process can call munlockall() to remove its own locks. (Interestingly, munlock() never required privilege, making this an inconsistency that was later fixed.)

MCL_CURRENT vs MCL_FUTURE — Side-by-Side Comparison

Scenario MCL_CURRENT alone MCL_FUTURE alone MCL_CURRENT | MCL_FUTURE
Existing stack pages ✓ Locked ✗ Not locked ✓ Locked
Existing heap pages ✓ Locked ✗ Not locked ✓ Locked
Future malloc() pages ✗ Not locked ✓ Auto-locked ✓ Auto-locked
Future mmap() regions ✗ Not locked ✓ Auto-locked ✓ Auto-locked
Future stack growth ✗ Not locked ✓ Auto-locked ✓ Auto-locked
malloc() may fail? No extra risk Yes — if limit hit Yes — if limit hit

Example 5: Choosing Between mlock() and mlockall()

/*
 * Use mlock() when:
 *   - You have specific known buffers to protect (audio ring buffer, key buffer)
 *   - You want minimal RAM footprint (only lock what you need)
 *   - RLIMIT_MEMLOCK is constrained
 *
 * Use mlockall() when:
 *   - You want zero-latency for the ENTIRE process
 *   - You cannot track all allocations individually
 *   - You are writing a real-time daemon or time-critical service
 */

#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Approach A: Selective locking with mlock() */
void approach_a_selective(void)
{
    char *audio_buf, *key_buf;
    posix_memalign((void **)&audio_buf, 4096, 64 * 1024);
    posix_memalign((void **)&key_buf,   4096, 4096);

    memset(audio_buf, 0, 64 * 1024);
    memset(key_buf,   0, 4096);

    mlock(audio_buf, 64 * 1024);   /* Lock only audio buffer */
    mlock(key_buf, 4096);          /* Lock only key buffer */

    printf("Selective: only 65KB locked, rest of process can swap\n");

    munlock(audio_buf, 64 * 1024);
    munlock(key_buf, 4096);
    free(audio_buf);
    free(key_buf);
}

/* Approach B: Full lock with mlockall() */
void approach_b_full(void)
{
    mlockall(MCL_CURRENT | MCL_FUTURE);

    /* Everything from here on is locked — no tracking needed */
    char *buf1 = malloc(16 * 1024);   /* auto-locked */
    char *buf2 = malloc(32 * 1024);   /* auto-locked */
    memset(buf1, 0, 16 * 1024);
    memset(buf2, 0, 32 * 1024);

    printf("Full lock: entire process address space locked\n");

    free(buf1);
    free(buf2);
    munlockall();
}

int main(void)
{
    printf("--- Approach A: Selective mlock() ---\n");
    approach_a_selective();

    printf("--- Approach B: Full mlockall() ---\n");
    approach_b_full();

    return 0;
}

Same Rules Apply as mlock() — Recap

Rule Applies to mlockall()? Detail
Not inherited by fork() Yes Child process has no locks
Not preserved across exec() Yes New program image starts with no locks
Shared pages stay locked Yes Pages shared via MAP_SHARED stay locked while any holder has a lock
RLIMIT_MEMLOCK applies Yes Total locked memory cannot exceed soft limit (unless CAP_IPC_LOCK)
munmap() removes lock Yes Unmapping a region also removes its lock

Interview Questions and Answers

Q1: What is the difference between mlock() and mlockall()?

mlock() locks a specific address range you specify. mlockall() locks the entire process address space — all segments (text, data, BSS, stack, heap, all mappings) in one call. mlockall() is more convenient for real-time processes that want to eliminate all page faults without tracking individual allocations.

Q2: What is MCL_CURRENT and when do you use it?

MCL_CURRENT locks all pages that are currently mapped in the process’s virtual address space. All these pages are guaranteed to be in RAM before mlockall() returns. Use it when you want to immediately eliminate page faults for all existing memory — code, data, stack, heap, and libraries.

Q3: What is MCL_FUTURE and what are its risks?

MCL_FUTURE causes all pages mapped after the mlockall() call to be automatically locked. This includes future malloc(), mmap(), and stack growth. The risk is that if the system runs out of RAM or RLIMIT_MEMLOCK is exceeded, these allocations will FAIL — malloc() returns NULL, mmap() returns MAP_FAILED, and stack growth may cause SIGSEGV.

Q4: What does munlockall() do beyond just unlocking pages?

munlockall() does two things: it removes all existing memory locks from the process, AND it cancels the effect of any previous mlockall(MCL_FUTURE) call. After munlockall(), future allocations will no longer be automatically locked.

Q5: Why is pre-faulting the stack important when using mlockall(MCL_CURRENT)?

mlockall(MCL_CURRENT) locks only the pages currently mapped — including the currently-used portion of the stack. But the stack grows dynamically as functions are called. Pages for deeper function calls are not yet mapped, so they are not locked. Pre-faulting the stack (by accessing stack memory in a dummy function to a large depth) forces the kernel to map those stack pages, so they get included in the MCL_CURRENT lock. Combined with MCL_FUTURE, further stack growth is also automatically locked.

Q6: What privilege is required to call mlockall()? Has this changed across kernel versions?

Historically, mlockall() required CAP_IPC_LOCK (root-equivalent capability). Since Linux 2.6.9, unprivileged processes can call mlockall() but are still subject to RLIMIT_MEMLOCK. CAP_IPC_LOCK bypasses RLIMIT_MEMLOCK and allows locking unlimited memory. Interestingly, before Linux 2.6.9, munlockall() also required CAP_IPC_LOCK (even though munlock() did not) — this inconsistency was corrected in 2.6.9.

Q7: Name a real-world scenario where mlockall(MCL_CURRENT | MCL_FUTURE) is used.

Real-time audio servers like JACK Audio Connection Kit call mlockall(MCL_CURRENT | MCL_FUTURE) at startup along with pre-faulting the stack. This guarantees that the audio callback thread never experiences a page fault — which would cause an audible glitch. Similarly, industrial robot controllers and automotive ECU software running on Linux RT-PREEMPT use this pattern to meet hard real-time timing requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *