其实这书里根本没有mmap相关内容,只是也是Linux相关内容,一并归入Linux宇宙介绍了。
mmap() 会在进程的虚拟地址空间中创建一段 内存区域(VMA),并且这段区域可以:
-
映射一个文件
-
映射匿名内存(当作 malloc 替代)
-
映射共享内存(跨进程通信)
-
映射设备文件
-
使用 copy-on-write 技术实现 fork 高效复制
再简单讲:
mmap = 给你一片虚拟地址,背后可以连到文件、内存或其他资源。
mmap 函数原型
cpp
#include <sys/mman.h>
void *mmap (void *__addr, size_t __len, int __prot,
int __flags, int __fd, __off_t __offset)
直观理解:在文件 FD 中从偏移量 OFFSET 开始,映射长度为 LEN 字节的地址
返回值为实际选择的映射地址,或者在出错时为 MAP_FAILED(此时会设置 `errno')。成功的 `mmap' 调用会释放受影响区域的任何先前映射。
addr ------ 建议映射地址(一般写 NULL)
-
传
NULL让内核自动选择一个合适的地址 -
也可以指定地址 请求 映射到某一处(不建议)
length ------ 映射大小
必须是 页对齐 (4KB 的整数倍)。
如果不是,内核会自动向上取整。
prot ------ 页权限
| 宏 | 意义 |
|---|---|
PROT_READ |
可读 |
PROT_WRITE |
可写 |
PROT_EXEC |
可执行 |
PROT_NONE |
不可访问 |
prot 在 mmap / mprotect 中是 int 类型的掩码(可以 | 组合):
cpp
mmap(NULL, size, PROT_READ | PROT_WRITE, ... );
-
PROT_WRITE不等于能写文件:-
要把映射写回到磁盘,必须使用
MAP_SHARED;MAP_PRIVATE写入只影响私有副本(COW),不修改文件。 -
而如果
fd是只读打开(O_RDONLY),内核可能拒绝PROT_WRITE映射(权限检查)。
-
-
PROT_EXEC受安全策略影响:-
即使你传了
PROT_EXEC,系统安全模块(SELinux、PaX、grsecurity、W^X)或 CPU 上的 NX bit 也可能阻止实际执行。 -
JIT 引擎常用"写时移除 EXEC,写完后设回 EXEC"的策略。
-
-
PROT_WRITE常与PROT_READ一起用:- 虽然位是独立的,但一些系统不允许
PROT_WRITE而没有PROT_READ;在可移植代码里建议PROT_READ|PROT_WRITE。
- 虽然位是独立的,但一些系统不允许
-
mprotect可改变权限:mprotect(addr, len, PROT_* )用于修改映射权限;改变为更宽松权限可能因为进程权限/文件权限而失败(EACCES等)。
flags ------ 控制映射行为
① 共享/私有映射(必须二选一)
| flag | 意义 |
|---|---|
MAP_SHARED |
写会同步到文件,多个进程共享 |
MAP_PRIVATE |
Copy-on-write,写不会改变文件 |
这是最重要的 flag,决定行为完全不同。
② 匿名映射(不映射文件)
MAP_ANONYMOUS或者MAP_ANON(这两宏是一样的)
此时 fd 必须为 -1,offset 必须为 0
③ 其它常用 flags
| flag | 意义 |
|---|---|
MAP_FIXED |
强制使用 addr(危险,会覆盖已有映射) |
MAP_POPULATE |
提前分配页框,避免 page fault |
MAP_LOCKED |
将页锁在内存,不允许换出 |
MAP_HUGETLB |
使用大页(Huge Page) |
fd ------ 文件描述符
如果是文件映射:fd就用open打开的文件描述符
如果是匿名映射:fd直接赋值-1
offset ------ 文件偏移
必须按页对齐。
例如 offset = 4096、8192 等。
什么是匿名映射?
在 mmap() 中,映射分两类:
1. 文件映射(File-backed mapping)
-
使用 真实文件 做后端存储
-
调用方式一般类似:
cpp
int fd = open("data.bin", O_RDWR);
void* p = mmap(nullptr, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
特点:
-
修改内存 → 写到文件
-
文件内容被映射进内存
-
文件大小必须足够大,否则会 SIGBUS(访问超界)
2. 匿名映射(Anonymous Mapping)
匿名映射 没有对应的文件 ,内核从 swap 分配私有内存页。
创建方式:
cpp
void* p = mmap(nullptr, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
特点:
-
没有文件后端,fd 必须为 -1
-
内容初始全是 0
-
修改不会写回任何地方
-
对进程来说就像是
malloc()分配的一大块内存
匿名映射 = malloc 的更底层版本。
实际上
-
malloc 小块:用 brk 扩展堆
-
malloc 大块:用 mmap 匿名映射
应用场景:
-
操作大块连续内存(大 buffer)
-
实现共享内存(如果加 MAP_SHARED)
munmap
cpp
int munmap(void *addr, size_t length);
取消映射
必须与 size 对应,否则会错误或部分解除。
mprotect
cpp
int mprotect(void *addr, size_t len, int prot);
修改映射区域权限。可以把区域改为只读、可写、不可执行等。
msync
cpp
int msync (void *__addr, size_t __len, int __flags);
刷新到磁盘。用于 MAP_SHARED:
| 常量 | 意义 |
|---|---|
MS_SYNC |
同步写 |
MS_ASYNC |
异步写 |
MS_INVALIDATE |
丢弃缓存重新读磁盘 |
madvise
cpp
int madvise (void *__addr, size_t __len, int __advice)
优化内存访问模式
常见:
| 常量 | 意义 |
|---|---|
MADV_RANDOM |
随机访问 |
MADV_SEQUENTIAL |
顺序访问 |
MADV_WILLNEED |
内核提前预读 |
MADV_DONTNEED |
这段不用了,可以回收页 |
示例代码1
通过mmap的方式追加写入文件
cpp
#include<sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include<unistd.h>
#include <string.h>
int main() {
//创建并打开一个文件
int fd = open("test.txt", O_RDWR | O_CREAT|O_APPEND, S_IRUSR | S_IWUSR | S_IXUSR);
//计算当前文件大小
int fsize = lseek(fd, 0, SEEK_END);
const char* msg = "hello mmap\n";
size_t len = strlen(msg);
size_t total_len = fsize + len;
// 关键步骤:先扩展文件大小!!
ftruncate(fd, total_len);
//通过mmap的方式读入内存,注意用了MAP_SHARED标志,可以写入文件
char* addr = static_cast<char*>(mmap(NULL, total_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
//把这句话写入文件末尾
strcpy(addr+ fsize, msg);
//同步写
msync(addr, total_len, MS_SYNC);
//取消映射
munmap(addr, total_len);
close(fd);
}
mmap 的底层原理(进程虚拟内存视角)
cpp
+----------------------+ <-- high address
| stack |
+----------------------+
| shared libraries |
+----------------------+
| heap (malloc) |
+----------------------+
| mmap region | <--- mmap 返回的地址
+----------------------+
| bss / data |
+----------------------+
| text |
+----------------------+ <-- low address
每一次 mmap 会创建一个 VMA(Virtual Memory Area)。
当你第一次访问对应页面时:
-
发生 page fault
-
内核为该页分配物理页
-
如果映射的是文件,加载该页内容
-
返回用户态继续执行
示例代码2:匿名共享内存
匿名映射没有路径,没有文件,所以不同进程 不能通过独立的 mmap 调用做到共享。
所以这个共享只能在fork层面的父子进程实现
子进程继承了整个虚拟地址空间:
cpp
父 addr ----+
| 指向同一物理页
子 addr ----+
因此父子进程:
-
访问的是同一个缓冲区
-
读写互相可见
-
不需要管 fd
cpp
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
const int SIZE = 4096;
char* addr = (char*)mmap(
NULL,
SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1,
0
);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(1); // 等父进程写
printf("child sees: %s\n", addr);
strcpy(addr, "Hello from child!");
}
else {
// 父进程
strcpy(addr, "Hello from parent!");
sleep(2); // 等子进程写回
printf("parent sees: %s\n", addr);
}
//父子进程都要调用
munmap(addr, SIZE);
return 0;
}
cpp
child sees: Hello from parent!
parent sees: Hello from child!
证明父子进程共享同一片物理内存。
注意,如果子进程使用exec系列函数,转到其他进程,子进程会完全丢弃当前进程的整个虚拟地址空间,并用新程序的 ELF 文件重新创建新的地址空间。这时候mmap创建的匿名内存就不能用了
还有一点,父子进程都可以(也应该)调用 munmap
munmap(addr, SIZE) 的作用是:
-
解除当前进程虚拟地址 → 物理页 的映射
-
但并不会影响:
-
其他进程的映射
-
共享物理页的存在
-
也就是说:
-
父进程 munmap:只影响父进程自己
-
子进程 munmap:只影响子进程自己
-
物理共享页什么时候释放?
→ 最后一个持有映射的进程也 munmap 后,物理页才真正被释放
所以,父子进程都 munmap 是完全正常和正确的。
有点类似C++的共享指针的设计
这个例子用sleep实现同步,其实不太合理,可以使用信号量的方式
cpp
sem_t *sem = mmap(NULL, sizeof(sem_t),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0);
sem_init(sem, 1, 0); // 第二个参数 1 表示可跨进程共享