分段与分页

利用基址寄存器和界限寄存器可以很容易把不同进程重定位到不同的物理内存区域,但是对于这些内存区域,堆和栈之间有一个很大的空闲区域。如果将整个地址空间都放到物理内存,那么堆和栈之间的空间没有被进程使用,从而造成浪费;此外进程使用的空间要比实际物理内存大,那么物理内存也承载不了。所以仅仅使用基址寄存器和界限寄存器并不能很好解决虚拟内存的问题

操作系统引入了分段和分页来分别解决这两个问题。关于分段和分页,其本身也是一个很大的议题,这里只讲述其基本的原理和知识点,如果想了解更深,可以参考《Operating System: three easy pieces》和《现代操作系统》

分段

分段的想法很简单,在MMU(内存管理单元)中引入不止一个基址寄存器和界限寄存器,而是给地址空间内的每个逻辑段一对。一段只是地址空间的一部分,经典的分段是分为代码、堆和栈,当然也有存在更细粒度的分段。

通过分段,可以将不同段的代码映射到不同的物理内存地址处,从而避免了虚拟内存中未使用的部分占用实际的物理内存

分段实现

分段一般额外提供两个数据,一个是段号,一个是段内地址(一般用偏移量表示,称段内偏移量)。段号是用来表明访问哪个段,即使用哪一对基址寄存器和界限寄存器,段内偏移量则是通过计算的虚拟地址

常用段地址计算的方式有硬件和软件两种方式。软件方式是比较常用的,利用地址的前几位来表示段号,后面表示段内偏移量,如图。硬件还有其他方法来决定特定地址在哪个段。在隐式(implicit)方式中,硬件通过地址产生的方式来确定段。例如,如果地址由程序计数器产生(即它是指令获取),那么地址在代码段。如果基于栈或基址指针,它一定在栈段。其他地址则在堆段。通常使用软件的方式比较多,这种方式在很多地方也有借鉴使用,比如雪花算法

对于栈(从大到小方向增长)的地址计算,还需要一个硬件支持的标志位,增长方向

同时,对于现代操作系统而言,共享是常用的技术,段也可以支持共享,只需要在硬件基础上添加一个保护位(读写位),来让段支持共享、保护和隔离的功能。增加了保护位,硬件判定也需要做相应修改,在进入物理地址时,需要判定访问权限

分段带来的问题

分段确实能够节省大量的物理空间,然而引入分段,也同样引入了问题

其一,操作系统在进行上下文切换时,需要保存和恢复寄存器,每个进程都有自己独立的地址空间,在进行切换时,需要保证寄存器正确赋值。这个问题操作系统实现就可以了

其二,管理物理内存的空闲空间。新的地址空间被创建时,需要在物理内存中分配,有时候虽然有很多空闲内存,但是找不到一块完整的内存大小分配,这个问题就是外部碎片。这种解决办法有两个,一种是紧凑物理内存,重新安排原有的段,说白了就是把所有的物理内存回收,然后重新分配,可想而知,这种方案效率及其低下;另一种是利用空闲列表管理算法,试图保留更大的内存块用于分配,这种算法有很多,后续会简单介绍

分页

分段管理空间,将空间分成不同的片之后,会存在外部碎片,随着时间的推移,导致分配内存比较困难,所以操作系统又引入了分页。

分页是把空间分成固定大小的单元,每个单元称为一页,相应的,我们把物理内存看成定长槽块的阵列,叫做页帧,每一个页帧包含一个虚拟内存页。虚拟内存页可以放到物理内存的任何页帧,不必连续。

优点:

  • 灵活。不用管进程如何使用地址空间,不会假定堆和栈的增长方向
  • 简单。提供给空闲空间管理的简单性

分页实现

为了记录虚拟地址在物理内存的位置,操作系统为每个进程保存一个数据结构,称为页表(PTE)。页表的主要作用是为地址空间的每个虚拟页面保存地址转换,从而知道每个页在物理内存中的位置

跟分段类似,分页为了转换,也需要两个变量:虚拟页面号(virtual page number, VPN)和页内偏移量。跟分段类似,前N位表示虚拟页面号,后面表示页内偏移量

页表有很多标志位,主要有有效位、保护位、存在位、参考位,用于一些硬件控制,具体可以参考Intel架构手册

分页存在的问题

  • 页表会非常大。因为操作系统需要存页表数据结构,需要大量的内存去存储虚拟页面号
  • 速度会很慢。对于每个内存引用,分页都需要我们执行一个额外的内存引用,以便首先从页表中获取地址转换。工作量很大!额外的内存引用开销很大,在这种情况下,可能会使进程减慢两倍或更多

所以分页需要仔细设计

快速地址转换(TLB)

由于纯软件实现分页,会导致上述的两个问题,首先看下分页速度慢的问题。如何快速进行分页地址转换,这就需要借助硬件来实现TLB(translation-lookaside buffer)。简单来说,TLB就是加了一个缓存,硬件的缓存而已

TLB基本算法:

  • 从虚拟地址提取页号(VPN)
  • 检查TLB是否有该VPN的映射
  • 如果有,则直接取出页帧号(PFN)
  • 如果没有,则进行转换,然后更新TLB

未命中缓存的处理设计:

一般有硬件和软件两种方案,硬件就是发生未命中时,硬件遍历页表,找到正确页表,取出想要的转换映射,然后更新TLB。更现代的做法是,发生未命中时,硬件抛出一个异常,然后系统陷入内核,内核处理映射,最后更新TLB,然后跳出内核,最后硬件重试该指令(此时会命中)

