好的,我们继续学习 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;
}

代码解释:

  1. 获取系统页大小,并计算要映射的总长度(3 页)和对应的页面数。
  2. 使用 malloc 分配一个 unsigned char 数组 vec,用于存储 mincore 的结果。数组大小等于页面数。
  3. 使用 mmap 创建一个 3 页大小的匿名私有可读写的内存映射。
  4. 第一次检查: 在访问任何映射内存之前调用 mincore。因为是匿名映射且刚刚创建,内核可能还没有为其分配物理页面,或者分配了但尚未加载到内存。结果可能显示页面不在内存中(但这取决于内核实现细节,匿名页面有时在首次映射时就会分配零页)。
  5. 访问第一个页面: 通过写入 mapped_memory[0]mapped_memory[pagesize/2] 来“触摸”第一个页面。这会触发缺页中断,内核会分配一个物理页面并将其加载到内存中。
  6. 第二次检查: 再次调用 mincore。结果应显示第一个页面在内存中
  7. 访问第三个页面: 通过写入 mapped_memory[2 * pagesize] 来“触摸”第三个页面。
  8. 第三次检查: 最后调用 mincore。结果应显示第一个和第三个页面在内存中,而第二个页面可能仍然不在(因为我们没有访问它)。
  9. 定义了一个 print_page_residency 函数来格式化并打印 vec 数组的内容,通过检查每个字节的最低位来判断页面是否在内存中。
  10. 使用 munmapfree 进行清理。

示例 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;
}

代码解释:

  1. 首先创建一个大约 100KB 的测试文件,里面填充了可预测的数据。
  2. 以只读模式打开该文件,并使用 fstat 获取其大小。
  3. 计算需要多少个页面来映射整个文件,并分配相应大小的 vec 数组。
  4. 使用 mmapMAP_PRIVATEPROT_READ 模式映射整个文件。
  5. 第一次 mincore 调用: 在访问文件内容之前检查页面状态。对于文件映射,初始状态可能因内核预读策略、文件系统缓存等因素而异。可能显示部分页面已在内存中(例如,内核为了效率可能预读了前几页)。
  6. 访问文件: 通过循环读取映射区域的前 1000 个字节来“触摸”这些页面。这会触发缺页中断,并将相应的文件页面从磁盘加载到内存。
  7. 第二次 mincore 调用: 在访问之后再次检查。结果应显示被访问的那些页面现在在内存中。
  8. 代码计算了大约访问了多少个页面,并与 mincore 报告的内存中页面数进行比较。
  9. 最后进行清理。

总结:

mincore 是一个用于查询虚拟内存页面物理驻留状态的有用工具。它可以帮助开发者了解程序的内存访问模式和内核的页面管理行为,对于性能调优和调试非常有价值。理解其工作原理的关键在于掌握 vec 数组的使用方法以及只检查最低位来判断页面是否在内存中。