好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 madvise 函数,它允许进程向内核提供关于其内存访问模式的建议(advice),从而帮助内核优化内存管理策略,提高应用程序的性能。


1. 函数介绍 見出しへのリンク

madvise (Memory Advice) 是一个 Linux 系统调用,它允许应用程序向操作系统内核传达其对某块内存区域未来访问模式的预期意图。这些建议并非强制性的指令,而是一种提示,内核可以利用这些信息来优化内存管理,例如:

  • 预读取 (Prefetching): 如果应用程序表明它将顺序访问大量数据,内核可以提前将后续页面加载到内存中,减少等待时间。
  • 页面回收 (Page Reclaim): 如果应用程序表明某些数据不再需要,内核可以更积极地将这些页面换出到交换空间或直接丢弃(对于匿名映射),从而释放物理内存给其他更需要的进程。
  • 延迟分配 (Lazy Allocation): 对于某些类型的大内存区域,内核可以根据建议推迟实际的物理内存分配,直到真正需要为止。

你可以把 madvise 想象成你对图书管理员说的话:“我接下来会按顺序看这几章书,麻烦你先把它们都准备好” 或 “这几页我看过了,暂时用不到了,你可以先收起来”。


2. 函数原型 見出しへのリンク

#include <sys/mman.h> // 必需

int madvise(void *addr, size_t length, int advice);

3. 功能 見出しへのリンク

  • 传达意图: 向内核传达从地址 addr 开始、长度为 length 字节的内存区域的访问意图。
  • 影响策略: 内核根据收到的 advice 来调整其对该内存区域的管理策略(如预读、换出、合并等)。
  • 范围操作: 可以对内存映射的任意子区域提供建议。

4. 参数 見出しへのリンク

  • void *addr: 指向要提供建议的内存区域的起始虚拟地址
    • 重要: 虽然 POSIX 允许 addr 不是页对齐的,但为了可移植性和确保建议应用于完整的页面,强烈建议 addr 是系统页大小的整数倍。可以使用 getpagesize() 获取页大小。
  • size_t length: 要提供建议的内存区域的长度(以字节为单位)。
    • 同样,为了确保建议应用于完整的页面,length 最好也是页大小的整数倍。如果不足一页,内核通常会向上舍入到最近的页边界。
  • int advice: 指定要提供的具体建议。这是一个整数常量,定义在 <sys/mman.h> 中。常见的包括:
    • MADV_NORMAL: 普通访问。这是默认行为,内核使用标准的页面读取和换出策略。
    • MADV_SEQUENTIAL: 顺序访问。应用程序预计会以向前的顺序访问这段内存。内核可能会增加预读的激进程度。
    • MADV_RANDOM: 随机访问。应用程序预计会以随机的顺序访问这段内存。内核可能会减少或禁用预读。
    • MADV_WILLNEED: 将会需要。应用程序预计很快就会访问这段内存。内核可能会提前将相关页面加载到物理内存中。
    • MADV_DONTNEED: 不需要。应用程序目前不需要这段内存中的数据(对于文件映射)或表明这些页面可以被丢弃(对于匿名映射,如 MAP_ANONYMOUS)。注意: 对于私有匿名映射 (MAP_PRIVATE | MAP_ANONYMOUS),这通常会导致页面内容被丢弃(下次访问会得到零页)。对于共享映射或文件映射,行为可能不同,主要是提示内核可以更积极地换出这些页面。
    • MADV_FREE (Linux 4.5+): 告诉内核这段内存可以被释放。与 MADV_DONTNEED 类似,但对于私有匿名页,它更明确地表示页面内容可以丢弃。如果之后又访问了这些页面,会得到零页。在某些内核版本中,MADV_FREE 的效果可能比 MADV_DONTNEED 更延迟,即内核可能不会立即释放物理页。
    • MADV_REMOVE: 尝试移除指定范围内的页面。对于支持的文件系统,这可能导致文件中对应部分被 deallocate(释放空间,类似 fallocateFALLOC_FL_PUNCH_HOLE)。对于匿名映射,行为类似于 MADV_DONTNEED
    • MADV_DONTFORK: 在 fork() 时,不要将此范围的页面复制到子进程中。子进程访问这些页面会导致缺页。
    • MADV_DOFORK: 取消 MADV_DONTFORK 的效果。
    • MADV_MERGEABLE / MADV_UNMERGEABLE: 允许/禁止内核使用 KSM (Kernel Samepage Merging) 来合并内容相同的页面。常用于虚拟机环境。
    • MADV_HUGEPAGE / MADV_NOHUGEPAGE: 鼓励/禁止内核为此区域使用大页 (Huge Pages)。
    • MADV_POPULATE_READ / MADV_POPULATE_WRITE (Linux 5.14+): 立即为区域触发读/写访问的缺页,将页面加载到内存。

