mlock() and munlock() Lock Specific Memory Regions in RAM

 

mlock() and munlock()
Chapter 50 · File 1 of 3 — Lock Specific Memory Regions in RAM

Why Would You Lock Memory?

Linux uses virtual memory. When your program allocates memory or maps a file, the kernel does not immediately load everything into RAM. Pages are loaded on demand — only when your code actually touches them. When RAM gets full, the kernel can swap pages out to disk.

When you next access a swapped-out page, the CPU raises a page fault, the kernel fetches the page from disk, and your program resumes. This fetch can take milliseconds. For most programs this is fine. But for:

  • Real-time audio/video — a millisecond delay causes glitches
  • Real-time control systems — delayed responses can be dangerous
  • Cryptographic code — private keys must never touch disk (swap file can be read)
  • High-frequency trading — microsecond latency matters

Memory locking tells the kernel: keep these pages in RAM always — do not swap them out.

How Pages Work — Visual Overview

Virtual Memory (Process View) Page State Physical RAM On Page Fault?
0x7fff0000 (stack page) Resident Frame #42 No fault needed
0x7ffe0000 (heap page) Swapped Out (on disk swap) Fault → disk read → slow!
0x7ffd0000 (locked page) Locked/Resident Frame #77 (pinned) Never swapped out

The mlock() and munlock() System Calls

#include <sys/mman.h>

int mlock(void *addr, size_t length);
int munlock(void *addr, size_t length);

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

mlock(addr, length) — Locks all pages that overlap the virtual address range from addr to addr + length - 1 into physical RAM.

munlock(addr, length) — Removes the lock. Pages may remain in RAM but are now eligible for eviction by the kernel.

Page Alignment Rules (Very Important!)

Locking happens at whole-page granularity. The kernel does not lock partial pages — it locks entire pages. Here is exactly how the locked range is calculated:

Parameter Rule Applied Example (page = 4096)
addr Rounded down to page boundary addr=2000 → starts at page 0
addr + length Rounded up to page boundary 2000+4000=6000 → ends at page 8191
Locked bytes 2 full pages locked bytes 0..8191 (two 4096-byte pages)

/* On a system with page size = 4096 bytes:
 *
 *   mlock(2000, 4000)
 *
 *   addr=2000 falls in page 0 (0..4095)   → page 0 is locked
 *   addr+len = 6000 falls in page 1 (4096..8191) → page 1 is locked
 *
 *   Result: bytes 0 to 8191 are locked (2 pages)
 */

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

int main(void)
{
    long page_size = sysconf(_SC_PAGESIZE);
    printf("System page size: %ld bytes\n", page_size);

    /* Calculate pages locked by mlock(2000, 4000) */
    size_t addr   = 2000;
    size_t length = 4000;
    size_t lock_start = (addr / page_size) * page_size;
    size_t lock_end   = ((addr + length + page_size - 1) / page_size) * page_size;

    printf("mlock(%zu, %zu) will lock bytes %zu to %zu\n",
           addr, length, lock_start, lock_end - 1);
    printf("Pages locked: %zu\n", (lock_end - lock_start) / page_size);
    return 0;
}

Example 1: Basic mlock() Usage

Lock a buffer in RAM so it is never swapped out:

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

#define BUFFER_SIZE (4 * 4096)   /* 4 pages = 16384 bytes */

int main(void)
{
    char *buf;

    /* Allocate aligned buffer */
    if (posix_memalign((void **)&buf, 4096, BUFFER_SIZE) != 0) {
        perror("posix_memalign");
        return 1;
    }

    /* Touch pages first — bring them into RAM */
    memset(buf, 0, BUFFER_SIZE);

    /* Lock buffer in RAM */
    if (mlock(buf, BUFFER_SIZE) == -1) {
        perror("mlock");
        /* Common reasons:
         * ENOMEM: not enough RAM or RLIMIT_MEMLOCK exceeded
         * EPERM:  process lacks CAP_IPC_LOCK (before Linux 2.6.9)
         */
        free(buf);
        return 1;
    }

    printf("Buffer locked in RAM (%d bytes)\n", BUFFER_SIZE);

    /* Do real-time work here — no page faults possible */
    buf[0] = 'A';
    buf[BUFFER_SIZE - 1] = 'Z';
    printf("First: %c, Last: %c\n", buf[0], buf[BUFFER_SIZE - 1]);

    /* Unlock when done */
    if (munlock(buf, BUFFER_SIZE) == -1) {
        perror("munlock");
    }
    printf("Buffer unlocked\n");

    free(buf);
    return 0;
}

Example 2: Protecting Cryptographic Keys

Private keys must never be written to swap. Use mlock() to keep them in RAM only, and explicitly zero them before unlocking:

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

#define KEY_SIZE 32   /* 256-bit key */

