本篇文章主要记录了共享内存映射的概念、原理,以及使用mmap()
来建立匿名映射、有名映射的方法,以及通过该种方法实现进程间通信。
1 共享内存概念
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。
对于像管道和消息队列等通信方式,则需要在内核和用户空间进行 两次运行级别切换(系统调用导致保护和恢复进程上下文环境)+四次数据拷贝 ,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
1.1 传统文件访问方式
UNIX访问文件的传统方法是用open
打开它们, 如果有多个进程访问同一个文件,则每一个进程在自己的地址空间都包含有该文件的副本,这不必要地浪费了存储空间。 下图说明了两个进程同时读一个文件的同一页的情形。
系统要将该页从磁盘读到高速缓冲区中, 每个进程再执行一个存储器内的复制操作将数据从高速缓冲区读到自己的地址空间。
1.2 共享存储映射方式
相对于传入文件访问方式,共享内存的的方式是:进程A和进程B都将该页映射到自己的地址空间,当进程A第一次访问该页中的数据时,它生成一个缺页中断。内核此时读入这一页到内存并更新页表使之指向它.以后, 当进程B访问同一页面而出现缺页中断时,该页已经在内存, 内核只需要将进程B的页表登记项指向次页即可。如下图所示:
2 mmap()
2.1 mmap 工作原理
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:
2.2 mmap实现过程
mmap内存映射的实现过程,总的来说可以分为三个阶段:
①进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
进程在用户空间调用mmap函数。
在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址。
为此虚拟去分配一个vm_area_struct结构,并对该结构各个域进行初始化。
将新建的vm_area_struct插入到进程的虚拟地址区域链表或树中。
②调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,连接到内核“已打开文集”中该文件的文件结构体struct file,每个文件结构体维护着和这个已打开文件的相关信息(从open函数调用到内核返回文件描述符这块隶属于文件系统的知识,后续专题补充)。
通过该文件的文件结构体,连接到file_operations模块,调用 内核函数mmap,int mmap(struct file *filep,struct vm_area_struct *vma)。
内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射,此时这片虚拟地址区域没有任何数据关联到主存中
③进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
进程的读写操作访问虚拟地址空间的这一段映射地址,通过查询页表,发现这一段地址不在物理页面上,因为只是建立了地址映射,真正的磁盘数据还没有拷贝到内存中,因此引发缺页异常缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程调页过程先在交换缓存空间中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页面从磁盘装入主存中之后进程可对这片主存进行读或写操作,如果写操作改变了内容,一定时间后系统会自动回写脏页面到对应的磁盘地址,也就是完成了写入到文件的过程修改过的脏页面不会立即更新到文件中,而是有一段时间的延迟,可以调用msync来强制同步,这样所写的内容就立即保存到文件里了。
对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。 所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
2.3 mmap()和munmap()函数原型
1 |
|
mmap()
函数用 映射文件描述符fd指定文件的 [offset,offset + length]
区域至调用进程的[addr, addr + length]
的内存区域。
addr
:映射区开始地址,用户可以给定。建议设定NULL,有内核给定起始地址。
length
:映射区的长度,需要大于等于文件的实际大小。
prot
:期望的内存保护标志,不能与文件打开模式冲突。
PROT_EXEC :页内容可以被执行
PROT_READ :页内容可以被读取
PROT_WRITE :页可以被写入
PROT_NONE :页不可访问
fd
:用于创建共享内存映射区的那个文件的 文件描述符;fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信,即用于通过fork/vfork/clone系统调用创建的子进程之间的匿名映射通信。)
offset
:设置文件从何处开始映射(对于不需要读入整个文件的情况),默认为0。
flags
:指定映射对象的类型,映射选项和映射页是否可以共享。一般设为MAP_SHARED,表明与其它所有映射这个对象的进程共享映射空间;此时对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用之前,文件实际上不会被更新。另外还可以设为MAP_PRIVATE,表示建立一个写入时拷贝的私有映射;此时内存区域的写入不会影响到原文件。这两个标识是互斥的,只能使用其中一个。其他标识参照man
手册介绍。
返回值:执行成功时返回addr
即映射内存的首地址,失败时将返回MAP_FAILED
,这是一个void*(-1)
,并会设置errno
。
函数的一般使用方法如下
1 | // 建立有名映射, 注意length参数需要手动指定 |
1 |
|
munmap()
用于释放映射区,参数含义与mmap()
对应,在此不做介绍。
2.4 mmap()使用注意事项
- 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
- 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
- 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
- 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED是, mmap的读写权限,应该 <=文件的open权限。只写不行。
- 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
- offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
- 对申请的映射区内存,不能越界访问。
- munmap用于释放的 地址,必须是mmap申请返回的地址。
- 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
- 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
2.5 mmap()使用实例
这是一个使用mmap()
使用有名映射的小例子。
- 先使用
open
新建一个文件,得到文件描述符 - 使用
ftruncate()
来拓展文件到指定大小 - 使用
mmap()
创建映射,得到映射内存首地址ptr
- 使用
memcpy()
向ptr
中写入字符串 - 使用
mmap()
关闭映射
代码如下
1 |
|
编译执行完成后,目录下将多出abc
普通文件。使用cat
查看文件内容
1 | cat abc |
3 使用mmap()完成进程间通信
3.1 mmap()用于父子进程间通信
父子进程间通信既可以使用有名映射也可以使用匿名映射。这里使用匿名映射映射进行演示。
显然共享内存映射应当在使用fork()
之前就建立好。代码如下
1 |
|
3.2 mmap()用于非亲缘进程间通信
在非亲缘进程间通信,不能用匿名映射,必须用有名映射。
这里先写一个负责向映射区写入数据的程序
1 |
|
这时再写一个负责从共享内存中读取数据的程序
1 |
|
此时将两个程序编译执行,先运行mmap_w
写程序,再运行mmap_r
读程序。现象如下
1 | ./mmap_w |
1 | ./mmap_r |
说明成功进行了进程间的通信。
4 结束语
本篇文章先介绍了mmap
实现共享内存映射的概念和原理,然后介绍了mmap
函数和常用参数,最后练习了使用mmap()
进行父子进程、非亲缘进程间通信。
关于共享内存的原理部分,主要都是参考其他的博客资料,后续再翻阅APUE一书,加深对这部分的理解。