好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mremap 函数,它是 mmap 的配套函数,专门用于扩展或缩小一个已存在的内存映射区域的大小。


1. 函数介绍 链接到标题

mremap 是一个 Linux 系统调用,用于修改先前通过 mmap 创建的内存映射区域的大小。它可以将一个现有的映射区域增大(扩展)或减小(收缩)。

当你最初使用 mmap 映射了一块内存区域,但后来发现需要更多(或更少)的空间时,mremap 提供了一种比 munmap + mmap 更高效的方式来调整这块区域的大小。

  • 扩展: 增加映射区域的长度。如果可能,内核会尝试在原映射区域的末尾直接扩展;如果不行(例如,后面的地址空间已被占用),内核可能会移动整个映射区域到新的位置。
  • 收缩: 减少映射区域的长度。超出新长度的部分将被取消映射。

你可以把它想象成调整一张已经贴在墙上的海报的大小。扩展就像是向下或向旁边多贴一些纸;收缩就像是从底部撕掉一些纸。


2. 函数原型 链接到标题

#define _GNU_SOURCE // 需要定义此宏以使用 mremap
#include <sys/mman.h> // 必需

void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ... /* void *new_address */);

注意: mremap 是 Linux 特有的扩展,不是 POSIX 标准的一部分。


3. 功能 链接到标题

  • 调整大小: 将从 old_address 开始、大小为 old_size 的现有映射区域调整为 new_size
  • 可能移动: 根据 flags 和内存布局,内核可能会将映射区域移动到新的地址 new_address
  • 返回新地址: 成功时,返回映射区域的新起始地址。这个地址可能与 old_address 相同(原地调整),也可能不同(映射被移动了)。

4. 参数 链接到标题

  • void *old_address: 指向要调整大小的现有映射区域的起始地址。这个地址必须是之前 mmap 调用返回的有效地址。
  • size_t old_size: 现有映射区域的当前大小(以字节为单位)。
  • size_t new_size: 请求的新大小(以字节为单位)。
    • 如果 new_size > old_size,则映射区域被扩展。
    • 如果 new_size < old_size,则映射区域被收缩。
    • 如果 new_size == old_size,则函数可能不执行任何操作或执行一些维护工作。
  • int flags: 控制 mremap 行为的标志。必须包含以下值之一:
    • MREMAP_MAYMOVE: 允许内核在必要时将映射移动到新的地址空间。如果未设置此标志,且原地扩展不可能,则 mremap 会失败。
    • MREMAP_FIXED: 与 MREMAP_MAYMOVE 一起使用。它要求映射必须被移动到由后续的 new_address 参数指定的地址。如果该地址不可用或无法用于映射,则调用失败。使用此标志需要非常小心。 此外,以下标志可以按位或:
    • MREMAP_DONTUNMAP (Linux 5.7+): 在移动映射时,不取消映射旧区域。这允许同时存在新旧映射。
  • ... (可变参数): 如果设置了 MREMAP_FIXED 标志,则此参数是必需的,类型为 void *,指定了映射区域必须移动到的新地址

5. 返回值 链接到标题

  • 成功时: 返回映射区域的新起始地址。这个地址可能与 old_address 相同,也可能不同。
  • 失败时: 返回 MAP_FAILED(即 (void *) -1),并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL 参数无效,ENOMEM 内存不足或地址不可用,EAGAIN 临时错误等)。

重要: 因为 mremap 可能会移动映射区域,所以调用者必须使用 mremap 返回的新地址来访问这块内存。继续使用 old_address 可能导致未定义行为(如果映射被移动)或访问到错误的数据(如果映射被收缩)。


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

  • mmap: 用于创建初始的内存映射。
  • munmap: 用于完全解除内存映射。
  • realloc: C 标准库函数,用于调整 malloc 分配的堆内存大小。mremap 在功能上与 realloc 类似,但作用于 mmap 的内存区域。
  • brk / sbrk: 用于调整由 brk 管理的堆的大小。

7. 示例代码 链接到标题

示例 1:扩展和收缩匿名内存映射 链接到标题

这个例子演示了如何使用 mremap 来扩展和收缩一个匿名内存映射区域。

#define _GNU_SOURCE // 必须定义以使用 mremap
#include <sys/mman.h> // mmap, munmap, mremap
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <string.h>   // strcpy, strlen
#include <unistd.h>   // getpagesize

