好的,我们继续按照您的要求学习 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)。
- 扩展功能: 相比于
mlock
,mlock2
通过flags
参数提供了额外的控制选项。
4. 参数 見出しへのリンク
const void *addr
: 指向要锁定的内存区域的起始虚拟地址。- 重要: 虽然 POSIX 允许
addr
不是页对齐的,但为了可移植性和效率,强烈建议addr
是系统页大小的整数倍。可以使用getpagesize()
获取页大小。
- 重要: 虽然 POSIX 允许
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
: 用于解锁之前通过mlock
或mlock2
锁定的内存页面。munlockall
: 一次性解锁调用进程锁定的所有内存页面。mlockall
: 锁定调用进程所有的内存页面(当前已映射的和未来将映射的)。getrlimit
/setrlimit
: 用于查询和设置进程的内存锁定限制(RLIMIT_MEMLOCK
)。每个进程能锁定的内存总量是有限制的,默认值通常很小(如 64KB),需要 root 权限或调整ulimit
才能增加。mincore
: 可以用来检查指定的内存页面是否当前驻留在物理内存(RAM)中。
7. 示例代码 見出しへのリンク
示例 1:基本的 mlock
和 mlock2
对比
見出しへのリンク
这个例子演示了 mlock
和 mlock2
(带 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;
}
代码解释:
- 定义了内存大小
ALLOC_SIZE
(10MB) 和print_lock_limit
函数来打印当前进程的内存锁定限制。 - 使用
malloc
分配一大块内存。 - 使用
memset
初始化内存内容,这通常会触发内核分配物理页面。 - 使用
mlock
锁定:- 调用
mlock(buffer, len)
。 mlock
会尝试立即将buffer
指向的内存区域的所有页面加载到物理内存中并锁定。- 如果系统内存不足或超出
RLIMIT_MEMLOCK
限制,mlock
会失败(返回 -1,errno = ENOMEM
)。
- 调用
- 访问已锁定的内存。
- 调用
munlock
解锁内存。 - 使用
mlock2
withMLOCK_ONFAULT
锁定:- 再次初始化内存。
- 调用
mlock2(buffer, len, MLOCK_ONFAULT)
。 mlock2
不会立即加载或锁定页面。它只是标记了这些页面,告诉内核:“当这个范围内的页面被访问时,请加载并锁定它”。- 如果内核不支持
mlock2
,调用会失败(errno = ENOSYS
)。
- 访问内存:
- 当程序第一次访问
buffer
中的某个页面时,会触发缺页中断(Page Fault)。 - 内核处理这个中断,将该页面从后备存储(如文件或匿名页)加载到物理内存。
- 由于该页面之前被
mlock2
标记过,内核在加载后会立即锁定该页面。 - 程序继续执行。
- 当程序第一次访问
- 再次调用
munlock
解锁。 - 释放
malloc
分配的内存。
示例 2:使用 mmap
和 mlock2
锁定大文件
見出しへのリンク
这个例子演示了如何使用 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;
}
代码解释:
- 创建一个 50MB 的大文件
large_file_for_mlock2.txt
。 - 使用
ftruncate
快速创建指定大小的稀疏文件。 - 以只读模式打开文件。
- 使用
mmap
将整个文件以MAP_PRIVATE
方式映射到内存中。此时,文件内容并未加载到物理内存。 - 使用
mlock2
锁定:- 调用
mlock2(mapped, sb.st_size, MLOCK_ONFAULT)
。 - 这不会立即加载文件内容。它只是告诉内核,这个映射区域的页面在被访问时需要被锁定。
- 调用
- 访问内存:
- 程序开始访问映射区域的前几页。
- 每次访问一个尚未加载的页面时,都会触发缺页中断。
- 内核处理中断,从磁盘读取该页面到物理内存。
- 由于该页面被
mlock2
标记过,内核会立即锁定该页面。
- 概念性检查: 提到了可以使用
mincore
来检查页面是否在物理内存中。 - 调用
munlock
解锁整个映射区域。 - 调用
munmap
解除映射。 - 关闭文件描述符。
重要提示与注意事项: 見出しへのリンク
- 权限和限制:
- 锁定内存需要权限。非特权用户进程锁定的内存量受
RLIMIT_MEMLOCK
限制(可通过ulimit -l
查看和设置)。 root
用户通常有更高的限制或无限制。
- 锁定内存需要权限。非特权用户进程锁定的内存量受
MLOCK_ONFAULT
的优势:- 避免 OOM: 这是其最大的优势。对于大内存区域,
mlock
立即加载所有页面可能导致系统内存瞬间耗尽,触发 OOM killer。MLOCK_ONFAULT
避免了这个问题。 - 延迟加载: 只有实际访问的页面才会被加载和锁定,节省了物理内存。
- 避免 OOM: 这是其最大的优势。对于大内存区域,
- 内核版本:
mlock2
需要 Linux 内核 4.4 或更高版本。如果在旧内核上调用,会返回ENOSYS
。 flags
参数: 目前只有0
和MLOCK_ONFAULT
是标准定义的。其他值会导致EINVAL
。- 与
mlock
的关系:mlock2(addr, len, 0)
在功能上完全等同于mlock(addr, len)
。 - 错误处理: 始终检查返回值。
ENOMEM
(超出限制或内存不足) 和ENOSYS
(不支持) 是常见的错误。 - 性能: 锁定内存可以显著提高对关键数据的访问性能,因为它消除了页面换入的延迟。但过度使用会消耗宝贵的物理内存资源,影响系统整体性能。
munlock
: 锁定的内存必须在不再需要时用munlock
或munlockall
解锁,否则会一直占用物理内存。
总结:
mlock2
是 mlock
的现代化扩展,其核心优势在于引入了 MLOCK_ONFAULT
标志,实现了按需锁定。这使得锁定大内存区域变得更加安全和高效,避免了传统 mlock
可能引发的内存压力和 OOM 问题。对于需要精细控制内存行为、追求极致性能或高安全性的应用程序来说,mlock2
是一个非常有价值的工具。理解其与 mlock
的区别以及 MLOCK_ONFAULT
的工作原理是掌握其精髓的关键。