`munmap`系统调用及示例
这次我们介绍 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
来指示具体的错误原因(例如EINVAL
addr
不是有效的映射地址或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> // exitint 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> // memsetint 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 成功后可以关闭 fdreturn 0; // Successerror: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);}// 映射文件的前 1KBresult1 = 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 程序至关重要。