TLB的内容大致为VPN|PFN|其他位,其他位主要是有效位、保护位、地址空间标识符、脏位等

有了TLB,那么在上下文切换的时候,也会引入新问题,就是TLB怎么处理。一种可行的方案是清空TLB,新的进程就用完全新的TLB,但是这种方案开销太大。为了减少这种开销,一些系统增加了硬件支持,实现跨上下文切换的 TLB 共享。比如有的系统在 TLB 中添加了一个地址空间标识符,可以把地址空间标识符看做是进程标识符,标记是这个进程的,如此,TLB就可以缓存不同进程的地址空间映射

另外还有一个问题是TLB的空间也有一定的大小,如果空间满了,那么怎么替换,这种一般是采用LRU的策略。TLB基本上涵盖了现代缓存机制的内容,代码中实现缓存的话,完全可以考虑TLB的这种思想

减小页表内存

前面分页引入了内存大的问题,这个问题的本质是如何让页表更小,主要的解决方案是:更大的页、分段分页混合实现、多级页表、反向页表。

更大的页

做一个简单的比喻,进程的内存看做是一本书,一本书的总页数其实就是页表存储的内存大小,如果书的页面越大,那么页数就越小。页表其实是类似的道理,所以将页表变大,就能减少内存消耗,这种方案会造成页面内的存储浪费,也就是内部碎片

分段和分页混合实现

将程序的每个段提供一个页表,常规的分段中分为代码、堆和栈三个段,那么页表也会提供三个,代码、堆和栈各一个。但是基址寄存器不是保存段号,而是保存该段在页表的物理地址,界限寄存器表示页表的结尾(即有多少有效页)

整个虚拟地址结构大致如图。前两位表示段号,中间是页号,最后表示映射地址

分段和分页虚拟地址结构

在 TLB 未命中时(假设硬件管理的 TLB,即硬件负责处理 TLB 未命中),硬件使用分段位(SN)来确定要用哪个基址和界限对。然后硬件将其中的物理地址与 VPN 结合起来,形成页表项(PTE)的地址

这种方法并非没有问题。首先,它仍然要求使用分段。正如我们讨论的那样,分段并不像我们需要的那样灵活,因为它假定地址空间有一定的使用模式。例如,如果有一个大而稀疏的堆,仍然可能导致大量的页表浪费。其次,这种杂合导致外部碎片再次出现。尽管大部分内存是以页面大小单位管理的,但页表现在可以是任意大小(是 PTE 的倍数)。

多级页表

多级页表的思想很简单。首先,将页表分成页大小的单元。然后,如果整页的页表项(PTE)无效,就完全不分配该页的页表。为了追踪页表的页是否有效(以及如果有效,
它在内存中的位置),使用了名为页目录(page directory)的新结构。页目录因此可以告诉你页表的页在哪里,或者页表的整个页不包含有效页

多级页表方法非常有效,现在很多现代操作系统都使用它。

如图展示了一个例子。图的左边是经典的线性页表。即使地址空间的大部分中间区域无效,我们仍然需要为这些区域分配页表空间(即页表的中间两页)。右侧是一个多级页表。页目录仅将页表的两页标记为有效(第一个和最后一个);因此,页表的这两页就驻留在内存中。因此,你可以形象地看到多级页表的工作方式:它只是让线性页表的一部分消失(释放这些帧用于其他用途),并用页目录来记录页表的哪些页被分配。

线性页表和多级列表

在一个简单的两级页表中,页目录为每页页表包含了一项。它由多个页目录项(Page Directory Entries,PDE)组成。PDE(至少)拥有有效位(valid bit)和页帧号(page frame number,PFN),类似于 PTE。但是,正如上面所暗示的,这个有效位的含义稍有不同:如果 PDE 项是有效的,则意味着该项指向的页表(通过 PFN)中至少有一页是有效的,即在该 PDE 所指向的页中,至少一个 PTE,其有效位被设置为 1。如果 PDE 项无效(即等于零),则 PDE的其余部分没有定义。

多级页表有明显优势

  • 节省空间。多级页表分配的页表空间,与你正在使用的地址空间内存量成比例
  • 紧凑,支持稀疏地址空间
  • 容易管理内存。页表的每个部分都可以整齐地放入一页中

也存在一些缺点

  • TLB未命中时,需要加载两次
  • 复杂度提升

反向页表

在反向页表(inverted page table)中,可以看到页表世界中更极端的空间节省。在这里,我们保留了一个页表,其中的项代表系统的每个物理页,而不是有许多页表(系统的每个进程一个)。页表项告诉我们哪个进程正在使用此页,以及该进程的哪个虚拟页映射到此物理页。

现在,要找到正确的项,就是要搜索这个数据结构。线性扫描是昂贵的,因此通常在
此基础结构上建立散列表,以加速查找。

小结

这篇文章是通过虚拟内存和物理内存之间的映射管理来讲述了分段和分页的实现方案,两者基本的定义以及实现,还有各自存在的一些问题,都进行了简单的描述。需要注意的是,其中很多思想,都是在开发过程会碰到的,也是可以借鉴的。而对于分页来说,页表只是一个数据结构,其实现有多种方式,可以自己根据需要自己实现

posted @   xnzone  阅读(395)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示