int main(void)
{
    unsigned char *key;

    /* Allocate page-aligned key buffer */
    if (posix_memalign((void **)&key, 4096, KEY_SIZE) != 0) {
        perror("posix_memalign");
        return 1;
    }

    /* Lock the key page in RAM BEFORE loading the key */
    if (mlock(key, KEY_SIZE) == -1) {
        perror("mlock — key will NOT be protected from swap!");
        /* You may choose to abort here in a real application */
    }

    /* Load the secret key (simulated here) */
    memcpy(key, "SUPER_SECRET_KEY_32_BYTES_HERE!!", KEY_SIZE);
    printf("Key loaded and locked in RAM\n");

    /* ... use the key for encryption ... */
    printf("Using key for crypto: %02x %02x ...\n", key[0], key[1]);

    /* IMPORTANT: Zero the key before freeing */
    memset(key, 0, KEY_SIZE);   /* Clear sensitive data */
    munlock(key, KEY_SIZE);     /* Unlock */
    free(key);

    printf("Key wiped and memory released\n");
    return 0;
}

Checking Locked Memory via /proc

You can check how much memory your process has locked at any time by reading /proc/PID/status and looking for the VmLck field:

/* Read locked memory size from /proc/self/status */
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

void print_locked_memory(void)
{
    FILE *fp = fopen("/proc/self/status", "r");
    if (!fp) return;

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "VmLck:", 6) == 0) {
            printf("Locked memory: %s", line + 6);
            break;
        }
    }
    fclose(fp);
}

int main(void)
{
    char *buf;
    posix_memalign((void **)&buf, 4096, 4 * 4096);

    print_locked_memory();   /* Before lock */

    mlock(buf, 4 * 4096);
    print_locked_memory();   /* After lock: VmLck increases */

    munlock(buf, 4 * 4096);
    print_locked_memory();   /* After unlock: VmLck decreases */

    free(buf);
    return 0;
}

/* Sample output:
 * Locked memory:       0 kB
 * Locked memory:      16 kB
 * Locked memory:       0 kB
 */

When Are Locks Removed Automatically?

You do not always need to call munlock() explicitly. Memory locks are removed automatically when:

# Event What Happens
1 Process terminates All locks for the process are freed
2 munmap() on locked pages Unmapping removes the lock too
3 mmap() with MAP_FIXED overlays locked pages Old lock is gone, new mapping starts unlocked

Memory Locking Semantics — The Important Rules

Rule 1: Locks are NOT inherited by child processes (fork)

When you call fork(), the child gets a copy of the parent’s address space, but NOT the memory locks. The child’s pages are not locked. If the child needs locking, it must call mlock() itself.

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

int main(void)
{
    char *buf;
    posix_memalign((void **)&buf, 4096, 4096);
    memset(buf, 0, 4096);

    mlock(buf, 4096);
    printf("Parent: memory locked\n");

    pid_t pid = fork();
    if (pid == 0) {
        /* Child process — lock NOT inherited */
        printf("Child: memory is NOT locked (fork does not inherit locks)\n");
        /* Child must call mlock() again if it needs locking */
        /* mlock(buf, 4096); */
        free(buf);
        return 0;
    }

    /* Parent continues with lock intact */
    printf("Parent: lock still held after fork\n");
    munlock(buf, 4096);
    free(buf);
    return 0;
}

Rule 2: Memory locks do NOT nest

Calling mlock() on the same address range multiple times does NOT create multiple locks. Only ONE lock exists. A single munlock() removes it completely. This is a common source of bugs.

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

int main(void)
{
    char *page;
    posix_memalign((void **)&page, 4096, 4096);
    memset(page, 0, 4096);

    /* Imagine two data structures on the SAME page */
    char *p1 = page;          /* first structure */
    char *p2 = page + 512;    /* second structure (same page!) */

    mlock(p1, 100);   /* Locks page 0 */
    mlock(p2, 100);   /* Same page — no new lock added */

    /* BUG: This unlocks the ENTIRE page including p2's data! */
    munlock(p1, 100);

    /* p2's data is now UNLOCKED even though we never called munlock(p2) */
    printf("p2 data is no longer locked! (non-nesting semantics)\n");

    /* Correct approach: avoid overlapping locks on the same page,
     * or use one mlock() call for the entire region */
    free(page);
    return 0;
}

Rule 3: Shared pages remain locked as long as ANY process holds a lock

If multiple processes share pages (e.g., via MAP_SHARED mmap), those pages stay locked in RAM as long as at least one process has a lock on them.

Rule 4: Locks are NOT preserved across exec()

When a process calls exec(), the address space is replaced with the new program. All memory locks are lost. The new program starts with no locks.

mlock() vs SHM_LOCK — Key Differences

Aspect mlock() / mlockall() shmctl() SHM_LOCK
When pages are loaded All pages loaded into RAM immediately when mlock() returns Pages loaded lazily — only when first accessed (faulted in)
Property of The process (per-process lock) The shared memory segment (global property)
Pages stay if all detach? No — pages unlocked when last process releases lock Yes — pages stay locked even after all processes detach
Shows in VmLck? Yes — reflected in /proc/PID/status VmLck No — NOT included in VmLck field

Example 3: Real-Time Audio Buffer Locking Pattern

/* Pattern used by real-time audio applications (like JACK audio)
 * to prevent page faults during audio callback */

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