5. 返回值 見出しへのリンク

  • 成功时: 返回 0。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL advice 无效或 addr/length 无效,ENOMEM 地址范围包含未映射区域等)。

6. 相似函数,或关联函数 見出しへのリンク

  • mmap: madvise 通常作用于通过 mmap 创建的内存区域。
  • mincore: 用于查询页面的当前驻留状态,可以与 madvise 结合使用来观察建议的效果。
  • mprotect: 用于修改内存区域的访问权限(读/写/执行)。
  • posix_madvise: POSIX 标准定义的函数,功能与 madvise 类似,但可移植性更好。在 Linux 上,它通常是 madvise 的一个薄包装。
  • fadvise64 / posix_fadvise: 与 madvise 类似,但作用于文件描述符文件偏移量范围,用于向内核提供文件访问模式的建议,优化文件 I/O。

7. 示例代码 見出しへのリンク

示例 1:使用 MADV_SEQUENTIALMADV_WILLNEED 見出しへのリンク

这个例子演示了如何为顺序处理的大数据块提供 MADV_SEQUENTIAL 建议,并为即将访问的区域提供 MADV_WILLNEED 建议。

#define _GNU_SOURCE // 可能需要
#include <sys/mman.h> // mmap, munmap, madvise
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open
#include <unistd.h>   // close, fstat, getpagesize
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <string.h>   // memset
#include <time.h>     // clock

// 模拟处理数据的函数
void process_data_chunk(char *data, size_t size) {
    volatile unsigned long sum = 0; // volatile 防止编译器优化
    for (size_t i = 0; i < size; ++i) {
        sum += data[i];
    }
    // 简单使用 sum 防止编译器完全优化掉循环
    if (sum % 2 == 0) { /* do nothing */ }
}

