这次我们介绍 munmap 函数,它是 mmap 的“搭档”,用于释放之前通过 mmap 创建的内存映射区域。
1. 函数介绍
munmap 是一个 Linux 系统调用,其主要功能是解除(或取消映射)之前通过 mmap 系统调用在进程地址空间中创建的内存映射区域。调用 munmap 会通知内核,进程不再需要访问由 mmap 建立的从 addr 开始、长度为 length 的那块虚拟内存区域。
你可以把它想象成从墙上撕下之前贴上的书页(由 mmap 贴上)。撕下来后,这块“墙上的区域”(进程的地址空间)就空出来了,可以被系统重新分配给其他用途,同时与之关联的文件(如果有的话)也不再通过这块内存访问。
2. 函数原型
#include <sys/mman.h> // 必需
int munmap(void *addr, size_t length);
3. 功能
- 解除映射: 告诉内核解除从地址
addr开始、长度为length字节的内存区域的映射关系。 - 释放资源: 内核会释放与该映射区域相关的内核数据结构,并可能将该区域的虚拟地址空间标记为未使用。
- 数据同步: 对于
MAP_SHARED映射,内核可能会将映射区域中被修改的页面刷新回底层文件(虽然不保证立即完成,msync可以强制同步)。 - 内存回收: 进程不再能通过
addr指针访问这块内存。如果这块内存是通过mmap映射文件得到的,那么对该区域的访问将导致段错误(Segmentation fault)。
4. 参数
void *addr: 这是之前调用mmap成功返回的地址。它标识了要解除映射的内存区域的起始地址。- 重要:
addr必须是mmap返回的确切地址。如果传递一个mmap返回的地址加上某个偏移量,行为是未定义的,通常会导致munmap失败 (EINVAL)。
- 重要:
size_t length: 这是要解除映射的内存区域的长度(以字节为单位)。- 重要: 这个
length应该与当初调用mmap时指定的length相匹配,或者至少覆盖你想解除映射的部分。如果length为 0,munmap调用无效。
- 重要: 这个
5. 返回值
- 成功时: 返回 0。
- 失败时: 返回 -1,并设置全局变量
errno来指示具体的错误原因(例如EINVALaddr不是有效的映射地址或length为 0 且addr有效,ENOMEM指定的地址范围不完全在已映射区域内等)。
重要提示: 必须检查 munmap 的返回值! 虽然很多示例代码忽略了,但 munmap 是可能失败的。忽略错误可能导致资源泄漏(虽然操作系统在进程退出时会清理所有映射,但在长时间运行的程序中,不清理可能导致问题)。
6. 相似函数,或关联函数
mmap: 与munmap相对应,用于创建内存映射。msync: 在调用munmap之前,如果需要确保MAP_SHARED映射的修改已写入文件,可以先调用msync。mprotect: 修改映射区域的保护属性,但这不涉及解除映射。free: 用于释放通过malloc分配的堆内存,与munmap释放mmap的内存相对应(尽管mmap有时也被malloc内部用于分配大块内存)。
7. 示例代码
示例 1:基本的 mmap 和 munmap 使用
这个例子结合了 mmap 和 munmap,展示了它们的标准使用流程。
#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h> // open, O_RDONLY
#include <unistd.h> // close, fstat
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
int main(int argc, char *argv[]) {
int fd;
struct stat sb;
char *mapped_memory;
size_t file_length;
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 打开文件
fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 获取文件大小
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
file_length = sb.st_size;
if (file_length == 0) {
printf("File is empty.\n");
close(fd);
exit(EXIT_SUCCESS);
}
// 3. 创建内存映射
mapped_memory = mmap(NULL, file_length, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("File '%s' mapped successfully. Address: %p, Length: %zu bytes\n",
argv[1], (void*)mapped_memory, file_length);
// 4. 使用映射的内存 (这里只是简单打印第一个字符)
printf("First character of the file: '%c'\n", mapped_memory[0]);
// ... 这里可以进行更多对 mapped_memory 的读/写操作 ...
// 5. 关键步骤:解除内存映射 - 释放资源
if (munmap(mapped_memory, file_length) == -1) {
// munmap 失败!这是一个需要处理的错误
perror("CRITICAL ERROR: munmap failed");
// 即使 munmap 失败,也应该尝试关闭文件
close(fd);
exit(EXIT_FAILURE); // 或根据应用逻辑决定如何处理
}
printf("Memory unmapped successfully.\n");
// 6. 关闭文件描述符 (可以在 munmap 之前或之后)
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
printf("File closed.\n");
printf("Program finished successfully.\n");
return 0;
}
代码解释:
- 打开文件并获取其大小。
- 调用
mmap将整个文件映射到内存。 - 检查
mmap是否成功。 - 使用映射的内存(示例中只打印了第一个字符)。
- 关键: 调用
munmap(mapped_memory, file_length)来解除映射。这是释放mmap资源的关键步骤。 - 最重要: 检查
munmap的返回值。如果返回 -1,打印错误信息并退出(或按应用逻辑处理)。 - 最后关闭文件描述符。
示例 2:处理 MAP_SHARED 映射的清理
这个例子强调在解除 MAP_SHARED 映射前使用 msync 确保数据写入。
#include <sys/mman.h> // mmap, munmap, msync
#include <sys/stat.h> // fstat
#include <fcntl.h> // open
#include <unistd.h> // close, fstat
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // memset
int main(int argc, char *argv[]) {
int fd;
struct stat sb;
char *mapped_memory;
size_t file_length = 4096; // 至少一页大小
const char *data_to_write = "Data written by process using MAP_SHARED and msync.";
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 打开或创建文件 (为了确保文件足够大,这里先创建/截断)
fd = open(argv[1], O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 确保文件至少有一页大小
if (ftruncate(fd, file_length) == -1) {
perror("ftruncate");
close(fd);
exit(EXIT_FAILURE);
}
// 2. 获取文件大小 (确认)
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(EXIT_FAILURE);
}
file_length = sb.st_size;
printf("File size is %zu bytes.\n", file_length);
// 3. 创建共享内存映射
mapped_memory = mmap(NULL, file_length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_memory == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("File '%s' mapped for shared read/write. Address: %p\n", argv[1], (void*)mapped_memory);
// 4. 修改映射的内存
// 清零前几字节
memset(mapped_memory, 0, 100);
// 写入我们的数据
size_t data_len = strlen(data_to_write);
if (data_len < file_length) {
for (size_t i = 0; i < data_len; ++i) {
mapped_memory[i] = data_to_write[i];
}
// 或 memcpy(mapped_memory, data_to_write, data_len);
}
printf("Modified memory via mmap.\n");
// 5. (重要) 强制同步修改到文件
// 对于 MAP_SHARED 映射,这是一个好习惯,尤其是在 munmap 之前
if (msync(mapped_memory, data_len, MS_SYNC) == -1) {
perror("msync");
// 即使 msync 失败,也尝试清理
} else {
printf("Changes synced to file using msync.\n");
}
// 6. 解除内存映射
if (munmap(mapped_memory, file_length) == -1) {
perror("munmap");
close(fd);
exit(EXIT_FAILURE);
}
printf("Memory unmapped.\n");
// 7. 关闭文件描述符
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
printf("File closed.\n");
printf("Process finished. Check the file content.\n");
return 0;
}
代码解释:
- 以读写模式打开或创建文件,并使用
ftruncate确保文件至少有一页大小。 - 调用
mmap创建MAP_SHARED映射。 - 修改映射的内存。
- 关键: 在
munmap之前调用msync(mapped_memory, data_len, MS_SYNC)。这确保了对共享映射区域的修改被强制写回到文件中。MS_SYNC会等待写入操作完成。 - 调用
munmap解除映射。 - 检查
munmap和msync的返回值。 - 关闭文件描述符。
示例 3:错误处理和多次映射/解除映射
这个例子展示了在一个程序中进行多次 mmap/munmap 操作,并强调了正确的错误处理。
#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h> // open
#include <unistd.h> // close, fstat
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
// 函数:安全地映射文件的一部分
int safe_mmap_part(const char *filename, size_t offset, size_t length, char **mapped_ptr) {
int fd = -1;
struct stat sb;
char *mapped = MAP_FAILED;
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open in safe_mmap_part");
goto error;
}
if (fstat(fd, &sb) == -1) {
perror("fstat in safe_mmap_part");
goto error;
}
// 检查偏移和长度是否有效
if (offset >= (size_t)sb.st_size || length == 0 || offset + length > (size_t)sb.st_size) {
fprintf(stderr, "Invalid offset or length for file '%s'\n", filename);
goto error;
}
// 确保 offset 是页对齐的 (虽然 mmap 通常能处理,但最好自己保证)
long pagesize = sysconf(_SC_PAGESIZE);
if (offset % pagesize != 0) {
fprintf(stderr, "Offset %zu is not page-aligned (%ld)\n", offset, pagesize);
goto error;
}
mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
if (mapped == MAP_FAILED) {
perror("mmap in safe_mmap_part");
goto error;
}
// 成功,设置输出参数
*mapped_ptr = mapped;
close(fd); // mmap 成功后可以关闭 fd
return 0; // Success
error:
if (mapped != MAP_FAILED) {
munmap(mapped, length); // 尝试清理,尽管可能失败
}
if (fd != -1) {
close(fd);
}
*mapped_ptr = MAP_FAILED;
return -1; // Failure
}
int main(int argc, char *argv[]) {
char *map1 = MAP_FAILED, *map2 = MAP_FAILED;
int result1, result2;
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 映射文件的前 1KB
result1 = safe_mmap_part(argv[1], 0, 1024, &map1);
if (result1 == 0 && map1 != MAP_FAILED) {
printf("Successfully mapped first 1KB of '%s' at %p\n", argv[1], (void*)map1);
printf("First char of part 1: '%c'\n", map1[0]);
} else {
printf("Failed to map first part.\n");
}
// 尝试映射文件的 1KB-2KB 部分 (偏移 1024)
result2 = safe_mmap_part(argv[1], 1024, 1024, &map2);
if (result2 == 0 && map2 != MAP_FAILED) {
printf("Successfully mapped 1KB starting at offset 1024 of '%s' at %p\n", argv[1], (void*)map2);
// 打印这部分的第一个字符
printf("First char of part 2: '%c'\n", map2[0]);
} else {
printf("Failed to map second part.\n");
}
// --- 清理阶段 ---
// 解除第一个映射
if (map1 != MAP_FAILED) {
if (munmap(map1, 1024) == -1) {
perror("munmap part 1");
// 根据应用决定是否 exit
} else {
printf("Unmapped first part successfully.\n");
}
map1 = MAP_FAILED; // 防止重复解除映射
}
// 解除第二个映射
if (map2 != MAP_FAILED) {
if (munmap(map2, 1024) == -1) {
perror("munmap part 2");
} else {
printf("Unmapped second part successfully.\n");
}
map2 = MAP_FAILED;
}
printf("All mappings cleaned up.\n");
return 0;
}
代码解释:
- 定义了一个
safe_mmap_part函数,它封装了打开文件、检查大小、调用mmap以及错误处理的逻辑。它返回 0 表示成功,-1 表示失败,并通过指针参数mapped_ptr返回映射地址。 - 在
main函数中,调用safe_mmap_part两次,分别映射文件的不同部分。 - 清理阶段: 遍历所有可能有效的映射指针(通过检查是否不等于
MAP_FAILED)并调用munmap。 - 关键: 对每一次
munmap调用都检查了返回值。如果失败,会打印错误信息。 - 解除映射后,将指针设置回
MAP_FAILED,这是一种防止重复解除映射的好习惯。
总结来说,munmap 是管理 mmap 资源的关键函数。养成始终检查 munmap 返回值的习惯,并在适当的时候(尤其是 MAP_SHARED 映射)结合 msync 使用,对于编写健壮和高效的 Linux 程序至关重要。
1214

被折叠的 条评论
为什么被折叠?



