为什么大厂爱不释手
如微信的MMKV 组件、美团的Logan组件,还有微信的日志模块xlog,为什么大厂偏爱它呢?他到底有什么魔力么?我认为主要原因如下:
- 跨平台,C++编写,可以支持多平台
- 跨进程,通过文件共享可以实现多个进程内存共享,实现进程通信
- 高性能,实现用户空间和内核空间的零拷贝,速度快且节约内存等
- 高稳定,页中断保护神,由操作系统实现的,稳定性可想而知
函数介绍
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr 代表映射的虚拟内存起始地址;
- length 代表该映射长度;
- prot 描述了这块新的内存区域的访问权限;
- flags 描述了该映射的类型;
- fd 代表文件描述符;
- offset 代表文件内的偏移值。
mmap的强大之处在于,它可以根据参数配置,用于创建共享内存,从而提高文件映射区域的IO效率,实现IO零拷贝,后面讲下零拷贝的技术,对比下,决定这些功能的主要就是三个参数,下面一一解释
prot
四种情况如下:
- PROT_EXEC,代表该内存映射有可执行权限,可以看成是代码段,通常存储CPU可执行机器码
- PROT_READ,代表该内存映射可读
- PROT_WRITE,代表该内存映射可写
- PROT_NONE,代表该内存映射不能被访问
flags
比较有代表性的如下:
- MAP_SHARED,创建一个共享映射区域
- MAP_PRIVATE,创建一个私有映射区域
- MAP_ANONYMOUS,创建一个匿名映射区域,该情况只需要传入-1即可
- MAP_FIXED,当操作系统以addr为起始地址进行内存映射时,如果发现不能满足长度或者权限要求时,将映射失败,如果非MAP_FIXED,则系统就会再找其他合适的区域进行映射
fd
当参数fd不等于0时,内存映射将与文件进行关联,如果等于0,就会变成匿名映射,此时flags必为MAP_ANONYMOUS
应用场景
一个mmap竟有如此丰富的功能,从申请分配内存到加载动态库,再到进程间通信,真的是无所不能,强大到让人五体投地。下面就着四种情况,拿一个我最关心的父子进程通信来举例看下,实现一个简单的父子进程通信逻辑,毕竟我们学习的目的就是为了应用,光有理论怎么能称之为合格的博客呢?
父子进程共享内存
#include <iostream> #include <sys/wait.h> #include <unistd.h> #include <sys/mman.h> int main() { pid_t c_pid = fork(); char* shm = (char*)mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (c_pid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (c_pid > 0) { printf("parent process pid: %d\n", getpid()); sprintf(shm, "%s", "hello, my child"); printf("parent process got a message: %s\n", shm); wait(nullptr); } else { printf("child process pid: %d\n", getpid()); sprintf(shm, "%s", "hello, father."); printf("child process got a message: %s\n", shm); exit(EXIT_SUCCESS); } return EXIT_SUCCESS; }
运行后打印如下
parent process pid: 87799 parent process got a message: hello, my child child process pid: 87800 child process got a message: hello, father. Process finished with exit code 0
用mmap创建了一块匿名共享内存区域,fd传入**-1和MAP_ANONYMOUS配置实现匿名映射,使用MAP_SHARED**创建共享区域,使用fork函数创建子进程,这样来实现子进程通信,通过sprintf将格式化后的数据写入到共享内存中。
通过简单的几行代码就实现了跨进程通信,如此简单,这么强大的东西,背后有什么支撑么?带着问题我们接着一探究竟。
MMAP背后的保护神
说到MMAP的保护神,首页了解下内存页:在页式虚拟存储器中,会在虚拟存储空间和物理主存空间都分割为一个个固定大小的页,为线程分配内存是也是以页为单位。比如:页的大小为 4K,那么 4GB 存储空间就需要4GB/4KB=1M 条记录,即有 100 多万个 4KB 的页,内存页中,当用户发生文件读写时,内核会申请一个内存页与文件进行读写操作,如图
这时如果内存页中没有数据,就会发生一种中断机制,它就叫缺页中断,此中断就是MMAP的保护神,为什么这么说呢?我们知道mmap函数调用后,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存,当访问这些没有建立映射关系的虚拟内存时,CPU加载指令发现代码段是缺失的,就触发了缺页中断,中断后,内核通过检查虚拟地址的所在区域,发现存在内存映射,就可以通过虚拟内存地址计算文件偏移,定位到内存所缺的页对应的文件的页,由内核启动磁盘IO,将对应的页从磁盘加载到内存中。最终保护mmap能顺利进行,无私奉献。了解完缺页中断,我们再来细聊下mmap四种场景下的内存分配原理
四种场景分配原理
上面是一个简单的原理总结,并没有详细的展开,感兴趣可以自己查查资料哈。
总结
本次分享,主要介绍了mmap的四种应用场景,通过一个实例验证了父子进程间的通信,并深入mmap找到它的保护神,且深入了解到mmap在四种场景下,操作系统是如何组织分配,通过对这些的了解,在你之后的mmap实战应用有了更好的理论基础,可以根据不同的需求,不同的性能要求等,选择最合适的实现。