好的,我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 mlock2


1. 函数介绍 链接到标题

mlock2 是一个 Linux 系统调用(内核版本 >= 4.4),它是 mlock 函数的扩展版本。它的核心功能是锁定调用进程的虚拟内存页面,防止这些页面被操作系统交换(swap)到磁盘上。

你可以把它想象成一个保险柜

  • 你有一些非常重要的文件(内存中的数据)。
  • 你担心这些文件会被别人(操作系统)放到一个不太安全的地方(磁盘交换空间)。
  • 你使用 mlock2 就像是把这些文件锁进保险柜(物理内存)里。
  • 这样,即使系统内存紧张,操作系统也不能把这些文件(内存页)移到保险柜外面(交换出去),确保它们始终在保险柜(物理内存)里,随时可以快速访问。

这对于需要极低延迟高安全性实时性的应用程序至关重要,例如:

  • 实时音频/视频处理: 避免因页面换入换出导致的音频爆音或视频卡顿。
  • 加密软件: 防止敏感的加密密钥被写入磁盘(即使是交换空间),提高安全性。
  • 数据库系统: 将热数据(频繁访问的数据)锁定在内存中,提高查询性能。
  • 高性能计算: 确保关键数据结构始终在物理内存中,避免性能抖动。

2. 函数原型 链接到标题

#define _GNU_SOURCE // 必须定义以使用 mlock2
#include <sys/mman.h> // 必需

int mlock2(const void *addr, size_t len, unsigned int flags);

3. 功能 链接到标题

  • 锁定内存页: 将从地址 addr 开始、长度为 len 字节的内存区域所对应的物理内存页面锁定在 RAM 中。
  • 防止交换: 确保这些被锁定的页面不会被内核的内存管理器换出到交换空间(swap space)。
  • 扩展功能: 相比于 mlockmlock2 通过 flags 参数提供了额外的控制选项。

4. 参数 链接到标题

  • const void *addr: 指向要锁定的内存区域的起始虚拟地址
    • 重要: 虽然 POSIX 允许 addr 不是页对齐的,但为了可移植性和效率,强烈建议 addr 是系统页大小的整数倍。可以使用 getpagesize() 获取页大小。
  • size_t len: 要锁定的内存区域的长度(以字节为单位)。
    • 同样,为了效率,len 最好也是页大小的整数倍。如果不足一页,内核通常会向上舍入到最近的页边界。
  • unsigned int flags: 指定锁定行为的标志位。这是一个位掩码,可以是以下值的按位或组合:
    • 0: 默认行为,等同于调用 mlock(addr, len)。锁定指定区域的页面。
    • MLOCK_ONFAULT: **按需锁定 **(Lock on Fault)。这是 mlock2 相比 mlock 的主要新增功能。
      • 使用此标志时,mlock2 不会立即将所有指定页面加载到物理内存并锁定它们。
      • 相反,它会标记这些页面,使得当进程首次访问(触发缺页中断)这些范围内的任一页时,内核会自动将该页加载到物理内存并立即锁定它。
      • 这避免了 mlock 可能引起的主内存不足(Out-of-Memory, OOM)问题,因为 mlock 会立即尝试将所有页面加载到内存中。

5. 返回值 链接到标题

  • 成功时: 返回 0。指定的内存区域(或将在按需锁定模式下访问的页面)已被成功标记为锁定。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 ENOMEM 内存不足或超出锁定限制,EPERM 权限不足,EINVAL 参数无效等)。

6. 相似函数,或关联函数 链接到标题

  • mlock: mlock(addr, len) 在功能上等同于 mlock2(addr, len, 0)。它是 mlock2 的前身。
  • munlock: 用于解锁之前通过 mlockmlock2 锁定的内存页面。
  • munlockall: 一次性解锁调用进程锁定的所有内存页面。
  • mlockall: 锁定调用进程所有的内存页面(当前已映射的和未来将映射的)。
  • getrlimit / setrlimit: 用于查询和设置进程的内存锁定限制RLIMIT_MEMLOCK)。每个进程能锁定的内存总量是有限制的,默认值通常很小(如 64KB),需要 root 权限或调整 ulimit 才能增加。
  • mincore: 可以用来检查指定的内存页面是否当前驻留在物理内存(RAM)中。

7. 示例代码 链接到标题

示例 1:基本的 mlockmlock2 对比 链接到标题

这个例子演示了 mlockmlock2 (带 MLOCK_ONFAULT 标志) 的基本用法和区别。

// mlock2_basic_example.c
#define _GNU_SOURCE // 必需
#include <sys/mman.h> // mlock, mlock2, munlock, mmap, munmap
#include <unistd.h>   // getpagesize, sysconf
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit, malloc, free
#include <string.h>   // memset
#include <errno.h>    // errno

