简 述: 本篇讲解另外一种进程间通信方式,内存映射区 mmap()
,以及对应的释放内存映射区 munmap()
,。前面两篇讲解了进程间通信,使用有名管道和匿名管道的方式进行 IPC,也是经常用到的,可以去接触一下。
- 对于有血缘关系的进程间通信:
- 有名内存映射区
- 匿名内存映射区(推荐)
- 对于无血缘关系的进程间通信:
- (只能用)有名内存映射区
[TOC]
本文初发于 “偕臧的小站“,同步转载于此。
编程环境:
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
mmap内存映射原理:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数 mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
创建内存映射区 mmap():
作用: 将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
- 参数:
- addr:
- 映射区的首地址,传
NULL
;系统会自动在虚拟地址空间的动态加载区,开辟一块大小为 len 的内存区域空间。
- 映射区的首地址,传
- len: //映射区的大小
- 必须是 4K 的整数倍,且不能够为 0。
- 一般文件有多大,len 就有多大
- prot: //映射区的权限
PROT_READ
– 映射区必须要有读权限PROT_WRITE
– 写权限
- **flags: //标志位参数 **
MAP_SHARED
共享区域,开启此权限,则内存中映射区域的内容,是和磁盘文件的内容保持一致MAP_PRIVATR
内存区域的映射内容,是和磁盘文件的内容,不是时刻同步的。
- fd: //文件描述符
- 磁盘文件(想要映射到内存中的共享区)的那个文件的文件描述符
- offset: //偏移文件的偏移量
- 当想要从文件的中间某处到结束区域,映射到内存中,就可以只用这个偏移
- addr:
- 返回值:
- *void : 开辟的那个区域的首地址,用指针传出来。
- 映射区的首地址 – 调用成功
- 调用失败, 返回 MAP_FAILED
- *void : 开辟的那个区域的首地址,用指针传出来。
释放内存映射区 munmap():
就像 malloc - free; new - delete; mmap - munmap 一样,有开辟空间,就有释放该内存区域
int munmap(void *addr, size_t len);
两个参数,就是 mmap() 的第一个和第二个参数。
写一个例子,验证内存内容和磁盘文件会同步:
对于一个已有的文本文件 it.txt 进行映射,创建一个内存映射区,然后在内存映射区里面修改文件的聂荣,再重新打开磁盘的文本文件查看内容是否同时发生了改变。显示修改之前文件和使用内存映射区的内容后的文件内容后,发现磁盘里面的内容的确是和同步的改变了。最后要记得使用 munmap()
关闭你使用 mmap()
创建的内存映射区的空间哦。
代码示例:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> int main(int argc, char *argv[]) { int fd = open("it.txt", O_RDWR); if (fd == -1) { perror("[open file] "); _exit(1); } int len = lseek(fd, 0, SEEK_END); void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //创建内存映射区 if (ptr == MAP_FAILED) { perror("[mmap fail] "); exit(1); } ((char *)ptr)[0] = 'a'; ((char *)ptr)[1] = 'b'; ((char *)ptr)[2] = 'c'; printf("%s\n", (char*)ptr); //释放内存映射区 munmap(ptr, len); close(fd); return 0; }
运行效果:
和预期的效果一直,在内存中改动内容,磁盘的文件的内容也随之改变。
对于 mmap() 的一些思考:
- 如果 mmap() 的返回值 (ptr)做++操作(ptr++),munmap是否能够成功?
- 不能,如果要做指针偏移的的话,可以 char* pt = ptr;
- 如果 open() 时候 O_RDONLY,mmap 时 prot 参数指定 PROR_READ | PROT_WRITE 会怎样?
- mmap 会调用失败
- open() 文件指定权限应该大于等于 mmap() 的第三个参数 prot 指定的权限
- 如果文件的偏移量为 1000 会怎么样?
- 会失败,其必须是 4096 的整数倍
- 如果不检查 mmap() 的返回值会怎样?
- 也不会怎么样
- mmap() 什么时候会调用失败?
- 第二个参数 len = 0
- 第三个必须要有 PROT_READ 权限;且 open()打开的权限要大于 mmap() 的 port 参数权限
- 可以open()的时候,O_CREAT 一个新文件来创建映射区吗?
- 可以,但是需要做文件扩展
- lseek()
- truncate(path,length)
- mmap 后关闭文件描述符,对 mmap 映射有没有影响?
- 文件被打开之后,就没有影响了。
- 对 ptr 越界操作会怎么样?
- 这个取决于 ptr 越界后面的内存写的是什么。但是大概率的会遇到段错误
mmap 实现内存映射?
- 必须要有一个文件
- 文件数据什么时候有用?
- 单纯的实现文件映射
- 进行进程间通信,磁盘的文件数据时没有用的。(在内存操作会更有效率,但是属于非阻塞)
父子进程间永远共享的东西?
- 文件描述符
- 内存映射区
例子实现父子进程间的通信:
通过改写上面的例子,创建 anonMmap.cpp 文件,创建子进程,父进程对内存映射区进行修改内容,在首段使用 strcpy() 添加一段中文语句,然后在子进程里面对复制进来的尾部’\0’进行覆盖,再次修改一段内容,然后在子进程里面间该短内容输出到终端显示。
代码实现:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> int main(int argc, char *argv[]) { int fd = open("it.txt", O_RDWR); if (fd == -1) { perror("[open file] "); _exit(1); } int len = lseek(fd, 0, SEEK_END); void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //创建内存映射区 if (ptr == MAP_FAILED) { perror("[mmap fail] "); exit(1); } pid_t pid = fork(); if (pid > 0) { //父进程 strcpy((char *)ptr, "(我是父进程写入数据到内存映射区内容)"); //下标0-53,一共 54 个,其中ptr[53]为'\0' wait(NULL); //回收子进程 } else if (pid == 0) { //子进程 // sleep(2); ((char *)ptr)[53] = 'a'; //故意覆盖掉'\0',方便打印出来后面文章 ((char *)ptr)[54] = 'b'; ((char *)ptr)[57] = 'c'; printf("%s", (char *)ptr); } munmap(ptr, len); close(fd); return 0; }
运行效果:
创建匿名内存映射区:
上面写的例子,都是对于有血缘关系的父子进程之间的通信例子,通过磁盘文件使用 mmap()
创建的是(有名)内存映射区; 但是改一下 mmap() 创建的倒数第二个参数,且不需要 open() 磁盘文件,创建出来的就是(匿名)内存映射区 ;
但是匿名内存映射区只能够适用于有血缘关系之间的进程通信。而有名内存映射区,可以使用与在有有血缘的进程和无血缘的进程之间的通信,都可以。
匿名内存映射区(有血缘关系进程通信):
(匿名)内存映射区 代码例子:
int main(int argc, char *argv[]) { int len = 4096; void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); //创建匿名内存映射区,只需要修改倒数 2、3 两个阐述即可 if (ptr == MAP_FAILED) { perror("[mmap fail] "); exit(1); } pid_t pid = fork(); if (pid > 0) { //父进程 strcpy((char *)ptr, "this is parent process"); wait(NULL); //回收子进程 } else if (pid == 0) { //子进程 // sleep(2); ((char *)ptr)[0] = 'a'; //故意覆盖掉'\0',打印出来后面文章 ((char *)ptr)[1] = 'b'; ((char *)ptr)[2] = 'c'; printf("%s", (char *)ptr); } munmap(ptr, len); return 0; }
运行效果:
有名内存映射区(无血缘关系进程通信):
而对于无血缘关系的进程间通信,只需要都打开同一个磁盘文件,各自的进程会按照这个顺序, 磁盘文件名 --> (各自的进程虚拟地址空间的)内存映射区 --> (共用一份的)物理内存的区域
, 然后都可以修改和读取这一段内存区域,从而实现进程间通信。
创建 aProcess.cpp 生成 a 进程,创建 bProcess.cpp 生成 b 进程;a 进程先对 c.txt 文件改写添加 “abc”,然后 b 进程再对 c.txt 文件改写添加 “ABC”,然后输出到终端显示。
代码显示:
实现伪代码如下,详细的源码见下面下载链接
//aProcess.cpp int main(int argc, char *argv[]) { int fd = open("c.txt", O_RDWR); ... int len = lseek(fd, 0, SEEK_END); void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ((char *)ptr)[0] = 'a'; ((char *)ptr)[1] = 'b'; ((char *)ptr)[2] = 'c'; munmap(ptr, len); ... } //bProcess.cpp int main(int argc, char *argv[]) { int fd = open("c.txt", O_RDWR); ... int len = lseek(fd, 0, SEEK_END); void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ((char *)ptr)[3] = 'A'; ((char *)ptr)[4] = 'B'; ((char *)ptr)[5] = 'C'; munmap(ptr, len); ... }
运行效果:
借鉴博客与总结:
发现一篇讲解的很棒的博客,更多的是理论和概念上面的分析 mmap() 的原理:认真分析mmap:是什么 为什么 怎么用, 其中文章开头的一段拿来再描述一下,其余则是本篇的侧重点是用代码来写两个例子,以及需要注意的一些坑,验证和学习这个内存映射区。
下载地址:
欢迎 star 和 fork 这个系列 的 linux 学习,附学习由浅入深的目录。