好的,我们继续学习 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;
}
代码解释:
- 定义了初始、扩展和收缩后的大小。
- 使用
mmap
创建一个大小为 1 页的匿名、私有、可读写的内存映射。 - 向初始映射区域写入一些数据。
- 扩展映射:
- 调用
mremap(mapped_memory, initial_size, larger_size, MREMAP_MAYMOVE)
。 - 指定了
MREMAP_MAYMOVE
,允许内核在需要时移动映射。 - 检查返回值。如果成功,必须将返回的新地址赋值给
mapped_memory
变量。 - 打印新的映射范围和大小。
- 验证原始数据是否仍然存在。
- 向扩展后的区域写入新数据。
- 调用
- 收缩映射:
- 调用
mremap(mapped_memory, larger_size, smaller_size, MREMAP_MAYMOVE)
。 - 同样,检查返回值并更新
mapped_memory
地址。 - 打印新的映射范围和大小。
- 尝试访问仍在新范围内的数据。
- 注释说明: 尝试访问被收缩掉的内存区域会导致段错误。示例中为了避免崩溃,将相关代码注释掉了。
- 调用
- 最后使用
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;
}
代码解释:
- 创建两个独立的匿名内存映射
map1
和map2
。 - 打印它们的地址,观察它们是否相邻。
- 调用
mremap(map1, ..., MREMAP_MAYMOVE)
尝试扩展map1
。 - 由于
map2
可能紧随map1
之后,原地扩展可能没有空间,因此内核很可能会将map1
移动到新的地址。 - 检查
mremap
的返回值remapped_map
是否与原来的map1
地址相同。 - 如果地址不同,则打印出旧地址和新地址,说明发生了移动。
- 访问新地址
remapped_map
处的数据,验证数据是否被正确复制(对于MAP_PRIVATE
映射,通常是这样)。 - 强调: 原来的
map1
指针现在是无效的,不应再使用。 - 使用正确的地址和大小调用
munmap
进行清理。
总结:
mremap
是一个强大且高效的工具,用于调整现有内存映射的大小。理解其关键在于记住它可能返回一个新的地址,调用者必须始终使用这个新地址。它特别适用于需要动态调整映射文件或匿名内存块大小的场景。与 realloc
类似,但作用域是 mmap
创建的区域。