#define ALLOC_SIZE (10 * 1024 * 1024) // 10 MB

void print_lock_limit() {
    struct rlimit rl;
    if (getrlimit(RLIMIT_MEMLOCK, &rl) == 0) {
        printf("Current MEMLOCK limit: soft=%ld, hard=%ld bytes\n",
               (long)rl.rlim_cur, (long)rl.rlim_max);
        if (rl.rlim_cur == RLIM_INFINITY) {
            printf("  Soft limit is unlimited.\n");
        }
        if (rl.rlim_max == RLIM_INFINITY) {
            printf("  Hard limit is unlimited.\n");
        }
    } else {
        perror("getrlimit RLIMIT_MEMLOCK");
    }
}

int main() {
    char *buffer;
    long pagesize = getpagesize();
    size_t len = ALLOC_SIZE;

    printf("Page size: %ld bytes\n", pagesize);
    print_lock_limit();

    // 1. 分配内存 (使用 malloc)
    buffer = malloc(len);
    if (buffer == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }
    printf("Allocated %zu bytes using malloc at %p\n", len, (void*)buffer);

    // 2. 初始化内存内容 (确保内核分配物理页)
    printf("Initializing memory content...\n");
    memset(buffer, 0xAA, len); // 填充特定值
    printf("Memory initialized.\n");

    // --- 使用 mlock 锁定 ---
    printf("\n--- Locking memory with mlock (immediate loading) ---\n");
    if (mlock(buffer, len) == -1) {
        perror("mlock failed");
        if (errno == ENOMEM) {
            printf("  Reason: Out of memory or exceeded memlock limit.\n");
            printf("  Consider running with 'ulimit -l <size>' or as root.\n");
        }
        free(buffer);
        exit(EXIT_FAILURE);
    }
    printf("Memory successfully locked with mlock.\n");

    // 检查内存是否在物理内存中 (概念性,实际需要 mincore)
    // 这里假设 mlock 成功后,页面就在物理内存中

    // 3. 使用内存 (模拟访问)
    printf("Accessing locked memory...\n");
    for (size_t i = 0; i < len; i += pagesize) {
        buffer[i] = (char)(i % 256); // 访问每个页面
    }
    printf("Memory accessed.\n");

    // 4. 解锁
    printf("Unlocking memory with munlock...\n");
    if (munlock(buffer, len) == -1) {
        perror("munlock failed");
    } else {
        printf("Memory unlocked successfully.\n");
    }

    // --- 使用 mlock2 with MLOCK_ONFAULT ---
    printf("\n--- Locking memory with mlock2(MLOCK_ONFAULT) ---\n");
    // 重新初始化内存内容
    memset(buffer, 0xBB, len);
    printf("Memory re-initialized.\n");

    if (mlock2(buffer, len, MLOCK_ONFAULT) == -1) {
        perror("mlock2 with MLOCK_ONFAULT failed");
        if (errno == ENOSYS) {
            printf("  Reason: mlock2 is not supported on this kernel (need >= 4.4).\n");
        } else if (errno == ENOMEM) {
            printf("  Reason: Out of memory or exceeded memlock limit.\n");
        }
        free(buffer);
        exit(EXIT_FAILURE);
    }
    printf("Memory successfully locked with mlock2(MLOCK_ONFAULT).\n");
    printf("Pages are NOT loaded into memory yet. They will be loaded and locked on first access.\n");

    // 5. 访问内存 (此时页面才会被按需加载和锁定)
    printf("Accessing memory (pages will be loaded and locked on fault)...\n");
    for (size_t i = 0; i < len; i += pagesize) {
        // 第一次访问每个页面时,会触发缺页中断,内核加载页面并锁定
        volatile char temp = buffer[i]; // volatile 防止编译器优化
        buffer[i] = (char)((i + 1) % 256);
    }
    printf("Memory accessed. Pages should now be loaded and locked.\n");

    // 6. 再次解锁
    printf("Unlocking memory with munlock...\n");
    if (munlock(buffer, len) == -1) {
        perror("munlock failed (second time)");
    } else {
        printf("Memory unlocked successfully (second time).\n");
    }

    // 7. 清理
    free(buffer);
    printf("\nAll operations completed and memory freed.\n");

    return 0;
}

