目录
一.mmap简介
二.为什么需要使用mmap
三.mmap的使用
四.mmap原理
什么是mmap了?从名字上来看是memory map也就是地址映射,是一种内存映射文件的方法。mmap是一个可以将一个文件或者其它对象映射到进程的地址空间实现磁盘的地址和进程虚拟地址空间一段虚拟地址的一一对应关系。通过mmap这个系统调用我们可以让进程之间通过映射到同一个普通文件实现共享内存,普通文件被映射到进程地址空间当中之后,进程可以向访问普通内存一样对文件进行一系列操作。
我们平时再读取文件的时候我们经常使用的方法就是read和write这两个操作系统给我们提供的方法来读写文件的时候,我们需要进行两次拷贝。由于read和write是系统调用所以我们需要先从用户态进入到内核态,然后将磁盘当中的数据拷贝到操作系统的缓冲区当中,然后再将缓冲区当中的数据拷贝到用户态当中。在这个过程当中我们进行了两次拷贝。其过程大致如下图所示:
但是如果我们使用mmap就可以减少一次拷贝这样带来性能上的提升是巨大的。并且我们采用内存操作比read和write要简单一些,我们不需要在用户层定义缓冲区用来保存从内核缓冲区读上来的数据,从而节约了内存的消耗。其大致流程如下:
总结:
1.首先我们来看看mmap这个函数的声明:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函数说明:
创建虚拟内存到物理内存或者文件的映射,下面我们来看看他的这几个参数:
下面我们来说明一下port的取值:
PORT_EXEC:映射的区域具有可执行权限
PROT_READ:映射的区域具有可读权限
PROT_WRITE:映射区域具有可写权限
PROT_NONE:映射区域不可被访问
对应flags的取值:
MAP_SHARED:对映射区域的写入操作直接反映到文件当中
MAP_FIXED:若在start上无法创建映射则失败(如果没有此标记会自动创建)
MAP_PRIVATE:对映射区域的写入操作只反映到缓冲区当中不会写入到真正的文件
MAP_ANONYMOUS:匿名映射将虚拟地址映射到物理内存而不是文件(忽略fd)
MAP_DENYWRITE:拒绝其它文件的写入操作
MAP_LOCKED:锁定映射区域保证其不被置换
返回值:函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
下面我们来演示一下映射到物理内存的案例:
#include
#include #include #include #include using namespace std; static const int SIZE = 4096; int main() { char *str = (char *)mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); //注意MAP_PRIVATE和MAP_SHARED //建立映射 if (str == MAP_FAILED) { printf("%s\n", strerror(errno)); return -2; } strcpy(str, "hello ksy"); puts(str); //用于取消映射 munmap(str, SIZE); return 0; }
运行结果:
下面我们来看一下这个映射到文件该如何进行操作了,这个是特别容易错的。
下面直接给代码(注意这个代码是错误的)
#include
#include #include #include #include #include #include #include #include using namespace std; static const int SIZE = 4096; int main() { int fd=open("./a.txt",O_RDWR|O_CREAT,0644); if(fd<0){ printf("%s\n",strerror(errno)); return -1; } char *str = (char *)mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //注意MAP_PRIVATE和MAP_SHARED //建立映射 if (str == MAP_FAILED) { printf("%s\n", strerror(errno)); close(fd); return -2; } strcpy(str,"helloworld"); close(fd); return 0; }
然后我们编译一下然后再看一下结果:
很多老铁可能直接就懵逼了,没问题啊文件也有啊映射也成功了啊为什么就是映射出现错误了。下面我们来分析一下:
mmap是将虚拟内存映射到文件(物理内存)。按照我们的想法"helloworld"这个字符串应该是要被写入到文件当中。但是我们想一下我们这个文件是新创建的,好像大小是0个字节耶,那么在映射的时候好像也是映射了0个字节,所以这个文件映射过来的内存是没有的,此时我们让里面写东西崩溃了也是正常的。此时我们可以使用truncate函数对文件提前进行处理一下
下面我们来看一下truncate这个函数的原型:
int truncate(const char *path, off_t length);
函数说明:truncate()会将参数path指定的文件大小改为参数length指定的大小。 如果原来的文件大小比参数length大,则超过的部分会被删除。我们就可以提前使用这个函数提前将文件的大小进行设置这样我们就可以向映射的这块内存进行写入了。下面我们对代码进行一下修改
#include
#include #include #include #include #include #include #include #include using namespace std; static const int SIZE = 4096; int main() { int fd = open("./a.txt", O_RDWR | O_CREAT, 0644); truncate("a.txt", 1024); if (fd < 0) { printf("%s\n", strerror(errno)); return -1; } char *str = (char *)mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //注意MAP_PRIVATE和MAP_SHARED //建立映射 if (str == MAP_FAILED) { printf("%s\n", strerror(errno)); close(fd); return -2; } strcpy(str, "helloworld"); close(fd); return 0; }
然后我们在运行一下代码:
此时我们发现就成功的将其写入到文件当中了.
mmap内存映射的实现过程主要分为三个阶段:
(一):进程启动映射过程并在虚拟地址空间当中为映射创建映射区域
1.进程在用户空间调用mmap也就是上面那个函数。
2.在当前进程的地址空间当中寻找一段连续的空虚的虚拟地址
3.给这块虚拟地址分配一个vm_area_struct的结构并对其各个区域进行初始化
4.将新键的虚拟结构插入到虚拟地址空间的链表或者红黑树当中
(二):实现物理内存地址和虚拟地址的映射关系
1.为映射分配了新的虚拟地址空间之后通过待映射的文件描述符指针,在文件描述符表当中找到对应的文件描述符链接到内核已经打开的文件描述符集当中的struct_file,这个struct_file维护着这个被打开的文件的各项信息
2.通过这个文件的结构体链接到file_operations,调用内核的mmap其函数原型为int mmap(struct file*filp,struct vm_area_struct*vma),请注意不是用户态的mmap
3.内核mmap函数通过虚拟文件系统当中的inode定位到文件的物理地址
4.通过reamp_pfn_range函数建立页表即实现了文件地址和虚拟地址的映射关系。
(三)
1.进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
2.缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
3.调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
4.之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程