mmap是linux提供的系统调用,用于将文件/设备映射到进程的虚拟内存地址中。随后进程可以像访问自己的内存一样来访问文件。
这是以前很多中文博客中对mmap的解释,多半是从英文文档中按字面意思翻译过来的,没有解释清楚跟mmap的实际作用。
比如为什么要“像访问自己的内存一样”访问文件?这有什么好处?什么场景适合用mmap、什么场景会带来overhead?这些问题我们一点点来看。
1
2
3
4
5
6
7
#include <sys/mman.h>
void *mmap(size_t length;
void addr[length], size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(size_t length;
void addr[length], size_t length);
- Direct Access: It maps a file (or a portion of it) into a program’s memory, bypassing explicit read and write system calls.
- Lazy Loading: It uses demand paging, meaning data is only loaded from disk into physical RAM when a specific memory location is actually accessed.
- Shared Memory: By using the MAP_SHARED flag, multiple processes can map the same file and communicate through it, creating an efficient form of Inter-Process Communication (IPC).
根据上述介绍,使用mmap之后,读取文件数据不再需要通过读写系统调用。
TODO:这能节省什么?切换到内核态的开销吗?具体是指什么样的系统调用?page fault有切换到内核态的开销吗?
mmap会按需加载数据,当数据不在内存时需要触发缺页中断,等待数据加载到page cache中。区别于普通read操作的是mmap不需要再从page cache拷贝数据到用户空间,而是由内核修改进程的页表,将虚拟地址指向刚才的page cache物理地址,减少了一次内核态到用户态的内存拷贝操作。
但既然是进page cache的,内存何时淘汰就由内核说了算,出现性能抖动时排查和调试都很复杂。这也是为什么数据库宁可自己用DirectIO+自己实现缓存管理,而不使用mmap。
mmap的映射方式
mmap默认是映射一个文件,此外还可以通过MAP_ANONYMOUS直接将页表映射到物理内存,这也是一种常见的内存申请方式。使用mmap的场景有:
- MAP_ANONYMOUS + MAP_PRIVATE: 申请大块内存
- MAP_ANONYMOUS + MAP_SHARED: 进程间通信,比如父子进程共享内存
- fd + MAP_PRIVATE: 加载动态库
- fd + MAP_SHARED: 内存映射IO
- MAP_SHARED: 修改对其他进程可见;
- MAP_PRIVATE:修改对其他进程不可见; 用fd+MAP_SHARED就可以实现page cache内“一份文件被多个进程共享”的效果。
除此以外还有:
巨页映射 (MAP_HUGETLB):利用内核的 Huge Pages(大页)机制分配内存(如 2MB 或 1GB 的页),可以减少页表条目
预热映射 (MAP_POPULATE):在映射建立时就提前填充页表(对于文件映射会触发预读),从而避免后续访问时产生的缺页中断
锁定映射 (MAP_LOCKED):将映射的内存锁定在物理内存中,防止其被交换(Swap)到磁盘
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
void anonymous_shared_example() {
std::cout << "--- 场景 1: 匿名共享映射 (父子进程通信) ---" << std::endl;
size_t size = 4096;
// MAP_ANONYMOUS: 不关联文件
// MAP_SHARED: 跨进程可见
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
return;
}
int* shared_data = static_cast<int*>(ptr);
*shared_data = 100; // 初始值
if (fork() == 0) { // 子进程
std::cout << "[子进程] 读取初始值: " << *shared_data << std::endl;
*shared_data = 200; // 修改共享内存
std::cout << "[子进程] 已修改值为 200" << std::endl;
munmap(ptr, size);
exit(0);
} else { // 父进程
wait(NULL); // 等待子进程结束
std::cout << "[父进程] 子进程修改后的值: " << *shared_data << std::endl;
munmap(ptr, size);
}
std::cout << std::endl;
}
void file_shared_example() {
std::cout << "--- 场景 2: 文件共享映射 (直接操作磁盘文件) ---" << std::endl;
const char* filepath = "test_mmap.txt";
int fd = open(filepath, O_RDWR | O_CREAT | O_TRUNC, 0666);
// 必须先扩展文件大小,否则映射后写入会触发 SIGBUS 错误
const char* text = "Hello Mmap!";
write(fd, text, strlen(text));
// 将文件映射到内存
size_t size = 1024;
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd); // 映射建立后即可关闭 fd
if (ptr == MAP_FAILED) {
perror("mmap");
return;
}
char* file_content = static_cast<char*>(ptr);
std::cout << "[读取文件内容]: " << file_content << std::endl;
// 直接修改内存,相当于修改了 Page Cache,系统会自动刷回磁盘
strcpy(file_content, "Mmap is fast!");
// 显式同步回磁盘(可选,内核通常会异步处理)
msync(ptr, size, MS_SYNC);
std::cout << "[修改完成] 请查看 " << filepath << " 的内容" << std::endl;
munmap(ptr, size);
}
int main() {
// 场景 1:申请内存,用于进程间共享数据
anonymous_shared_example();
// 场景 2:映射文件,用于高性能文件读写
file_shared_example();
return 0;
}
munmap, madvise
二者都可以用于释放内存,区别在于mmap会解除内存映射,释放对应的虚拟内存,如果进程再访问对应虚拟内存地址会直接segment fault;madvise释放物理内存但虚拟地址依然是完整的,后续如果再访问该地址,内核会重新触发缺页中断来分配新的内存页。
madivse有若干flag可以执行不同的操作:
- MADV_WILLNEED:告诉内核“我马上要用这段内存了”。内核会在后台异步地将数据从磁盘预读进 Page Cache,从而隐藏后续访问时的缺页中断延迟。
- MADV_SEQUENTIAL:建议内核进程将顺序访问这段内存。内核会激进地进行预读(read-ahead),并在读取后尽快释放前面已经访问过的物理页,避免污染 Page Cache。
- MADV_RANDOM:告诉内核访问是完全随机的。内核将关闭预读机制,避免浪费大量的磁盘 I/O 吞吐量。
The End