`mmap` 系统调用详解
mmap
是 Unix/Linux 系统中一个强大且多用途的系统调用,用于将文件或设备映射到进程的地址空间,实现内存映射I/O。
1. 函数的概念与用途
mmap
(内存映射)函数允许程序将文件或其他对象直接映射到其地址空间,这样文件内容就可以通过内存指针直接访问,而不需要使用传统的 read
/write
系统调用。
主要用途:
- 文件I/O优化:提供对文件的高效随机访问,避免频繁的
read
/write
系统调用 - 进程间通信:通过映射同一文件实现共享内存
- 内存分配:用于实现高效的内存分配器(如
malloc
) - 程序加载:用于加载可执行文件和共享库
- 零拷贝I/O:在某些情况下可以实现零拷贝网络传输
2. 函数的声明与出处
mmap
函数定义在 <sys/mman.h>
头文件中,是 POSIX 标准的一部分。
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
3. 返回值的含义与取值范围
- 成功时:返回映射区域的起始地址
- 失败时:返回
MAP_FAILED
(通常是(void *)-1
),并设置errno
来指示错误原因
常见错误码 (errno):
EACCES
:文件描述符不支持所请求的访问类型EAGAIN
:文件已被锁定,或太多内存被锁定EBADF
:文件描述符无效EINVAL
:参数无效(如长度为零、不支持的保护或标志)ENFILE
:已达到系统对打开文件总数的限制ENODEV
:文件所在文件系统不支持内存映射ENOMEM
:没有可用内存,或进程超出内存映射限制EPERM
:操作被拒绝(如尝试写入只写区域)
4. 参数的含义与取值范围
-
void *addr
- 含义:建议的映射起始地址(通常设为 NULL,由内核自动选择)
- 取值范围:任何有效的地址或 NULL
-
size_t length
- 含义:要映射的字节数
- 取值范围:大于 0 的值
-
int prot
- 含义:内存保护标志,指定访问权限
- 常见取值(使用按位或组合):
PROT_NONE
:页面不可访问PROT_READ
:页面可读PROT_WRITE
:页面可写PROT_EXEC
:页面可执行
-
int flags
- 含义:映射类型和选项
- 常见取值(使用按位或组合):
MAP_SHARED
:共享映射,修改对其他进程可见MAP_PRIVATE
:私有映射,修改不会写回文件MAP_ANONYMOUS
:不映射文件,创建匿名内存MAP_FIXED
:必须使用指定地址MAP_LOCKED
:锁定页面在内存中
-
int fd
- 含义:要映射的文件描述符
- 取值范围:有效的文件描述符(对于匿名映射,设为 -1)
-
off_t offset
- 含义:文件中的偏移量,必须是系统页面大小的倍数
- 取值范围:非负值,通常是页面大小的倍数
5. 函数使用案例
案例1:文件映射与读取
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main() {const char *filename = "example.txt";const int file_size = 1024;// 创建并写入示例文件int fd = open(filename, O_RDWR | O_CREAT, 0644);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}// 调整文件大小if (ftruncate(fd, file_size) == -1) {perror("ftruncate");close(fd);exit(EXIT_FAILURE);}// 映射文件到内存char *mapped = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mapped == MAP_FAILED) {perror("mmap");close(fd);exit(EXIT_FAILURE);}// 通过内存访问文件内容const char *message = "Hello, mmap!";strncpy(mapped, message, strlen(message));printf("File content via mmap: %s\n", mapped);// 取消映射并关闭文件if (munmap(mapped, file_size) == -1) {perror("munmap");}close(fd);return 0;
}
案例2:进程间共享内存
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>#define SHM_SIZE 1024int main() {// 创建共享内存区域(匿名映射)char *shared_mem = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (shared_mem == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}pid_t pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) { // 子进程printf("Child process writing to shared memory\n");strcpy(shared_mem, "Message from child process");exit(EXIT_SUCCESS);} else { // 父进程wait(NULL); // 等待子进程结束printf("Parent process reading from shared memory: %s\n", shared_mem);// 清理if (munmap(shared_mem, SHM_SIZE) == -1) {perror("munmap");}}return 0;
}
案例3:高效大数据处理
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>#define FILE_SIZE (1024 * 1024 * 100) // 100MBint main() {const char *filename = "large_file.bin";int fd = open(filename, O_RDWR | O_CREAT, 0644);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}// 调整文件大小if (ftruncate(fd, FILE_SIZE) == -1) {perror("ftruncate");close(fd);exit(EXIT_FAILURE);}// 映射文件char *mapped = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mapped == MAP_FAILED) {perror("mmap");close(fd);exit(EXIT_FAILURE);}// 使用内存映射进行高效处理clock_t start = clock();for (size_t i = 0; i < FILE_SIZE; i++) {mapped[i] = (char)(i % 256); // 填充数据}clock_t end = clock();double elapsed = (double)(end - start) / CLOCKS_PER_SEC;printf("Processed %d MB in %.2f seconds (%.2f MB/s)\n", FILE_SIZE / (1024 * 1024), elapsed, (FILE_SIZE / (1024.0 * 1024.0)) / elapsed);// 清理if (munmap(mapped, FILE_SIZE) == -1) {perror("munmap");}close(fd);remove(filename); // 删除临时文件return 0;
}
6. 编译方式与注意事项
编译命令:
gcc -o mmap_example1 mmap_example1.c
gcc -o mmap_example2 mmap_example2.c
gcc -o mmap_example3 mmap_example3.c
注意事项:
- 权限检查:确保对映射的文件有适当的访问权限
- 对齐要求:
offset
参数必须是系统页面大小的倍数(可用sysconf(_SC_PAGE_SIZE)
获取) - 资源清理:始终使用
munmap()
取消映射,并使用close()
关闭文件描述符 - 错误处理:始终检查返回值并处理可能的错误
- 同步:对于共享映射,修改可能不会立即写回文件,可使用
msync()
强制同步 - 内存保护:注意设置适当的保护标志,避免安全漏洞
- 可移植性:不同系统可能有不同的页面大小和限制
7. 执行结果说明
案例1输出:
File content via mmap: Hello, mmap!
这个示例创建了一个文件,通过内存映射写入内容,然后读取并显示内容。
案例2输出:
Child process writing to shared memory
Parent process reading from shared memory: Message from child process
这个示例演示了父子进程如何通过匿名映射共享内存区域。
案例3输出:
Processed 100 MB in 0.15 seconds (666.67 MB/s)
这个示例展示了使用内存映射处理大文件的高效性,通过直接内存访问避免了频繁的系统调用。
8. 图文总结
以下是 mmap
系统调用的工作流程:
关键点总结:
mmap
提供了一种高效的文件访问方式,避免了频繁的read
/write
系统调用- 支持文件映射和匿名映射两种模式,分别用于文件I/O和进程间通信
- 采用惰性加载机制,只有在实际访问时才会将数据加载到内存
- 对于共享映射,修改可能会自动写回文件(取决于标志)
- 必须正确使用
munmap()
释放映射区域,避免资源泄漏 - 适当设置保护标志和映射选项对性能和安全性至关重要
mmap
是一个强大的系统调用,正确使用可以显著提高I/O性能,但需要仔细考虑内存管理和同步问题。