我们已经讲述了缓存最基本的结构,然而一个核心的关键问题在于在现代的体系结构中,运行中的进程都是使用的虚拟地址,这是由硬件和操作系统提供了一个美好的假象。在上一节中,我们已经明确了一个核心关键点,内存地址与缓存之间是有一定的映射关系,那么我们是应该用虚拟地址还是物理地址呢?一种美好的愿望是我们直接使用虚拟地址,因为我们这样就不需要把虚拟地址通过MMU转化为物理地址,从而提高效率。然而,愿望始终是愿望。虚拟缓存给操作系统带来了极大的负担来保证一致性。

虚拟缓存的基本操作

无论如何,我们也只能有两种基本的操作,要么读要么写。当我们去读一个虚拟地址其内存值的时候,我们仍然按照相同的操作去查询缓存是否命中,如果缓存命中了,直接返回缓存值即可。如果缓存没有命中,我们需要先把虚拟地址通过MMU转化为物理地址,然后从内存中读取数据,并加载到缓存中。

写的问题就更加复杂了,由于写的策略有两种write-through和write-back(我们假设其都支持write-allocate)。我们需要考虑在这两种策略下,虚拟缓存是如何进行写的操作的。对于write-through,无论是命中了cache还是没有命中cache,其都需要把虚拟地址通过MMU转化为物理地址,然后写入内存。对于没有命中cache的情况,其会直接读取内存的值写入cache中。对于write-back而言,对于没有命中cache的情况,其和write-through是相似的,对于命中了cache的情况,我们只需要在cache中写入即可。

虚拟缓存

你可能会发现,除了我们需要读取内存的时候要把虚拟地址转化为物理地址的情况,虚拟缓存似乎也没有什么本质改变。然而,我们必须得思考一个问题,就是读写权限的问题。

  • 缓存读的虚拟地址对应的物理地址实际上已经变得无法读取了,然而缓存并不知道这个信息,如果我们仍然访问这个虚拟地址,缓存会直接给CPU返回其保存的值,显然我们需要判断这个物理地址是否可读。
  • 同理,缓存写的虚拟地址对应的物理地址也可能变得无法写入了。对于cache miss的情况,我们能够轻易地进行判断,因为我们会把虚拟地址通过MMU转化为物理地址,可以判断这个物理地址是否可写。对于cache hit的情况,如果其dirty flag已经是1,我们显然可以得知这个虚拟地址对应的物理地址是可写的。麻烦的情况在于dirty flag为0,我们必须知道其到底是否可写,所以我们需要把虚拟地址转化为物理地址去判断这个地址是否可写。

歧义(Ambiguity)与别名(Alias)

歧义

我们首先回忆一下操作系统是如何与硬件结合实现虚拟地址转化到物理地址。对于每一个进程,操作系统需要维持一个页表,这个页表包含了从虚拟地址到物理地址的映射,操作系统分配内存时,会自动从空闲的内存中分配页然后修改页表,然后按照硬件规则完成虚拟地址到物理地址的映射。也就是对于两个不同的进程,其相同的虚拟地址完全可以对应不同的物理地址。当操作系统对进程进行调度后,由于我们是按照虚拟地址进行缓存的映射,虚拟缓存无法意识到这个物理地址已经改变了。因此,操作系统在此时必须做一些事情来刷新缓存。

虚拟缓存-歧义

别名

进程间的通信有一个重要的方法就是共享内存,即不同的虚拟地址能够映射到同一个物理地址。然而,由于两个进程的虚拟地址不同,其对应的cache set也会不同,假设采取的策略为write-back,当某一个进程修改了共享内存,其只会改变与其对应的cache line的值,却不会改变另一个进程的cache line的值,进而导致不一致问题。

虚拟缓存-别名

操作系统与虚拟缓存的交互

由于歧义和别名问题,在许多场景下,操作系统必须要采取一些策略来保证缓存的一致性。

上下文切换

当发生上下文切换时,一个虚拟地址可能会映射到另一个物理地址,此时虚拟缓存就应该失效了。所以内核会强制刷新所有当前进程的缓存。对于只进行了读操作的cache line,我们只需要让这个cache line的valid位置为0。对于进行了写的操作,如果其策略为write-through,其操作也很简单,将valid位置为0,然而如果写策略为write-back,我们需要把每一个改变了的cache line重新写入内存,会花费大量的时间。

除此之外,如果上下文切换相当地频繁,缓存的命中率会急剧地下降。因此,虚拟缓存最合适的场景是计算型任务以及批处理任务。减少了虚拟地址转化为物理地址的损耗,同时避免上下文切换带来的性能损耗。

共享内存

对于两个进程使用共享内存的情况,是不存在别名问题的,因为上下文切换时会自动解决这个问题。然而,麻烦的在于可能存在同一个进程两个不同的虚拟地址映射到同一块共享内存,这样就导致了别名问题。一个很简单的解决方法就是不使用cache。

I/O

对于buffered IO而言,由于操作系统管理的内核使用统一的虚拟地址,其不会存在歧义和别名问题。然而,最大的问题在于DMA设备,由于DMA设备直接与内存交互,会导致缓存与内存的不一致问题,即unbuffered I/O。

当用户采用系统调用write时,且缓存写策略为write-back时,不会直接把修改的数据写入到内存中,如果此时DMA设备读取相关的数据,就会导致不一致性问题。所以当DMA设备需要读取数据时,操作系统应强制刷新缓存。如果采用阻塞I/O,当执行write或者read系统调用时,操作系统本身就会进行上下文的切换,对缓存进行刷新。

参考资料