在csapp的描述中,虚拟内存的形象更加具化,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组,内存充当了磁盘的缓存,粗呢内存的许多概念与SRAM缓存是相似的。虚拟页面有以下三种状态:
- 未分配(pte的有效位为0,且pte的地址段为空)
- 未缓存(pte的有效位为0,且pte的地址段指向磁盘的某一位置)
- 已缓存(pte的有效位为1,且pte的地址段指向内存的某一位置)
这是将物理地址的范围扩大到除了内存之外,还包含磁盘。虚拟地址照常根据MMU进行多级页表转换,得到的地址可能是物理页号(内存中)或者磁盘地址。虚拟内存系统是虚拟地址的扩展应用。
在xv6系统和JOS系统中,并没有使用到磁盘上的虚拟内存,MMU得到的结果始终在内存上,否则就是缺页,在内存上分配新的页面,并无磁盘的利用。推测应该使用swap交换区来实现。
当操作系统调用malloc分配一个新的虚拟内存页的时候,首先会在磁盘上创建空间,并更新PTE。
在磁盘和内存之间传送页面的活动称为交换(swapping)或者页面调度,分为换入和换出。页面调度通常发生在有不命中发生的时候,这叫做按需页面调度。也可以通过预测的方式提前将可能用到的页面调入内存,这就有点像分支预测,但通常不被使用,可能是因为比起分支预测的惩罚,页面调度失败的代价比较大。由于程序的局部性,页面调度可能不需要频繁发生,如果工作集的大小超过了物理内存的大小,就不可避免地会发生频繁的换入换出,这种状态称为抖动,会大大影响程序的性能。不管虚拟地址翻译的结果在不在磁盘,最终cpu都要从内存读数据,因此如果不在内存,有效位会是0,发生正常的缺页,进行页面调度,如果内存已满,则要根据一些策略选择换出页面,这里还涉及到像缓存里相似的直写和写回的选择。
【缺页异常可能有三种原因】
- 段错误,访问一个不存在的页面;
- 正常缺页,则进行页面调度,然后重新执行;
- 保护异常,违反了页面的权限许可,返回错误码即可。
此外,pte上不止有有效位,还有各种标志位,控制了不同程序对页面的访问和读写权限。正因为此,我们说虚拟内存可以更好地管理内存。
【关于共享内存】
比如图中的PP6,操作系统对共享内存的控制是:不允许进程修改任何与其他进程共享的虚拟页面,除非所有的共享者都显式地允许它这么做。
在Copy-On-Write中,当需要写一段共享内存的时候,会将这段内存在另一个内存区域复制出副本,此时便不再是共享的内存,不必受到共享内存的限制。在其它情况下,如果要写共享内存,则需要对共享者之间的进程通信,得到所有进程的允许信号后才写入。
【由共享内存,可以扩展到C++多线程编程中的共享对象】
进程与线程的区别是,不同进程有不同的页表基地址,不容易出现共享内存,而线程作为一个进程内的子单元,使用同样的页表基地址,内存都是共享的。(在xv6实验multithreading中,提到linux对多线程的支持,待总结。)当一个对象被多个线程同时使用的时候,常常采用加锁的方式、或者shared_ptr/weak_ptr这种智能指针的方式保证数据读写安全,尤其是对象析构时期的安全,此时,这段共享内存的安全读写需要另一段内存里的对象的锁成员,或者栈上的智能指针来保护,这些都是在用户进程中显式约定的,而不是由OS控制。
虚拟内存的映射和高速缓存比较相似,那么虚拟内存和高速缓存是怎么结合,共同为CPU提供数据服务的呢?
答案是,CPU查找数据的时候,首先去cache中查找,cache查找失败的话,也是先从内存load到cache中,总之是一定经过cache的,这样的缓存原理也可以解释为什么近期浏览的网页记录丢失的话,可以从缓存里找到踪迹,因为近期访问过的内存都会存入缓存。
除了cache高速缓存之外,为了让cpu更快地访问内存,还有第二个措施,就是TLB快表。cache缓存的是物理地址对应的数据,快表缓存的是虚拟地址对应的物理地址,存放于MMU中,与处理器同在CPU芯片内。