Linux mmap内存映射
mmap
是 Linux 系统中一个非常重要且强大的系统调用,它提供了将文件或设备直接映射到进程地址空间的能力。这种机制在程序设计中用途广泛,从文件 I/O 到进程间通信,再到内存管理,都能看到它的身影。
核心概念
mmap
的核心思想是:让一块虚拟内存区域与一个文件(或其他对象)关联起来。当你读写这块内存时,系统会自动将数据同步到对应的文件中,或者从文件中加载数据到内存。这就像是给文件内容开了一个“直接操作”的窗口。
函数原型
c
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数解析:
addr
: 建议的映射起始地址。通常设为NULL
,让内核自动选择最适合的地址。如果指定,内核会将其作为参考。length
: 要映射的字节数。prot
: 指定内存区域的保护模式(不能与文件的打开模式冲突)。PROT_EXEC
: 页面可执行。PROT_READ
: 页面可读。PROT_WRITE
: 页面可写。PROT_NONE
: 页面不可访问。
flags
: 控制映射行为的标志。这是理解mmap
用法的关键。MAP_SHARED
: 共享映射。对映射区域的修改会写回文件,并且其他映射了同一文件的进程可以看到这些更改。用于文件 I/O 和 IPC。MAP_PRIVATE
: 私有映射。对映射区域的修改不会写回文件,而是创建一个该页的副本(Copy-on-Write)。用于加载动态库、创建进程的私有内存空间。MAP_ANONYMOUS
(或MAP_ANON
): 匿名映射。映射的区域不与任何文件关联,内容初始化为零。常用于分配大块内存(如malloc
的底层实现)或 IPC。使用此标志时,fd
参数被忽略,通常设为 -1。MAP_FIXED
: 强制使用指定的addr
作为映射地址,不建议使用,可能不安全。MAP_LOCKED
: 将映射页面锁定在内存中,防止被换出(swap out)。MAP_POPULATE
: 预先为映射填充页表,建立物理内存的映射,避免后续缺页中断。
fd
: 要映射的文件的文件描述符。如果是匿名映射,则设为 -1。offset
: 从文件开头开始的偏移量,必须是系统页大小(通常为 4096 字节)的整数倍。
返回值:
成功时返回映射区域的起始地址;失败时返回 MAP_FAILED
(即 (void *) -1
)。
解除映射:munmap
c
#include <sys/mman.h>int munmap(void *addr, size_t length);
用于解除一个映射关系。addr
必须是 mmap
返回的地址,length
是之前映射的大小。进程退出时也会自动解除所有映射。
同步回磁盘:msync
c
#include <sys/mman.h>int msync(void *addr, size_t length, int flags);
如果你使用 MAP_SHARED
,内核会在适当时候将脏页写回磁盘。但如果你需要确保数据立刻落盘,可以调用 msync
。
flags
:MS_SYNC
: 同步写,阻塞直到所有修改都物理写入存储设备。MS_ASYNC
: 异步写,调度一次写操作但立即返回。
主要应用场景
1. 文件 I/O(内存映射文件)
这是最经典的用法。相比于传统的 read
/write
系统调用,mmap
有显著优势:
减少数据拷贝: 传统 I/O 需要将数据从内核缓冲区拷贝到用户缓冲区。
mmap
直接将文件映射到用户空间,操作内存就等于操作文件,省去了一次拷贝(CPU copy),尤其对大文件性能提升明显。随机访问更高效: 访问文件任意位置只需简单的指针操作,无需
lseek
。与内核共享缓存: 映射区域由内核的页缓存(Page Cache)支持,可以充分利用系统的缓存机制。
示例代码:读取文件
c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>int main() {int fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open");return 1;}struct stat sb;if (fstat(fd, &sb) == -1) {perror("fstat");return 1;}// 将整个文件映射到内存char *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);if (mapped == MAP_FAILED) {perror("mmap");return 1;}// 现在可以像操作内存一样操作文件内容for (off_t i = 0; i < sb.st_size; i++) {putchar(mapped[i]); // 逐个字符打印}// 解除映射并关闭文件munmap(mapped, sb.st_size);close(fd);return 0;
}
2. 进程间通信 (IPC)
通过 mmap
可以实现高效的进程间共享内存。
方式一(有文件背景): 两个进程使用
MAP_SHARED
映射同一个文件。他们对映射区域的修改会立刻被对方看到。方式二(匿名映射): 使用
MAP_SHARED | MAP_ANONYMOUS
创建一块与文件无关的共享内存区域。通常由父进程在fork()
之前创建,子进程会自然继承这块映射,从而实现通信。
示例代码:匿名共享内存 IPC
c
#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {int *shared_var = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (shared_var == MAP_FAILED) {perror("mmap");return 1;}*shared_var = 100; // 初始值pid_t pid = fork();if (pid == -1) {perror("fork");return 1;}if (pid == 0) { // 子进程printf("Child reads: %d\n", *shared_var);(*shared_var)++; // 修改共享内存printf("Child incremented to: %d\n", *shared_var);munmap(shared_var, sizeof(int)); // 子进程解除映射} else { // 父进程wait(NULL); // 等待子进程结束printf("Parent reads: %d\n", *shared_var);munmap(shared_var, sizeof(int)); // 父进程解除映射}return 0;
}
// 输出可能是:
// Child reads: 100
// Child incremented to: 101
// Parent reads: 101
3. 分配大块内存(malloc
的底层)
glibc
中的 malloc()
函数在需要分配非常大(例如超过 MMAP_THRESHOLD
,默认 128KB)的内存块时,会使用 mmap
而不是 brk/sbrk
。因为 mmap
分配的内存可以独立且方便地被 free()
归还给系统,而 brk
管理的堆内存则容易产生碎片。
4. 加载动态共享库
当你调用 dlopen()
或程序启动时加载 .so
文件,系统就是使用 mmap
将库文件的代码段(.text
)和数据段(.data
, .bss
)映射到进程的地址空间。这通常是 MAP_PRIVATE
映射,这样多个进程可以共享同一份库的物理内存页(写时复制),节省大量内存。
优点与缺点
优点:
高性能文件 I/O: 减少数据拷贝,尤其适合大文件或随机访问。
高效的 IPC: 共享内存是速度最快的 IPC 方式,因为数据不需要在进程间拷贝。
内存高效: 支持写时复制、共享库等高级特性,节省物理内存。
自然对齐: 映射的内存地址总是页对齐的。
缺点:
粒度不灵活: 映射大小必须是页大小的整数倍,可能造成内部碎片。
资源开销: 每次映射都会在进程内核数据结构(如 VMA 链表)中创建新条目,映射大量小文件不划算。
复杂性: 需要程序员自己处理同步问题(如对共享映射的访问需要加锁)。
延迟性错误: 对映射区域的访问可能触发 SIGBUS 或 SIGSEGV 信号(例如文件被截断时访问已映射的区域),错误处理比普通 I/O 更复杂。
总结
mmap
是 Linux 系统编程中一把强大的“瑞士军刀”。它通过巧妙地将文件、内存和进程地址空间结合起来,提供了远超传统 I/O 操作的灵活性和性能。理解并熟练运用 mmap
,尤其是区分 MAP_SHARED
、MAP_PRIVATE
和 MAP_ANONYMOUS
的用途,是成为一名高级 Linux/C 程序员的重要标志。