int main() {
    const char *filename = "/tmp/large_data_file.bin";
    int fd;
    struct stat sb;
    char *mapped;
    size_t length;
    long pagesize;
    size_t chunk_size;
    clock_t start, end;
    double cpu_time_used;

    pagesize = getpagesize();
    chunk_size = 10 * pagesize; // 每次处理 10 页

    // 1. 创建一个较大的测试文件
    printf("Creating a large test file...\n");
    fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open for creation");
        exit(EXIT_FAILURE);
    }
    // 写入大约 1MB 的随机数据
    size_t total_size = 1 * 1024 * 1024; // 1 MB
    char *buffer = malloc(pagesize);
    if (!buffer) {
        perror("malloc buffer");
        close(fd);
        exit(EXIT_FAILURE);
    }
    for (size_t written = 0; written < total_size; written += pagesize) {
        // 填充缓冲区
        for (int i = 0; i < pagesize; ++i) {
            buffer[i] = (char)(rand() % 256);
        }
        if (write(fd, buffer, pagesize) != pagesize) {
            perror("write");
            free(buffer);
            close(fd);
            exit(EXIT_FAILURE);
        }
    }
    free(buffer);
    if (close(fd) == -1) {
        perror("close after creation");
        exit(EXIT_FAILURE);
    }
    printf("Created test file '%s' of size %zu bytes.\n", filename, total_size);

    // 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;

    mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("File mapped successfully. Address: %p, Size: %zu bytes\n", (void*)mapped, length);

    // 3. 提供 MADV_SEQUENTIAL 建议 (对整个区域)
    printf("\nProviding MADV_SEQUENTIAL advice for the entire mapped region...\n");
    if (madvise(mapped, length, MADV_SEQUENTIAL) == -1) {
        perror("madvise MADV_SEQUENTIAL");
        // 不是致命错误,继续执行
    } else {
        printf("MADV_SEQUENTIAL advice given successfully.\n");
    }

    // 4. 顺序处理数据块
    printf("Starting sequential processing...\n");
    start = clock();

    for (size_t offset = 0; offset < length; offset += chunk_size) {
        size_t this_chunk_size = (offset + chunk_size > length) ? (length - offset) : chunk_size;

        // 为下一个即将处理的块提供 MADV_WILLNEED 建议
        if (offset + chunk_size < length) {
            size_t next_offset = offset + chunk_size;
            size_t next_chunk_size = (next_offset + chunk_size > length) ? (length - next_offset) : chunk_size;
            if (madvise(mapped + next_offset, next_chunk_size, MADV_WILLNEED) == -1) {
                perror("madvise MADV_WILLNEED");
                // 不是致命错误,继续执行
            }
        }

        // 处理当前块
        process_data_chunk(mapped + offset, this_chunk_size);
        // printf("Processed chunk at offset %zu, size %zu\n", offset, this_chunk_size); // 可选打印
    }

    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Sequential processing completed.\n");
    printf("CPU time used: %f seconds\n", cpu_time_used);

    // 5. 清理
    if (munmap(mapped, length) == -1) {
        perror("munmap");
    }
    if (close(fd) == -1) {
        perror("close");
    }
    // 可选:删除测试文件
    // unlink(filename);

    printf("Test completed.\n");
    return 0;
}

代码解释:

  1. 创建一个 1MB 的大型测试文件。
  2. 以只读方式打开并映射该文件。
  3. 调用 madvise(mapped, length, MADV_SEQUENTIAL),告诉内核整个映射区域将被顺序访问。这鼓励内核进行更激进的预读。
  4. 进入一个循环,按固定大小(chunk_size)顺序处理数据。
  5. 关键: 在处理当前块之前,为下一个即将处理的块调用 madvise(..., MADV_WILLNEED)。这提示内核提前将下一个块的页面加载到内存中,以减少处理下一个块时的等待时间。
  6. 调用 process_data_chunk 模拟对数据的实际处理。
  7. 记录并打印处理总耗时。
  8. 最后进行清理。

示例 2:使用 MADV_DONTNEED 释放不需要的内存 見出しへのリンク

这个例子演示了如何使用 MADV_DONTNEED 来告知内核某块内存区域的数据不再需要,可以被丢弃或换出。

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