#define AUDIO_BUF_FRAMES  1024
#define SAMPLE_SIZE       4      /* 32-bit float */
#define CHANNELS          2

int main(void)
{
    size_t buf_size = AUDIO_BUF_FRAMES * SAMPLE_SIZE * CHANNELS;
    float *audio_buf;

    /* Allocate page-aligned audio buffer */
    if (posix_memalign((void **)&audio_buf, 4096, buf_size) != 0) {
        perror("posix_memalign");
        return 1;
    }

    /* Pre-fault: touch every page to bring it into RAM */
    memset(audio_buf, 0, buf_size);

    /* Lock the audio buffer — no page faults during playback */
    if (mlock(audio_buf, buf_size) == -1) {
        perror("mlock — audio may glitch under memory pressure");
        /* Continue anyway — locking is an optimisation here */
    } else {
        printf("Audio buffer locked: %zu bytes\n", buf_size);
    }

    /* Simulate audio callback — no page faults possible */
    for (int frame = 0; frame < AUDIO_BUF_FRAMES; frame++) {
        audio_buf[frame * CHANNELS + 0] = 0.5f;   /* Left channel */
        audio_buf[frame * CHANNELS + 1] = 0.5f;   /* Right channel */
    }
    printf("Audio frames written without page faults\n");

    /* Cleanup */
    munlock(audio_buf, buf_size);
    free(audio_buf);
    return 0;
}

RLIMIT_MEMLOCK — Limit on Locked Memory

By default, unprivileged processes can only lock a limited amount of memory. This is controlled by the RLIMIT_MEMLOCK resource limit. If you try to lock more than allowed, mlock() fails with ENOMEM.

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

int main(void)
{
    struct rlimit rl;

    /* Check current RLIMIT_MEMLOCK */
    getrlimit(RLIMIT_MEMLOCK, &rl);
    printf("RLIMIT_MEMLOCK soft: %lu bytes\n", (unsigned long)rl.rlim_cur);
    printf("RLIMIT_MEMLOCK hard: %lu bytes\n", (unsigned long)rl.rlim_max);

    /* Raise the limit (requires root or CAP_IPC_LOCK for hard limit) */
    rl.rlim_cur = 64 * 1024 * 1024;   /* 64 MB soft limit */
    if (setrlimit(RLIMIT_MEMLOCK, &rl) == -1) {
        perror("setrlimit RLIMIT_MEMLOCK");
        printf("Note: raising limit requires CAP_IPC_LOCK\n");
    } else {
        printf("Limit raised to 64 MB\n");
    }

    return 0;
}

/* From shell, check limits with:
 *   ulimit -l           # shows RLIMIT_MEMLOCK in kB
 *   ulimit -l unlimited # raise to unlimited (as root)
 */

Interview Questions and Answers

Q1: What does mlock() do and why would you use it?

mlock() pins a range of virtual memory pages into physical RAM, preventing the kernel from swapping them to disk. You use it when page faults are unacceptable — for example in real-time audio, low-latency networking, cryptographic key storage, or hard real-time control systems.

Q2: Does addr need to be page-aligned for mlock()?

No — the kernel accepts any addr and automatically rounds it down to the page boundary. However, SUSv3 permits implementations to require page alignment, so for portable code you should always pass a page-aligned addr. Use posix_memalign() or align manually using the page size from sysconf(_SC_PAGESIZE).

Q3: Are memory locks inherited by a child process after fork()?

No. Memory locks are NOT inherited across fork(). The child process starts with an unlocked copy of the parent’s address space. Similarly, locks are NOT preserved across exec() since the entire address space is replaced.

Q4: What happens if you call mlock() twice on the same region?

Memory locks do not nest. Calling mlock() multiple times on the same virtual address range still results in only ONE lock. A single munlock() call on that range will completely remove the lock — even if mlock() was called multiple times. This is a common pitfall when two data structures on the same page are independently locked.

Q5: When does munlock() guarantee pages are removed from RAM?

It does NOT. munlock() makes pages eligible for eviction by the kernel, but the kernel only actually removes them from RAM when it needs the memory for another process. The pages may stay resident for a long time after munlock().

Q6: What is the difference between mlock() and shmctl() SHM_LOCK?

mlock() immediately loads all pages into RAM and is a per-process property — pages are unlocked when the last locking process releases them. SHM_LOCK is a property of the shared memory segment — pages are loaded lazily (on first access) and remain locked even after all processes detach the segment. Also, SHM_LOCK-locked pages are NOT reflected in the VmLck field of /proc/PID/status.

Q7: How do you check how much memory a process has currently locked?

Read the VmLck field from /proc/PID/status (or /proc/self/status for the current process). Note that this does NOT include memory locked via shmctl() SHM_LOCK — only mlock()/mlockall() locks are shown.

Q8: What is RLIMIT_MEMLOCK and who does it apply to?

RLIMIT_MEMLOCK is a per-process resource limit that caps the total number of bytes that an unprivileged process can lock into RAM. If a process tries to lock more than this limit, mlock() fails with ENOMEM. Privileged processes (with CAP_IPC_LOCK capability) are not subject to this limit and can lock unlimited memory.

Leave a Reply

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