代码解释:

  1. 定义了内存大小 ALLOC_SIZE (10MB) 和 print_lock_limit 函数来打印当前进程的内存锁定限制。
  2. 使用 malloc 分配一大块内存。
  3. 使用 memset 初始化内存内容,这通常会触发内核分配物理页面。
  4. 使用 mlock 锁定:
    • 调用 mlock(buffer, len)
    • mlock 会尝试立即buffer 指向的内存区域的所有页面加载到物理内存中并锁定。
    • 如果系统内存不足或超出 RLIMIT_MEMLOCK 限制,mlock 会失败(返回 -1,errno = ENOMEM)。
  5. 访问已锁定的内存。
  6. 调用 munlock 解锁内存。
  7. 使用 mlock2 with MLOCK_ONFAULT 锁定:
    • 再次初始化内存。
    • 调用 mlock2(buffer, len, MLOCK_ONFAULT)
    • mlock2 不会立即加载或锁定页面。它只是标记了这些页面,告诉内核:“当这个范围内的页面被访问时,请加载并锁定它”。
    • 如果内核不支持 mlock2,调用会失败(errno = ENOSYS)。
  8. 访问内存:
    • 当程序第一次访问 buffer 中的某个页面时,会触发缺页中断(Page Fault)。
    • 内核处理这个中断,将该页面从后备存储(如文件或匿名页)加载到物理内存。
    • 由于该页面之前被 mlock2 标记过,内核在加载后会立即锁定该页面。
    • 程序继续执行。
  9. 再次调用 munlock 解锁。
  10. 释放 malloc 分配的内存。

示例 2:使用 mmapmlock2 锁定大文件 链接到标题

这个例子演示了如何使用 mmap 将一个大文件映射到内存,然后使用 mlock2 锁定映射区域。

// mlock2_mmap_example.c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define FILE_SIZE (50 * 1024 * 1024) // 50 MB
#define FILE_NAME "large_file_for_mlock2.txt"

int main() {
    int fd;
    char *mapped;
    struct stat sb;
    long pagesize = getpagesize();

    printf("Page size: %ld bytes\n", pagesize);

    // 1. 创建一个大文件
    printf("Creating a large file '%s' of size %d MB...\n", FILE_NAME, FILE_SIZE / (1024*1024));
    fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open for creation");
        exit(EXIT_FAILURE);
    }

    // 使用 ftruncate 快速创建指定大小的文件
    if (ftruncate(fd, FILE_SIZE) == -1) {
        perror("ftruncate");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 可选:写入一些数据
    const char *data = "Initial data in the file.\n";
    if (write(fd, data, strlen(data)) == -1) {
        perror("write initial data");
        close(fd);
        exit(EXIT_FAILURE);
    }

    if (close(fd) == -1) {
        perror("close after creation");
        exit(EXIT_FAILURE);
    }
    printf("File '%s' created successfully.\n", FILE_NAME);

    // 2. 以只读方式打开文件并映射
    fd = open(FILE_NAME, O_RDONLY);
    if (fd == -1) {
        perror("open for reading");
        exit(EXIT_FAILURE);
    }

    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("Mapping file '%s' (size: %ld bytes) into memory...\n", FILE_NAME, (long)sb.st_size);

    mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("File mapped successfully at %p.\n", (void*)mapped);

    // 3. 使用 mlock2 锁定映射区域 (按需锁定)
    printf("\nLocking mapped memory with mlock2(MLOCK_ONFAULT)...\n");
    if (mlock2(mapped, sb.st_size, MLOCK_ONFAULT) == -1) {
        perror("mlock2 MLOCK_ONFAULT failed");
        if (errno == ENOSYS) {
            printf("  mlock2 not supported, trying mlock...\n");
            if (mlock(mapped, sb.st_size) == -1) {
                perror("mlock fallback also failed");
                munmap(mapped, sb.st_size);
                close(fd);
                exit(EXIT_FAILURE);
            } else {
                printf("  Fallback to mlock successful.\n");
            }
        } else {
            munmap(mapped, sb.st_size);
            close(fd);
            exit(EXIT_FAILURE);
        }
    } else {
        printf("Mapped memory locked with mlock2(MLOCK_ONFAULT) successfully.\n");
        printf("Pages will be loaded and locked as they are accessed.\n");
    }

    // 4. 访问映射区域的一部分 (触发按需锁定)
    printf("\nAccessing first few pages of the mapped memory...\n");
    size_t access_size = 10 * pagesize; // 访问前 10 页
    if (access_size > (size_t)sb.st_size) access_size = sb.st_size;

    for (size_t i = 0; i < access_size; i += pagesize) {
        volatile char temp = mapped[i]; // 触发缺页并按需锁定
        printf("  Accessed page at offset %zu (address %p)\n", i, (void*)(mapped + i));
    }
    printf("Accessed %zu bytes. Corresponding pages should now be locked.\n", access_size);

    // 5. 检查内存是否在物理内存中 (使用 mincore,概念性)
    printf("\nChecking page residency with mincore (conceptual)...\n");
    // 实际使用 mincore 需要分配一个足够大的向量,这里简化
    // unsigned char *vec = malloc((sb.st_size + pagesize - 1) / pagesize);
    // if (mincore(mapped, sb.st_size, vec) == 0) {
    //     for (int i = 0; i < 10; ++i) { // 检查前 10 页
    //         if (vec[i] & 1) {
    //             printf("  Page %d: IN MEMORY\n", i);
    //         } else {
    //             printf("  Page %d: NOT in memory\n", i);
    //         }
    //     }
    // }
    // free(vec);
    printf("  [Concept] Would use mincore to check if pages are in physical memory.\n");

    // 6. 解锁
    printf("\nUnlocking mapped memory...\n");
    if (munlock(mapped, sb.st_size) == -1) {
        perror("munlock failed");
    } else {
        printf("Mapped memory unlocked successfully.\n");
    }

    // 7. 清理
    if (munmap(mapped, sb.st_size) == -1) {
        perror("munmap");
    } else {
        printf("Memory unmapped.\n");
    }

    if (close(fd) == -1) {
        perror("close");
    } else {
        printf("File descriptor closed.\n");
    }

    // 可选:删除测试文件
    // unlink(FILE_NAME);

    printf("\nMmap and mlock2 example completed.\n");
    return 0;
}

