好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mincore
函数,它用于查询一个内存映射区域的哪些页面当前驻留(或驻留)在物理内存(RAM)中,哪些页面当前只是存在于虚拟地址空间中,需要从磁盘(如文件或交换空间)按需调页(Demand Paging)才能访问。
1. 函数介绍 見出しへのリンク
mincore
(Memory in core) 是一个 Linux 系统调用,它提供了一种方法来窥探内核的内存管理信息。具体来说,它可以告诉你一个由 mmap
映射的(或进程堆、栈等)虚拟内存区域中的每个页面当前是否被加载到了物理内存(RAM)中。
这在一些场景下很有用:
- 性能分析: 了解程序的数据局部性。如果一个频繁访问的内存区域大部分都不在物理内存中,可能会导致大量的缺页中断(Page Faults),从而影响性能。
- 预取策略: 在访问大量数据之前,可以先检查哪些页面在内存中,哪些不在,从而决定是否需要采取措施(如
madvise
)来影响内核的页面调度。 - 调试: 帮助理解程序的内存使用模式和内核的页面管理行为。
你可以把它想象成一个“X光机”,可以透视一块区域,看到哪些部分是“实心的”(在物理内存中),哪些部分是“空心的”(只在虚拟内存中,需要从磁盘加载)。
2. 函数原型 見出しへのリンク
#include <sys/mman.h> // 必需
#include <unistd.h> // 可能需要
int mincore(void *addr, size_t length, unsigned char *vec);
3. 功能 見出しへのリンク
- 查询页面状态: 检查从虚拟地址
addr
开始、长度为length
字节的内存区域中,每个内存页面的驻留状态。 - 填充向量: 将查询结果填充到调用者提供的
vec
数组(向量)中。vec
数组中的每个字节对应输入区域的一个页面。
4. 参数 見出しへのリンク
void *addr
: 指向要查询的内存区域的起始虚拟地址。- 重要:
addr
必须是页面对齐的(即地址是系统页大小的整数倍)。可以使用getpagesize()
获取页大小。
- 重要:
size_t length
: 要查询的内存区域的长度(以字节为单位)。- 内核会检查包含
[addr, addr + length - 1]
这个范围的所有页面。
- 内核会检查包含
unsigned char *vec
: 指向一个调用者分配的字符数组(向量)。- 该数组的大小至少需要
ceil(length / page_size)
个字节。 - 例如,如果查询 4097 字节(超过 1 页),且页大小为 4096,则需要至少 2 个字节的
vec
数组。 mincore
调用成功后,会修改这个数组的内容。
- 该数组的大小至少需要
5. vec
数组的含义
見出しへのリンク
mincore
调用成功返回后,vec
数组中的每个字节(unsigned char
)都对应于被查询区域中的一个页面:
- 如果
vec[i]
的最低有效位 (LSB) 被设置为 1(即(vec[i] & 1) != 0
),则表示对应的第i
个页面当前驻留在物理内存(RAM)中。 - 如果
vec[i]
的最低有效位是 0(即(vec[i] & 1) == 0
),则表示对应的第i
个页面当前不在物理内存中(它可能在磁盘上,或者尚未分配)。
注意: vec[i]
的其他位(bit 1-7)被内核保留用于未来使用或提供额外信息,应用程序应只检查最低位。
6. 返回值 見出しへのリンク
- 成功时: 返回 0。同时,
vec
数组被填充了查询结果。 - 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如ENOMEM
地址范围无效或包含未映射区域,EINVAL
addr
未对齐等)。
7. 相似函数,或关联函数 見出しへのリンク
mmap
:mincore
通常用于查询mmap
创建的映射区域的页面状态。madvise
: 可以用来向内核提供建议,影响页面的换入/换出行为(例如MADV_WILLNEED
建议内核将页面加载到内存,MADV_DONTNEED
建议内核可以丢弃页面)。/proc/PID/stat
: 这个文件包含了进程级别的内存统计信息,如majflt
(主缺页)和minflt
(次缺页)计数。- 性能分析工具: 如
perf
,valgrind
等可以提供更详细的内存访问和页面错误信息。
8. 示例代码 見出しへのリンク
示例 1:检查 mmap
匿名映射的页面状态
見出しへのリンク
这个例子演示了如何使用 mincore
检查一个匿名内存映射区域的页面驻留状态。
#define _GNU_SOURCE // 可能需要
#include <sys/mman.h> // mmap, munmap, mincore
#include <unistd.h> // getpagesize
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit, malloc, free
#include <string.h> // memset
void print_page_residency(const unsigned char *vec, int num_pages, const char *description) {
printf("--- Page residency for %s ---\n", description);
for (int i = 0; i < num_pages; ++i) {
// 检查最低位 (bit 0)
if (vec[i] & 1) {
printf(" Page %d: IN MEMORY (0x%02x)\n", i, vec[i]);
} else {
printf(" Page %d: NOT in memory (0x%02x)\n", i, vec[i]);
}
// 注意:vec[i] 的其他位是保留的,通常不打印或解释
}
printf("---------------------------\n");
}
int main() {
long pagesize = getpagesize();
size_t length = 3 * pagesize; // 映射 3 个页面
char *mapped_memory;
unsigned char *vec; // 用于存储 mincore 结果的向量
int num_pages = (length + pagesize - 1) / pagesize; // 向上取整计算页面数
printf("Page size: %ld bytes\n", pagesize);
printf("Mapping length: %zu bytes (%d pages)\n", length, num_pages);
// 1. 分配用于存储结果的向量
vec = malloc(num_pages * sizeof(unsigned char));
if (vec == NULL) {
perror("malloc vec");
exit(EXIT_FAILURE);
}
// 2. 创建一个匿名的私有内存映射
mapped_memory = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
free(vec);
exit(EXIT_FAILURE);
}
printf("Memory mapped at: %p\n", (void*)mapped_memory);
// 3. 初始状态:检查页面驻留情况
printf("\n1. Checking page residency BEFORE any access:\n");
if (mincore(mapped_memory, length, vec) == -1) {
perror("mincore before access");
munmap(mapped_memory, length);
free(vec);
exit(EXIT_FAILURE);
}
print_page_residency(vec, num_pages, "Before access");
// 4. 访问第一个页面
printf("\n2. Accessing (touching) the FIRST page...\n");
mapped_memory[0] = 'A'; // 触发第一页的按需调页
mapped_memory[pagesize / 2] = 'B'; // 同一页的另一个位置
// 5. 再次检查:第一个页面应该在内存中了
printf("Checking page residency AFTER accessing the first page:\n");
if (mincore(mapped_memory, length, vec) == -1) {
perror("mincore after accessing first page");
munmap(mapped_memory, length);
free(vec);
exit(EXIT_FAILURE);
}
print_page_residency(vec, num_pages, "After accessing first page");
// 6. 访问第三个页面 (索引 2)
printf("\n3. Accessing (touching) the THIRD page...\n");
mapped_memory[2 * pagesize] = 'C'; // 触发第三页的按需调页
// 7. 最后检查:第一和第三页应该在内存中
printf("Checking page residency AFTER accessing the third page:\n");
if (mincore(mapped_memory, length, vec) == -1) {
perror("mincore after accessing third page");
munmap(mapped_memory, length);
free(vec);
exit(EXIT_FAILURE);
}
print_page_residency(vec, num_pages, "After accessing third page");
// 8. 清理
if (munmap(mapped_memory, length) == -1) {
perror("munmap");
}
free(vec);
printf("\nTest completed.\n");
return 0;
}
代码解释:
- 获取系统页大小,并计算要映射的总长度(3 页)和对应的页面数。
- 使用
malloc
分配一个unsigned char
数组vec
,用于存储mincore
的结果。数组大小等于页面数。 - 使用
mmap
创建一个 3 页大小的匿名、私有、可读写的内存映射。 - 第一次检查: 在访问任何映射内存之前调用
mincore
。因为是匿名映射且刚刚创建,内核可能还没有为其分配物理页面,或者分配了但尚未加载到内存。结果可能显示页面不在内存中(但这取决于内核实现细节,匿名页面有时在首次映射时就会分配零页)。 - 访问第一个页面: 通过写入
mapped_memory[0]
和mapped_memory[pagesize/2]
来“触摸”第一个页面。这会触发缺页中断,内核会分配一个物理页面并将其加载到内存中。 - 第二次检查: 再次调用
mincore
。结果应显示第一个页面在内存中。 - 访问第三个页面: 通过写入
mapped_memory[2 * pagesize]
来“触摸”第三个页面。 - 第三次检查: 最后调用
mincore
。结果应显示第一个和第三个页面在内存中,而第二个页面可能仍然不在(因为我们没有访问它)。 - 定义了一个
print_page_residency
函数来格式化并打印vec
数组的内容,通过检查每个字节的最低位来判断页面是否在内存中。 - 使用
munmap
和free
进行清理。
示例 2:检查 mmap
文件映射的页面状态
見出しへのリンク
这个例子演示了如何使用 mincore
检查一个文件映射区域的页面状态,并与文件内容关联起来。
#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>
int main() {
const char *filename = "large_file_for_mincore.txt";
int fd;
struct stat sb;
char *mapped;
size_t length;
unsigned char *vec;
long pagesize;
int num_pages;
pagesize = getpagesize();
// 1. 创建一个较大的测试文件
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open for creation");
exit(EXIT_FAILURE);
}
// 写入大约 100KB 的数据
for (int i = 0; i < 100; ++i) {
char buffer[1024];
memset(buffer, 'A' + (i % 26), sizeof(buffer) - 1);
buffer[sizeof(buffer)-1] = '\n';
if (write(fd, buffer, sizeof(buffer)) != sizeof(buffer)) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
}
if (close(fd) == -1) {
perror("close after creation");
exit(EXIT_FAILURE);
}
printf("Created test file '%s'.\n", filename);
// 2. 以只读方式打开文件并映射
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open for reading");
exit(EXIT_FAILURE);
}
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
length = sb.st_size;
num_pages = (length + pagesize - 1) / pagesize;
printf("File size: %zu bytes (%d pages of %ld bytes each)\n", length, num_pages, pagesize);
vec = malloc(num_pages * sizeof(unsigned char));
if (vec == NULL) {
perror("malloc vec");
close(fd);
exit(EXIT_FAILURE);
}
// 页对齐的地址是必需的
mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
free(vec);
close(fd);
exit(EXIT_FAILURE);
}
printf("File mapped successfully at %p.\n", (void*)mapped);
// 3. 初始检查 (文件刚映射,页面可能还未从磁盘加载)
printf("\n--- Checking page residency BEFORE any file access ---\n");
if (mincore(mapped, length, vec) == -1) {
perror("mincore before access");
// 即使失败也继续,因为某些系统/文件系统可能不完全支持
// 或者文件太小,没有跨页
printf("mincore failed, continuing...\n");
} else {
int pages_in_mem = 0;
for (int i = 0; i < num_pages; ++i) {
if (vec[i] & 1) {
pages_in_mem++;
}
}
printf(" Pages in memory: %d out of %d\n", pages_in_mem, num_pages);
// 打印前几页的状态作为示例
for (int i = 0; i < (num_pages > 5 ? 5 : num_pages); ++i) {
printf(" Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
}
if (num_pages > 5) {
printf(" ... (showing first 5 pages only)\n");
}
}
// 4. 访问文件内容的一部分 (例如,读取前 1000 个字节)
printf("\n--- Accessing first 1000 bytes of the file ---\n");
size_t access_len = (length > 1000) ? 1000 : length;
volatile char sum = 0; // volatile 防止编译器优化掉循环
for (size_t i = 0; i < access_len; ++i) {
sum += mapped[i]; // 简单访问,确保页面被加载
}
printf(" Sum of first %zu bytes (to ensure access): %d\n", access_len, (int)sum);
// 5. 再次检查 (访问后,相关页面应该在内存中)
printf("\n--- Checking page residency AFTER accessing first 1000 bytes ---\n");
if (mincore(mapped, length, vec) == -1) {
perror("mincore after access");
} else {
int pages_in_mem = 0;
// 计算大约涉及多少个页面 (0 到 access_len - 1)
int pages_touched = ((access_len - 1) / pagesize) + 1;
for (int i = 0; i < num_pages; ++i) {
if (vec[i] & 1) {
pages_in_mem++;
}
}
printf(" Pages in memory: %d out of %d\n", pages_in_mem, num_pages);
printf(" Approximately %d pages should have been touched.\n", pages_touched);
// 打印前几页的状态
for (int i = 0; i < (num_pages > 5 ? 5 : num_pages); ++i) {
printf(" Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
}
if (num_pages > 5) {
printf(" ... (showing first 5 pages only)\n");
}
}
// 6. 清理
if (munmap(mapped, length) == -1) {
perror("munmap");
}
free(vec);
if (close(fd) == -1) {
perror("close");
}
// 可选:删除测试文件
// unlink(filename);
printf("\nTest completed.\n");
return 0;
}
代码解释:
- 首先创建一个大约 100KB 的测试文件,里面填充了可预测的数据。
- 以只读模式打开该文件,并使用
fstat
获取其大小。 - 计算需要多少个页面来映射整个文件,并分配相应大小的
vec
数组。 - 使用
mmap
以MAP_PRIVATE
和PROT_READ
模式映射整个文件。 - 第一次
mincore
调用: 在访问文件内容之前检查页面状态。对于文件映射,初始状态可能因内核预读策略、文件系统缓存等因素而异。可能显示部分页面已在内存中(例如,内核为了效率可能预读了前几页)。 - 访问文件: 通过循环读取映射区域的前 1000 个字节来“触摸”这些页面。这会触发缺页中断,并将相应的文件页面从磁盘加载到内存。
- 第二次
mincore
调用: 在访问之后再次检查。结果应显示被访问的那些页面现在在内存中。 - 代码计算了大约访问了多少个页面,并与
mincore
报告的内存中页面数进行比较。 - 最后进行清理。
总结:
mincore
是一个用于查询虚拟内存页面物理驻留状态的有用工具。它可以帮助开发者了解程序的内存访问模式和内核的页面管理行为,对于性能调优和调试非常有价值。理解其工作原理的关键在于掌握 vec
数组的使用方法以及只检查最低位来判断页面是否在内存中。