二十五、linux内存映射
一、物理地址空间是什么
理解虚拟地址空间还得从物理地址空间开始说起。我们知道内存就像一个数组,每个存储单元被分配了一个地址,这个地址就是物理地址,所有物理地址构成的集合就是物理地址空间。物理地址也就是真实的地址,对应真实的那个内存条。
如果CPU使用物理地址向内存寻址的话,就是下面这样,这条指令中的地址就是数据真实存放的地址。
二、虚拟地址空间是什么
引入虚拟地址之后,对于每一个进程,操作系统提供一种假象,让每个进程感觉自己拥有一个巨大的连续的内存可以使用,这个虚拟的空间甚至还可以比内存的容量还大。这个“假象”就是虚拟地址空间。虚拟地址是面向每个进程的,只是一个“假象”罢了。
此时CPU使用虚拟地址向内存寻址,通过专用的内存管理单元(MMU)硬件把虚拟地址转换为真实的物理地址(地址翻译),操作系统负责把虚拟地址和物理地址的映射关系维护在页表之中。
指令中的地址不是数据真实存放的地址.
三、程序和进程
当我们写完代码,编译,链接并且生成可执行文件后,得到的这个东西就是一系列二进制代码的集合,我们管这东西叫做程序,存储在磁盘上。只有当我们执行这个文件后,程序才会被操作系统读入内存运行,但是注意系统并不会把程序全部读入内存,我们把正在运行的程序叫做进程。
从进程的视角来看,我的数据和代码被存放在一个连续的空间之中,每个区域分别有着不同的功能。典型的如存放代码的区域和存放数据的区域。
然而我们知道,这只是个假象。代码和数据中的地址都是一个虚拟地址,还需要经过地址翻译才能得到真正的物理地址。
四、分页、页表和缺页异常
虚拟地址和物理地址的映射关系是以“页”为单位的。分页就是把整个虚拟内存和物理内存分割成大小固定的块,以一个页作为映射的最小单位。运行时,CPU请求一个虚拟地址,虚拟地址又被翻译为物理地址,从而确定数据在内存中的哪个位置。下面的页表中记录了这个进程虚拟内存每个页的映射关系。
当CPU寻址的时候,这个映射会有三种可能。
- 未分配:虚拟地址所在的那一页并未被分配,代表没有数据和他们关联,这部分也不会占用内存。
- 未缓存:虚拟地址所在的那一页被分配了,但并不在内存中。
- 已缓存:虚拟地址所在的那一页就在内存中。
当访问一个未缓存的区域时,系统将产生缺页中断,然后进程被阻塞,等待操作系统将缺失的那一页从磁盘复制到内存。当复制完成后,CPU继续执行导致缺页中断的那条指令,此时就会正常执行了。这种仅在需要的时候将页面拷贝到内存的策略叫做按需调度页面。 可以想象当程序被装入内存的时候,开始时仅有有很小的一部分内容被放入内存。程序在运行中不断缺页,不断的把需要的部分拷贝进内存。
从上面的图中还可以看出,虚拟内存实际上就是磁盘的缓存。系统通过缺页中断的机制,小心的维护着每个进程的虚拟地址假象。
五、虚拟内存的应用
在Linux中,将一片虚拟内存和一个磁盘上的对象关联起来,并用磁盘上的对象初始化这片虚拟内存,这个机制就叫做内存映射。
1、化简资源的共享
当我们使用共享库的函数时时,例如printf(),没有必要为每个进程拷贝一本代码,这样太浪费内存了。我们只需要让每个进程的一块虚拟内存映射到相同的对象上就可以了。
假如进程1和进程2想要共享同一个文件。其中文件A已经被映射到进程2(文件被缓存在内存中),进程2还是通过缺页中断载入的文件A。
此时进程1也打开文件A,由于文件的名字在系统中是唯一的,操作系统清楚文件A已经被缓存在内存之中。因此系统将进程1的虚拟内存映射到相同内存之中,完成文件的共享。
2、零拷贝技术的一种实现
对于linux中最常用的I/O函数 read() 来说,文件先会被系统复制到内核空间的缓冲区,然后再复制到用户空间。读一个文件需要复制两次显然不是我们希望的,尤其是读大文件的时候, sad...
通过内存映射,我们可以绕过内核缓冲,直接将文件A映射到虚拟内存,这里一共就发生一次拷贝,nice~
六、总结
虚拟内存无时无刻都在为我们工作,而且我们不需要任何干涉就能自动地工作。虚拟内存可以看成对磁盘的一个缓存,它通过缺页中断触发操作系统处理访问未缓存块的问题。虚拟内存可以应用在处理共享对象、减少I/O开销问题中。