详细解析Linux的mmap(内存映射)
mmap(Memory Map,内存映射)是 Linux 系统编程中最迷人、最强大,但也最容易出错的机制之一。
它打破了"文件是文件,内存是内存"的界限,让程序员可以像操作内存数组一样直接操作文件,甚至直接操作硬件(在嵌入式开发中尤为重要)。
以下是对 mmap 的全方位深度解析。
1. 核心概念:什么是内存映射?
在传统的 read/write 模型中,如果想修改文件:
- 内核态:内核把数据从磁盘读到内核高速缓存(Page Cache)。
- 拷贝 :内核把数据从 Page Cache **拷贝**到用户进程的缓冲区(User Buffer)。
- 修改:用户在 User Buffer 修改数据。
- 拷贝 :用户把数据拷贝回内核。
- 内核态:内核把数据刷回磁盘。
关键词:内核->Page Cache(页缓存)->用户缓存并修改->Page Cache(页缓存)->内核缓存->磁盘
mmap 模型:
mmap 请求内核:"请把这个文件直接'投影'到我的虚拟内存里。"
- 零拷贝 (Zero Copy):内核直接把文件的 Page Cache 映射到用户的虚拟地址空间。
- 直接操作:用户进程直接读写这段内存,实际上就是在读写内核的 Page Cache。
- 自动同步:内核负责在后台将 Page Cache 的变动刷新到磁盘。
一句话总结:mmap 建立了"进程虚拟内存"与"文件对象"之间的一座桥梁。
2. 函数原型与关键参数
头文件:<sys/mman.h>
c
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
| 参数 | 含义 | 详解 |
|---|---|---|
| addr | 映射起始地址 | 通常设为 NULL,让内核自动选择一块空闲地址。 |
| length | 映射长度 | 想要映射多少字节(通常是文件大小)。 |
| prot | 保护权限 | PROT_READ (可读), PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE (不可访问)。必须与 open 时的权限匹配。 |
| flags | 映射标志 | 关键参数! 决定了内存修改是否同步回文件(见下文)。 |
| fd | 文件描述符 | 由 open() 返回的句柄。 |
| offset | 文件偏移量 | 从文件的哪里开始映射。必须是分页大小(通常4KB)的整数倍(如 0, 4096...)。 |
返回值:
- 成功:返回映射区的首地址指针。
- 失败:返回
MAP_FAILED(即(void *)-1),注意不是 NULL。
3. 两种核心模式:MAP_SHARED vs MAP_PRIVATE
这是 flags 参数中最关键的选择:
A. MAP_SHARED(变动是共享的)
- 含义 :你对内存的修改会写回磁盘文件。
- 可见性:其他映射了该文件的进程也能立即看到你的修改。
- 用途 :
- 文件操作 :替代
read/write修改文件。 - 进程间通信 (IPC) :两个进程
mmap同一个文件,实现最高效的数据共享。 - 硬件操作 :嵌入式中映射
/dev/mem来操作寄存器。
- 文件操作 :替代
B. MAP_PRIVATE(写时复制,Copy-On-Write)
- 含义 :你对内存的修改不会写回磁盘文件。
- 机制:当你第一次尝试修改数据时,内核会悄悄拷贝一份数据给你(私有副本),你修改的是副本。
- 用途 :
- 加载动态库 :
.so文件加载到内存时,代码段是共享的,但数据段是私有的。 - 调试器:在不破坏源文件的情况下修改内存中的程序逻辑。
- 加载动态库 :
4. 嵌入式开发专属:mmap 操作硬件寄存器
在您的 i.MX6ULL 开发中,mmap 是驱动开发和裸机应用的核心。
原理:
CPU 有物理地址空间,硬件外设(如 GPIO 控制器)映射在特定的物理地址上(查数据手册可知,例如 0x20AC000)。Linux 用户态程序不能直接访问物理地址,必须用 mmap 将物理地址映射成虚拟地址。
伪代码示例:
c
int fd = open("/dev/mem", O_RDWR); // 打开物理内存设备
// 将物理地址 0x20AC000 开始的 4KB 映射到虚拟内存
unsigned char *gpio_base = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x20AC000);
// 现在可以直接操作寄存器点灯了!
*(volatile unsigned int *)(gpio_base + 0x04) = 0xFF; // 写寄存器
5. mmap 与 read/write 的性能对比
很多初学者认为 mmap 总是比 read/write 快,这是误区。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 小文件 / 顺序读写 | read / write |
mmap 建立映射和销毁映射有较大的系统开销(Setup Cost)。对于简单读写,系统调用反而更快。 |
| 大文件 / 随机读写 | mmap |
read/write 需要频繁移动文件指针 (lseek) 并进行多次内存拷贝。mmap 直接通过指针跳转,效率极高。 |
| 频繁修改文件 | mmap |
修改内存比调用 write 写入磁盘快得多。 |
| 流式数据 (Pipe/Socket) | read / write |
mmap 只能映射可定位的文件(seekable),不能映射管道或套接字。 |
6. 实战中的"坑"与注意事项
A. 缺页异常 (Page Fault) 是隐形成本
mmap 调用返回极快,因为此时内核并没有真的把文件读入内存。
-
当你第一次访问
ptr[0]时,CPU 触发缺页异常。 -
内核捕获异常,暂停你的进程,去磁盘读取数据到 Page Cache,修改页表。
-
恢复进程执行。
这意味着:mmap 是"懒加载"的。
B. 文件大小限制 (SIGBUS 错误)
mmap 不能改变文件大小。
- 假设文件只有 100 字节。
- 你
mmap了 4096 字节(一页)。 - 你可以读写 0~99 字节。
- 如果你写
ptr[200]:虽然它在映射的 4KB 页内,但它超过了文件实际大小。你会收到 SIGBUS 信号,程序崩溃。 - 解决: 在
mmap之前,先用lseek+write或者ftruncate()把文件撑大。
C. 同步问题
MAP_SHARED 并不保证立即可靠地写入磁盘,它只是写到了内核缓存。
- 如果系统断电,数据可能丢失。
- 强制同步: 使用
msync(addr, len, MS_SYNC)函数,类似于文件 I/O 的fsync。
D. 解除映射
使用完毕后,必须调用:
c
munmap(void *addr, size_t length);
如果不调用,进程结束后内核才会自动回收,因此长期运行的服务会造成虚拟内存泄漏。
7. 总结代码示例
这是一个标准的用 mmap 修改文件的模板:
c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *filepath = "data.bin";
// 1. 打开文件
int fd = open(filepath, O_RDWR);
if (fd < 0) { perror("open"); return 1; }
// 2. 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) { perror("fstat"); return 1; }
// 3. 执行内存映射
// 注意:MAP_SHARED 才能写回文件
char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) { perror("mmap"); return 1; }
// 4. 像操作数组一样操作文件
printf("Old: %c\n", mapped[0]);
mapped[0] = 'X'; // 修改内存,实际上就是修改文件
// 5. (可选) 强制同步回磁盘
msync(mapped, sb.st_size, MS_SYNC);
// 6. 解除映射
munmap(mapped, sb.st_size);
// 7. 关闭文件
close(fd);
return 0;
}