代码解释:

  1. 创建一个 50MB 的大文件 large_file_for_mlock2.txt
  2. 使用 ftruncate 快速创建指定大小的稀疏文件。
  3. 以只读模式打开文件。
  4. 使用 mmap 将整个文件以 MAP_PRIVATE 方式映射到内存中。此时,文件内容并未加载到物理内存。
  5. 使用 mlock2 锁定:
    • 调用 mlock2(mapped, sb.st_size, MLOCK_ONFAULT)
    • 这不会立即加载文件内容。它只是告诉内核,这个映射区域的页面在被访问时需要被锁定。
  6. 访问内存:
    • 程序开始访问映射区域的前几页。
    • 每次访问一个尚未加载的页面时,都会触发缺页中断。
    • 内核处理中断,从磁盘读取该页面到物理内存。
    • 由于该页面被 mlock2 标记过,内核会立即锁定该页面。
  7. 概念性检查: 提到了可以使用 mincore 来检查页面是否在物理内存中。
  8. 调用 munlock 解锁整个映射区域。
  9. 调用 munmap 解除映射。
  10. 关闭文件描述符。

重要提示与注意事项: 链接到标题

  1. 权限和限制:
    • 锁定内存需要权限。非特权用户进程锁定的内存量受 RLIMIT_MEMLOCK 限制(可通过 ulimit -l 查看和设置)。
    • root 用户通常有更高的限制或无限制。
  2. MLOCK_ONFAULT 的优势:
    • 避免 OOM: 这是其最大的优势。对于大内存区域,mlock 立即加载所有页面可能导致系统内存瞬间耗尽,触发 OOM killer。MLOCK_ONFAULT 避免了这个问题。
    • 延迟加载: 只有实际访问的页面才会被加载和锁定,节省了物理内存。
  3. 内核版本: mlock2 需要 Linux 内核 4.4 或更高版本。如果在旧内核上调用,会返回 ENOSYS
  4. flags 参数: 目前只有 0MLOCK_ONFAULT 是标准定义的。其他值会导致 EINVAL
  5. mlock 的关系: mlock2(addr, len, 0) 在功能上完全等同于 mlock(addr, len)
  6. 错误处理: 始终检查返回值。ENOMEM (超出限制或内存不足) 和 ENOSYS (不支持) 是常见的错误。
  7. 性能: 锁定内存可以显著提高对关键数据的访问性能,因为它消除了页面换入的延迟。但过度使用会消耗宝贵的物理内存资源,影响系统整体性能。
  8. munlock: 锁定的内存必须在不再需要时用 munlockmunlockall 解锁,否则会一直占用物理内存。

总结:

mlock2mlock 的现代化扩展,其核心优势在于引入了 MLOCK_ONFAULT 标志,实现了按需锁定。这使得锁定大内存区域变得更加安全和高效,避免了传统 mlock 可能引发的内存压力和 OOM 问题。对于需要精细控制内存行为、追求极致性能或高安全性的应用程序来说,mlock2 是一个非常有价值的工具。理解其与 mlock 的区别以及 MLOCK_ONFAULT 的工作原理是掌握其精髓的关键。