int main() {
    long pagesize = getpagesize();
    size_t size = 5 * pagesize; // 5 页
    char *mapped_memory;
    unsigned char *vec; // 用于 mincore 检查

    // 1. 分配用于 mincore 的向量
    vec = malloc(5 * sizeof(unsigned char));
    if (vec == NULL) {
        perror("malloc vec");
        exit(EXIT_FAILURE);
    }

    // 2. 创建匿名私有映射
    mapped_memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mapped_memory == MAP_FAILED) {
        perror("mmap");
        free(vec);
        exit(EXIT_FAILURE);
    }
    printf("Anonymous memory mapped at: %p (Size: %zu bytes, %ld pages)\n",
           (void*)mapped_memory, size, size / pagesize);

    // 3. 在映射区域中放入一些数据
    printf("Filling memory with data...\n");
    for (size_t i = 0; i < size; ++i) {
        mapped_memory[i] = (char)(i % 256);
    }

    // 4. 检查初始页面状态 (应该都在内存中,因为已访问)
    printf("\n--- Page residency BEFORE MADV_DONTNEED ---\n");
    if (mincore(mapped_memory, size, vec) != -1) {
        for (int i = 0; i < 5; ++i) {
            printf("  Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
        }
    } else {
        perror("mincore before");
    }

    // 5. 访问数据以验证其内容
    printf("\nVerifying data in first few bytes: ");
    for (int i = 0; i < 10; ++i) {
        printf("%02x ", (unsigned char)mapped_memory[i]);
    }
    printf("\n");

    // 6. 提供 MADV_DONTNEED 建议 (针对前 3 页)
    printf("\nProviding MADV_DONTNEED advice for the first 3 pages...\n");
    if (madvise(mapped_memory, 3 * pagesize, MADV_DONTNEED) == -1) {
        perror("madvise MADV_DONTNEED");
    } else {
        printf("MADV_DONTNEED advice given for first 3 pages.\n");
    }

    // 7. 再次检查页面状态
    printf("\n--- Page residency AFTER MADV_DONTNEED ---\n");
    if (mincore(mapped_memory, size, vec) != -1) {
        for (int i = 0; i < 5; ++i) {
            printf("  Page %d: %s\n", i, (vec[i] & 1) ? "IN MEMORY" : "NOT in memory");
        }
    } else {
        perror("mincore after");
    }

    // 8. 尝试访问被标记为 DONTNEED 的页面的数据
    printf("\nAccessing data in the first page (marked DONTNEED)...\n");
    // 对于 MAP_PRIVATE | MAP_ANONYMOUS, MADV_DONTNEED 通常会导致内容被丢弃
    printf("First few bytes of first page: ");
    for (int i = 0; i < 10; ++i) {
        // 访问后,页面会被重新分配并填充零
        printf("%02x ", (unsigned char)mapped_memory[i]);
    }
    printf("\n(Notice: Data might be zeros if page was discarded)\n");

    // 9. 访问未被标记为 DONTNEED 的页面 (第 4, 5 页)
    printf("\nAccessing data in the fourth page (NOT marked DONTNEED)...\n");
    printf("First few bytes of fourth page: ");
    for (int i = 0; i < 10; ++i) {
        printf("%02x ", (unsigned char)mapped_memory[3 * pagesize + i]);
    }
    printf("\n(Notice: Data should still be original values)\n");


    // 10. 清理
    if (munmap(mapped_memory, size) == -1) {
        perror("munmap");
    }
    free(vec);

    printf("\nTest completed.\n");
    return 0;
}

代码解释:

  1. 创建一个 5 页大小的匿名私有内存映射。
  2. 向整个映射区域写入一些数据。
  3. 使用 mincore 检查初始页面状态,确认所有页面都在内存中。
  4. 打印部分数据以验证其内容。
  5. 调用 madvise(mapped_memory, 3 * pagesize, MADV_DONTNEED),告诉内核前 3 页的数据不再需要。
  6. 再次使用 mincore 检查页面状态。可能显示前 3 页已不在内存中(或内核已将其标记为可丢弃)。
  7. 关键: 尝试访问第一个页面(已标记 DONTNEED)的数据。对于 MAP_PRIVATE | MAP_ANONYMOUS 映射,MADV_DONTNEED 通常会导致该页面内容被丢弃。因此,再次访问时,可能会得到零填充的页面。
  8. 访问第 4 页和第 5 页(未标记 DONTNEED)的数据,确认其内容仍然保留。
  9. 最后进行清理。

重要提示与注意事项:

  1. 建议,不是命令: madviseadvice 只是建议,内核可以忽略。实际行为取决于内核版本、配置和当前系统负载。
  2. 效果延迟: 内核可能不会立即执行建议的操作(如立即加载页面或丢弃页面),而是在后续的内存管理周期中处理。
  3. 平台差异: 不同的操作系统和内核版本对 madvise 的支持和具体行为可能有所不同。
  4. 结合使用: madvise 经常与 mmap, mincore 结合使用,以实现更精细的内存控制和性能优化。
  5. 性能影响: 正确使用 madvise 可以显著提升性能,尤其是在处理大文件或大数据集时。错误的建议(例如,对随机访问的数据使用 MADV_SEQUENTIAL)可能会适得其反。

总结:

madvise 是一个强大的工具,允许应用程序与操作系统内核协作,优化内存管理。通过提供关于内存访问模式的建议,程序可以引导内核采取更有效的预读、换出等策略,从而提高整体性能。理解各种 advice 选项的含义和适用场景是有效利用 madvise 的关键。