《虚拟内存的架构和操作系统支持》一书(英文名为Architectural and Operating System Support for Virtual Memory),隶属于Synthesis Lectures on Computer Architecture系列。本节简单回顾了虚拟内存抽象的基础知识。
典型的虚拟地址空间
上图简单描述了一个32位和64位进程的地址空间,下图详细展示了一个名为/opt/test
的32位Linux进程的地址空间,这是通过打印虚拟文件/proc/<pid>/maps
的内容收集的。
用户和内核内存区域的划分是由操作系统决定的,在32位系统中由于内存容量有限,这一点很重要,例如Linux通常将3GB以下的内存作为用户空间,剩下1GB留给内核,而Windows采用2GB+2GB的分割方式。相比之下,64位系统这一点就不再那么关键,且大多数空间没有使用,例如x86-64架构目前要求虚拟地址的48-63位保持一致。
上图中我们注意到还有一段空间叫vDSO
,即virtually dynamically linked shared object,是一个特殊的性能优化,可以加速用户和内核代码之间的一些交互,比如内核管理了各种计时器数据结构,原来我们需要通过syscall来进行访问,但通过vDSO
可以直接访问这些结构。这也说明现代虚拟内存系统中,各种复杂的机制都是围绕内存保护来定义、执行和优化的。
内存权限
多线程程序
一个进程中的所有线程共享同一个地址空间,但每个线程都有自己的stack
共享内存、Synonym和Homonym
虚拟内存并不总是在虚拟和物理内存之间执行一对一的映射。一个被多个进程重复使用的单一虚拟地址可以指向多个物理地址,这叫做homonym。反之,如果多个虚拟地址指向同一个物理地址,则被称为synonym。共享内存更进一步,因为它允许多个进程设置不同的虚拟地址,这些虚拟地址指向同一个物理地址。
Homonym
TLB需要处理homonym的问题,第一种方法是在两个地址可能被先后使用(例如内核执行上下文切换)时刷新TLB,第二种方法是在虚拟地址上附加进程或地址空间的ID。不过TLB并不是唯一需要考虑homonym的结构,在VIVT缓存中也存在问题,对于数据不是只读的缓存等结构,使用进程ID的解决方案可能是不完整的。Load buffer和store buffer等处理器的内部结构也会受到影响,不能仅仅根据虚拟地址来进行数据转发。
Synonym
虚拟地址0xb974c000和0x39297000是synonym
Synonym也会带来很多问题。例如假设有两个虚拟页,其中第一个标记为dirty,但第二个页在访问时可能会认为是clean的,并在没有写回dirty数据的情况下直接覆盖了这一页。VIPT缓存会因为synonym导致一致性协议无法满足。和之前一样,load buffer和store buffer也会受到影响,例如尽管load的地址无法在store buffer中找到,但其实可能映射到了同一个物理页,从而错过了数据的转发,一个简单的解决方案是基于物理地址来进行转发,但会让TLB位于性能的关键路径上,更常见的方法是进行推测性的load,然后再进行物理地址检查。
线程本地存储
一些线程实现还提供了线程本地存储(TLS)的机制,TLS提供了进程的虚拟地址空间的一些子集,这些子集只能由该线程进行访问。在编程时,用户通过一些API来初始化一些数据结构来表示线程的本地存储数据。在运行时,TLS实现会给每个线程分配寄存器(如ARM的CP15 c13、x86的FS或GS)或基址指针。
TLS改变了虚拟地址在每个线程上被转换为物理地址的方式,在这个例子中,两个线程都使用虚拟地址0x90ed4c000,但每个线程在地址翻译之前都会加上自己的offset