这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
为实现多进程同时执行,需要做硬件虚拟化,之前讨论了CPU通过时间复用进行虚拟化。对于内存虚拟化来说。由于在进行上下文切换时需要保存进程的代码段,栈,堆。所以如果采用时间复用的方式,只能将代码、栈、堆临时存储到磁盘中。而磁盘的IO速递相对于与内存和CPU的运算来说实在太慢,所以内存通常不会采用时间复用方式,而是采用空间复用的方式。
内存虚拟化简单设计
基于空间复用的拆分方式进行内存虚拟化,假设有一个空间为64k的内存地址。
简单粗暴
先来看一种最简单的方式。假设每个进程只需要使用16k的内存地址。则64k可以划分给4个进程使用。在每个4k地址中,又需要划分3部分存储栈、代码段、堆。三部分中,代码段使用的内存空间是固定不变的,而堆和栈是随着代码运行进行增长。为了满足增长的需求。可以将这三部分这样安排。代码段从地址0x0000开始存放。代码段地址结束后,紧跟着存放栈的地址空间,当栈增长时,向高位地址增加。最后的堆空间的起始地址放在4k的位置,当堆增长时,向低位地址空间进行增长。
进程在访问内存时直接使用整个内存空间的地址进行读写即可,但这样会带来一个严重的问题,任意一个进程都能随意读写另外一个进程的内存空间,这样做太危险了,所以我们还需要实现内存隔离机制。实现内存隔离的一种方式是,将进程的起始地址存放到进程元数据中(存储进程相关信息的位置),当在读写内存时候,都是用0~4k的地址空间,最后通过计算得出实际的物理地址。在这种情况化,0~4k称为虚拟地址,物理地址=虚拟地址+起始地址,当访问的地址超过了进程最大地址空间,就会由操作系统进行异常处理。
这种方式的缺点是内存空间利用率不高,因为不论进程使用多少内存,栈和堆之间,会有一大段空闲空间。
分段访问
为了提高利用率。可以对代码、栈、堆进行分段访问,每个段都有一个基址和起始地址。由此可以将栈和堆各自划分一个更小的空间,增大内存利用率。但产生了另外一个问题:当我访问内存地址:0x44时,我应该用哪个段的基址计算出物理地址呢?
通常的解决方式是重新设计0x44地址的生成规则。比如4位的内存地址。在增加两位到6位,前两位用来指明是访问代码、栈、堆的段,后四位用来说明访问的虚拟地址。
使用分段的方式会提高内存利用率,但会是内存管理变得更加困难。在内存块空间大小固定的情况下内存分配和回收只需要一个简单的空闲列表管理。并且其空间紧凑,没有外部的内存碎片。但是当内存块大小不固定时,就会产生外部内存碎片。对于这种空间需要增长导致产生内存碎片情况,通常会使用整理空间的策略来进行挽救。
分页
分段通过分配不同大小块可以提高利用率,但其内存管理更为复杂且低效。而不分段又会使内存利用率低。所以采用分页的方式,将64k内存分为大小相同的页,进程需要几个页就分配多少。
具体实现方式是将64k分为4个页,每个页大小16k。为了实现内存隔离,在访问地址时,会输入6位的地址,前两位作为虚拟页码,标识访问第几个虚拟页,后四位则是虚拟地址。在实际访问中,需要先将虚拟页码转为实际的物理页基址,在加上虚拟地址可以计算出实际的物理地址。
总结
设想的一下内存虚拟化设计,感觉写的不是很好。也有很多点没有说到。比如在实现内存隔离后,当内核进程进行系统调用时,需要将数据从进程空间拷贝到操作系统的内核空间。为什么?
- 不拷贝需要传数据的内存地址,打破了内存隔离
- 不进行拷贝,有可能进程会修改地址指向的数据
- 比如在磁盘读取数据情况下,不进行拷贝,则后续对数据的访问都需要访问操作系统的内存地址
所以,通常在IO相关的系统调用都会有两次拷贝,第一次是进程到操作系统,第二次操作系统硬件。两次拷贝会使得IO很慢,所以就出现了一个0拷贝的概念:不经过操作系统的IO称为0拷贝(毕竟没有中间商赚差价)。其实现有硬件的DMA。