int main() {
    size_t initial_size = 4096; // 1 页
    size_t larger_size = 12288; // 3 页
    size_t smaller_size = 2048; // 半页多一点
    char *mapped_memory;
    char *new_mapped_memory;

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

    // 1. 创建初始的匿名内存映射
    mapped_memory = mmap(NULL, initial_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (mapped_memory == MAP_FAILED) {
        perror("mmap initial");
        exit(EXIT_FAILURE);
    }
    printf("Initial mapping: %p - %p (Size: %zu bytes)\n",
           (void*)mapped_memory, (char*)mapped_memory + initial_size - 1, initial_size);

    // 2. 在初始映射中放入一些数据
    const char *initial_data = "Initial data in the mapped region.";
    size_t data_len = strlen(initial_data);
    if (data_len < initial_size) {
        strcpy(mapped_memory, initial_data);
        printf("Written initial data: '%s'\n", mapped_memory);
    }

    // --- 3. 扩展映射区域 ---
    printf("\n--- Expanding mapping ---\n");
    // MREMAP_MAYMOVE: 允许内核移动映射
    new_mapped_memory = mremap(mapped_memory, initial_size, larger_size, MREMAP_MAYMOVE);
    if (new_mapped_memory == MAP_FAILED) {
        perror("mremap expand");
        // 清理并退出
        munmap(mapped_memory, initial_size);
        exit(EXIT_FAILURE);
    }
    // 关键:使用 mremap 返回的新地址!
    mapped_memory = new_mapped_memory;
    printf("Expanded mapping: %p - %p (Size: %zu bytes)\n",
           (void*)mapped_memory, (char*)mapped_memory + larger_size - 1, larger_size);

    // 4. 验证初始数据是否还在 (通常会在)
    printf("Data after expansion: '%s'\n", mapped_memory);

    // 5. 在扩展后的区域写入新数据
    const char *new_data = "New data written after expansion!";
    size_t new_data_len = strlen(new_data);
    // 写入到扩展区域的后面部分
    if (initial_size + new_data_len < larger_size) {
        strcpy(mapped_memory + initial_size, new_data);
        printf("Written new data at offset %zu: '%s'\n", initial_size, mapped_memory + initial_size);
    }


    // --- 6. 收缩映射区域 ---
    printf("\n--- Shrinking mapping ---\n");
    new_mapped_memory = mremap(mapped_memory, larger_size, smaller_size, MREMAP_MAYMOVE);
    if (new_mapped_memory == MAP_FAILED) {
        perror("mremap shrink");
        // 清理并退出
        munmap(mapped_memory, larger_size); // 使用旧大小清理
        exit(EXIT_FAILURE);
    }
    // 再次更新地址
    mapped_memory = new_mapped_memory;
    printf("Shrunk mapping: %p - %p (Size: %zu bytes)\n",
           (void*)mapped_memory, (char*)mapped_memory + smaller_size - 1, smaller_size);

    // 7. 尝试访问收缩后仍在范围内的数据
    printf("First %zu characters after shrinking: ", (smaller_size < 100) ? smaller_size : 100);
    for (size_t i = 0; i < smaller_size && i < 100; ++i) {
        putchar(mapped_memory[i]);
    }
    printf("\n");

    // 8. 尝试访问被收缩掉的部分 (会导致段错误 Segmentation fault)
    // printf("Trying to access data beyond new end...\n");
    // char c = mapped_memory[initial_size]; // 例如,访问原来初始数据之后的位置
    // printf("Accessed character (should not happen): %c\n", c);
    // 为了避免崩溃,注释掉上面两行。取消注释会导致程序崩溃。


    // --- 9. 清理 ---
    if (munmap(mapped_memory, smaller_size) == -1) {
        perror("munmap final");
    }

    printf("\nMemory unmapped successfully.\n");
    return 0;
}

代码解释:

  1. 定义了初始、扩展和收缩后的大小。
  2. 使用 mmap 创建一个大小为 1 页的匿名私有可读写的内存映射。
  3. 向初始映射区域写入一些数据。
  4. 扩展映射:
    • 调用 mremap(mapped_memory, initial_size, larger_size, MREMAP_MAYMOVE)
    • 指定了 MREMAP_MAYMOVE,允许内核在需要时移动映射。
    • 检查返回值。如果成功,必须将返回的新地址赋值给 mapped_memory 变量。
    • 打印新的映射范围和大小。
    • 验证原始数据是否仍然存在。
    • 向扩展后的区域写入新数据。
  5. 收缩映射:
    • 调用 mremap(mapped_memory, larger_size, smaller_size, MREMAP_MAYMOVE)
    • 同样,检查返回值并更新 mapped_memory 地址。
    • 打印新的映射范围和大小。
    • 尝试访问仍在新范围内的数据。
    • 注释说明: 尝试访问被收缩掉的内存区域会导致段错误。示例中为了避免崩溃,将相关代码注释掉了。
  6. 最后使用 munmap 释放最终大小的映射区域。

示例 2:处理 mremap 可能的地址移动 链接到标题

这个例子更明确地展示了 mremap 返回不同地址的情况。

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

int main() {
    size_t size1 = 4096;
    size_t size2 = 8192;
    char *map1, *map2, *remapped_map;

    // 1. 创建第一个映射
    map1 = mmap(NULL, size1, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (map1 == MAP_FAILED) {
        perror("mmap map1");
        exit(EXIT_FAILURE);
    }
    printf("Map1 created at: %p\n", (void*)map1);
    strcpy(map1, "Data in Map1");

    // 2. 创建第二个映射 (紧随第一个之后创建,地址可能相邻)
    map2 = mmap(NULL, size1, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (map2 == MAP_FAILED) {
        perror("mmap map2");
        munmap(map1, size1);
        exit(EXIT_FAILURE);
    }
    printf("Map2 created at: %p\n", (void*)map2);
    strcpy(map2, "Data in Map2");

    // 3. 尝试扩展 map1。如果 map2 紧跟在 map1 后面,原地扩展可能失败。
    printf("\n--- Attempting to expand Map1 ---\n");
    printf("Map1 range: %p - %p\n", (void*)map1, (char*)map1 + size1 - 1);
    printf("Map2 range: %p - %p\n", (void*)map2, (char*)map2 + size1 - 1);

    // 检查是否相邻 (简化检查)
    if ((char*)map1 + size1 == (char*)map2) {
        printf("Map1 and Map2 are adjacent. Expansion might require moving.\n");
    }

    // 尝试扩展 map1
    remapped_map = mremap(map1, size1, size2, MREMAP_MAYMOVE);
    if (remapped_map == MAP_FAILED) {
        perror("mremap map1");
        munmap(map1, size1);
        munmap(map2, size1);
        exit(EXIT_FAILURE);
    }

    // 4. 检查地址是否改变
    if (remapped_map != map1) {
        printf("Map1 was MOVED by mremap!\n");
        printf("  Old address: %p\n", (void*)map1);
        printf("  New address: %p\n", (void*)remapped_map);
    } else {
        printf("Map1 was expanded IN PLACE.\n");
        printf("  Address remains: %p\n", (void*)remapped_map);
    }
    printf("New Map1 range: %p - %p (Size: %zu)\n",
           (void*)remapped_map, (char*)remapped_map + size2 - 1, size2);

    // 5. 验证数据 (如果移动了,数据应该被复制)
    printf("Data in (new) Map1: '%s'\n", remapped_map);

    // 6. 原来的 map1 指针现在无效了,不要再使用
    // printf("Trying to access old map1 pointer... "); // 不要这样做
    // printf("%s\n", map1); // 危险!

    // 7. 清理所有映射
    // 使用新的地址和大小清理 map1
    if (munmap(remapped_map, size2) == -1) {
        perror("munmap remapped_map");
    }
    // 清理 map2
    if (munmap(map2, size1) == -1) {
        perror("munmap map2");
    }

    printf("\nAll mappings cleaned up.\n");
    return 0;
}

代码解释:

  1. 创建两个独立的匿名内存映射 map1map2
  2. 打印它们的地址,观察它们是否相邻。
  3. 调用 mremap(map1, ..., MREMAP_MAYMOVE) 尝试扩展 map1
  4. 由于 map2 可能紧随 map1 之后,原地扩展可能没有空间,因此内核很可能会将 map1 移动到新的地址。
  5. 检查 mremap 的返回值 remapped_map 是否与原来的 map1 地址相同。
  6. 如果地址不同,则打印出旧地址和新地址,说明发生了移动。
  7. 访问新地址 remapped_map 处的数据,验证数据是否被正确复制(对于 MAP_PRIVATE 映射,通常是这样)。
  8. 强调: 原来的 map1 指针现在是无效的,不应再使用。
  9. 使用正确的地址和大小调用 munmap 进行清理。

总结:

mremap 是一个强大且高效的工具,用于调整现有内存映射的大小。理解其关键在于记住它可能返回一个新的地址,调用者必须始终使用这个新地址。它特别适用于需要动态调整映射文件或匿名内存块大小的场景。与 realloc 类似,但作用域是 mmap 创建的区域。