1. 在谈 mmap 之前
1.1 系统调用 read 的过程
在进程想要读取某个文件时(当然也可以是其他资源,因为 Linux 下 一切皆文件,这里指的是磁盘文件),会发起 read 系统调用。CPU 会从用户态转换到内核态,伴随着 CPU 上下文的切换、快表缓存的失效等;进入内核态后,接下来通过进程在 CPU 的 CR3 寄存器中保留的页表物理地址,找到进程想要访问的文件内容在页缓存中的物理地址。如果找到了,就直接把内容返回给用户态(当然这中间还包含着信号的处理流程,可能又涉及多次用户态、内核态的转化);如果没找到,就再到磁盘中加载。
1.2 系统调用 write 的过程
大体上和 read 相同,不过不同在于:CPU 通过 页表找到页缓存中的内容并作出改变后,write 的调用流程就结束了(页缓存是在磁盘和内存之间加了个中间层,存在于物理内存之上,所有进程对于磁盘的访问操作都会优先从这个页缓存中进行,如果没有的话,再从磁盘中加载进来。在这里我们不难发现,一个进程对页缓存的更改,会对其他进程的访问同样造成影响,即"没有持久化的硬盘操作")。之后内核会自行把页缓存中的内容更改落盘
2. mmap 文件映射
2.1 什么是 mmap
mmap 是 Linux 为了解决上面频繁进行系统调用开销过大而使用的另一个系统调用,包含在 <sys/mman.h> 中。mmap所做的事情,简单概括一下,就是直接把页缓存中的文件,通过页表,直接映射到进程的地址空间当中(更精确的,会把它映射到虚拟地址空间的堆和栈中间的位置)。
2.2 mmap 相比 read 和 write 提供了什么优势
从前面介绍的 read 和 write 的调用流程,我们不难发现,后两者在坏情况下涉及内核到用户、内存到硬盘的两次交互,而且都是比较费时的操作。有了 mmap 之后,我们的操作直接就可以落实到物理内存的页缓存之上,也就是减少了一次拷贝。同时不只是提供更高效率的写文件,也可以通过参数实现在物理内存上映射一块没有后端存储的区域给进程的地址空间(C语言的malloc就是用了 mmap)
2.3 系统调用接口
2.3.1 mmap
cpp
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
addr,表示想让当前进程地址空间的具体哪个区域映射到物理内存,如果给空指针的话,有内核决定len,表示映射区域的长度,或者说映射区域的大小prot,表示映射区域具有的权限,一般读写权限即可,设为PROT_READ、PROT_WRITE(注意,这里的权限要和 fildes 指定的文件描述符权限匹配,不能这里允许写但是文件描述符只允许读)flags,指定映射的其他属性,比如对映射的改变是否共享,通过MAP_SHARED、MAP_PRIVATE指定fildes,要操作的具体文件的文件描述符(Linux下一切皆文件,文件描述符也可能对应设备,比如0、1、2,如果不关联文件、设备,比如 malloc 的话,就可以设置为 -1,表示不需要后端存储设备)off,偏移量,映射起始位置相对于文件开头的偏移量,0表示从头开始映射
2.3.2 munmap
用来释放 mmap,需要传入 mmap 返回的文件映射区域指针,以及映射区域的大小
cpp
int munmap(void *addr, size_t len);
2.4 【示例】最简版 malloc
malloc 其实就是为调用它的进程的物理空间中,塞了一块只有它能访问、修改的内存块,这恰好就可以通过我们的 mmap 来实现,不过需要注意的是,这样的纯内存映射需要通过 mmap 的参数指定为匿名映射,即不对应磁盘文件
cpp
void* my_malloc(int size){
if(size <= 0){
perror("incorrect size");
return nullptr;
}
void* ret_ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(ret_ptr == MAP_FAILED){
perror("mmap fail");
return nullptr;
}
return ret_ptr;
}
void my_free(void* ptr, int size){
if(ptr == nullptr || size <= 0){
return;
}
::munmap(ptr, size);
}