操作系统是通过内存分段和内存分页的方式管理虚拟内存地址和物理内存地址之间的关系
内存分段
程序是由若干个逻辑分段组成的,代码分段、数据分段、栈段、堆段组成,不同的段有不同的属性,所以就用分段的形式分离开。
分段机制下的虚拟内存由两部分组成,段选择因子和段内偏移量。段选择因子里面有段号,段号是段表的索引,段表是一个表,里面保存的有该段的基地址、段的界限和特权等级。段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
分段机制会把程序的虚拟地址分为4个段,每个段在段表中有一个项,在这一个项找到段的基地址,然后加上偏移量,就能找到在物理内存中的地址。
每个段的长度是不一样的,而且每个段内部都是从0开始编制的。 每个段内部是连续分配内存,但是段和段之间是离散分配的。
分段的方式解决了程序员不需要关心具体的物理地址内存的问题,但是有一些不足之处:
- 存在内存碎片(外部碎片),可以通过内存交换的方式解决
- 内存交换的效率低,把内存写到硬盘上面太慢了
内存分页
内存分页是为了减少内存碎片、加快内存交换的方式。
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。每个页的大小是4k。 虚拟地址与物理地址之间通过页表来映射。
MMU是内存管理单元 ,当进程访问虚拟内存地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核分配物理内存,更新进程页表,然后再返回用户空间,恢复进程的运行。
分页是怎么解决分段的「外部内存碎片和内存交换效率低」的问题?
分页的话,页与页之间是紧密排列的,所以不会存在外部碎片。内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会存在内部内存碎片的现象。
如果内存分页不够,操作系统会把其他正在运行的进程中的最近没有被使用的内存页给换出到硬盘上面。 一旦需要的话,再从磁盘上面换入到内存里面。由于一次性换入或者换出的只有少数几个页,所以不用花费太多的时间。
更进一步,分页的方式使得我们在加载程序的时候,不再需要一次性把全部的程序加载到物理内存中。我们可以在进行虚拟内存和物理内存的页之间映射之后,并不把真正的页加载到物理内存里面,而是只有在程序运行中,需要用到对应的虚拟内存页里面的指令和数据的时候,在加载到物理内存中去。
分页机制下,虚拟地址和物理地址是如何映射的?
虚拟地址分为两部分,页号和页内偏移,页号是页表的索引,页表中存储的是虚拟页号和物理页号,物理页号可以查找物理内存的基地址,基地址划伤页内偏移量就能找到真实的物理地址。
简单的分页有什么问题?
32位的环境下,每个进程的虚拟地址空间都有4GB,一个页的大小是4kb,那么就会存在4GB / 4KB = 100万个页,每个页中有的页表项 需要4个字节大小来存储,那么整个4GB的映射需要4MB的内存来存储页表。 如果有100个进程的话,需要400M来存储,耗费比较大。
多级页表是什么?
每个进程的100万个页共用的一个页表 ,该页表占用的4MB空间。 4M空间来说有些大了,我们把这100万个页表项的单级页再分页,将一级页表再分为1024个二级页表,每个二级页表中包含1024个页表项。
分了二级表,映射 4GB 地址空间就需要 4KB(一级页表)+ 4MB(二级页表)的内存,这样占用空间不是更大了吗?
如果把4GB的虚拟地址全部都映射到了物理内存上的话,确实二级分页占用的空间更大了,但是,我们不会为一个进程分配太多的内存。 对于每个进程来说,其使用的空间没有达到4GB,因为会存在部分对应的页表项都是空的,对于已经分配的页表项,如果最近一段时间没有被使用,在物理内存紧张的情况下,也是会被换出的。
那么为什么不分级的页表就做不到这样节约内存呢?
页表一定要覆盖全部的虚拟地址空间,不分级的页表需要100万个页表项来映射,而分级的页表只需要1024个页表项。