虚拟内存
操作系统需要管理的就是各个进程的内存,对于进程,需要存储代码、堆、栈等信息,如果让程序员直接来操控物理内存管理进程的话,难度会更大,需要考虑进程在哪个位置分配、会不会冲突等问题,那么操作系统提供了虚拟内存给程序员使用,背后的实现这些脏活累活都交给操作系统去完成了,对于虚拟内存,有一些追求的目标:
-
透明:每个进程看上去都有自己的内存,从0地址开始
-
效率:在虚拟内存转换到物理内存的时候兼顾时间(TLB哈希表)和空间(多级页表懒加载)
-
保护:每个进程是隔离的
除了硬件的底层实现,同时也需要有一些上层的策略(如可用内存如何管理、内存不够哪些内存可以释放)
硬件的地址转换
硬件的地址转换也可以叫做虚拟内存和物理内存的联系:物理内存 == 虚拟地址 配合 基址寄存器,可以理解为基址寄存器保存着虚拟地址转换来的物理地址的起始点,界限寄存器用来保证访问不越界。
这两个寄存器是CPU的重要部分,具体来说是CPU的内存管理单元MMU的重要部分,MMU是操作系统在进程上下文切换用来临时保存将要执行的进程的信息,而对于首次运行的进程,需要从进程列表(保存所有的进程信息)中的进程结构PCB拿出进程信息恢复到MMU,然后返回用户态执行进程。
对于越界访问的进程,界限寄存器会判断并跳转到内核态的异常处理程序。
分段
对于进程内部的虚拟内存,栈和堆之间有很大一块空闲空间,我们可以通过分段把这块空闲空间放到进程外部,把进程内存细分为几个段(如代码段、堆段、栈段),每个段有自己独立的基址寄存器和界限寄存器,也就是说MMU里面有多个寄存器对,进程上下文切换的时候需要恢复多个寄存器对。
我们如何才能知道访问到哪个段?将虚拟地址切割,前两位表示哪个段(对应使用哪个寄存器对),后几位表示段内偏移量,还有几位表示保护位(规定进程的访问权限,用于进程共享时检查)。
分段的空闲内存管理
对于分段这种机制,拿堆的内存分配举例,会产生许多的外部碎片,我们可以使用空闲列表的方式去管理这些空闲的内存(外部碎片),对于每块空闲内存,他都有一个头部用来存放当前空闲内存的字节数(进程使用完后free()释放这块内存)、以及指向下一个空闲内存的指针。当需要申请增大堆空间的时候,只需要把空闲列表尾节点的指针指向新申请到的内存。
当归还的内存和原有的空闲内存的地址相邻的时候,我们需要去合并他们,这里简单介绍一下简单合并的方法,伙伴系统(不针对分段):将一块堆内存空间分成很多小块以及更小的块,每个小块或者更小的块都是2^n字节大小(递归分配),在进程需要内存的时候,分配一块2^n字节大小的给进程(没有外部碎片但是有内部碎片),归还的时候只需要看看兄弟节点是否是空闲的,然后递归合并。
分页
对于分段,每个段的大小是不相同的,产生的外部碎片会比较多,不利于空间的分配,我们需要一种分页的机制,每个内存页的大小相同,就不存在外部碎片了,空闲页同样会用空闲列表保存。那么分页是如何进行地址转换的呢?虚拟内存会被分为多个部分,比如VPN(虚拟内存页号)、偏移量,存放在内存的页表将VPN转换成PFN(物理页号)再 + 偏移量,就得到实际的物理地址了,分页这里用页表替换了基址寄存器的功能。
当然相应的也会存在一些问题:分页需要内存的页表进行虚拟地址到物理地址的映射转换,就需要额外多一次访问内存、以及页表也是需要占用内存空间的,我们无法忍受多进程下页表占用大量的内存空间。
TLB(快速地址转换)
对于多一次访问内存,我们可以在CPU加入地址映射缓存TLB来加快访问速率,CPU对于CPU缓存的访问是比访问内存快的,但是缓存一般会比较小,同时需要去维护缓存。
在进程上下文切换的时候,和分段不同,不用保存和恢复寄存器对的内容,而是使用TLB代替,需要恢复TLB,但是还是需要保存恢复程序计数器等其他的硬件信息的,同时会将旧的进程的页表映射缓存设置为无效(逻辑删除),当新的进程需要访问物理内存的时候,硬件发出异常,进程陷入到内核态,操作系统去完成异常中断程序(将需要的页表映射加载到TLB),进程再回到用户态再次执行刚刚访问物理内存的指令,这一次就会命中TLB缓存。
上述还有一个问题,在将需要的页表映射加载到TLB的时候,如果TLB是满的,如何选择替换哪个无效的页表映射呢?就需要用到类似于内存硬盘数据页换入换出的算法了,比如LRU等。
多级页表
页表也是需要占用内存空间的,我们一开始采用的是线性页表,即一开始分配一块线性的空间用来存储页表,对于多进程的现代计算机来说,需要提前分配好足够大的内存,并且使用过程中可能出现很大的空间浪费,后面我们采用了类似于树的结构,也就是多级页表这样的结构存储页表,虽然牺牲了一部分查找的时间,但是显然我们觉得内存是更加宝贵的资源,采取这种时间-空间折中的办法,而线性页表完全是用空间换取了时间。
在进程需要访问物理内存的时候,同样会到TLB先查询缓存,如果缓存未命中,才会去查询多级页表,对于多级页表的查询过程:
-
先查询低级的页表索引目录
-
再逐步查找更高级的N级页表索引目录
-
最后加上偏移量找到页内数据