非连续内存分配

概述:为什么需要非连续内存?

从上一节介绍的连续分配内存方法中,我们可以发现,无论采用什么连续内存分配方法,都会带来一些无法避免的问题。

   连续内存分配的缺点

  • 分配给一个程序的物理内存是连续的
  • 内存利用率较低
  • 有外碎片、内碎片的问题

  如果采用非连续的内存分配方法,能不能改善这些问题呢?为此我们需要了解非连续内存分配的优缺点。

  非连续分配的优点

  • 一个程序的物理地址空间是非连续的
  • 更好的内存利用和管理
  • 允许共享代码与数据(共享库等...)
  • 支持动态加载和动态链接

   非连续分配的缺点
   如何建立虚拟地址和物理地址之间的转换之间的开销,这里有两个方案

  • 软件方案
  • 硬件方案

   如果完全依赖计算机软件实现硬件地址和软件地址之间的转换,这里面的开销是相当大的,所以很多时候必须依赖于硬件中的某些部分,共同进行非连续内存的管理。本文主要介绍两种硬件方法

  • 分段(Segmentation)
  • 分页(Paging)

分段

 关注的问题

  • 程序的分段地址空间
  • 分段寻址机制

    程序的分段地址空间

  我们首先看一下,在分段视角下,应用程序是什么样子:

  如图所示,在分段视角下,代码和程序都能被分为不同的段,代码可以被分解为主程序,共享库等,数据可以被分为栈段,堆段等。分段机制希望把这些段进行隔离,从而更好地管理应用程序。 对于一个应用程序来说,它的逻辑地址空间是虽然是连续的,但通过分段机制,可以将逻辑地址中的内容分散到不连续的物理地址空间中。从而实现程序不同部分的分离:

 

  

分段的优点

1. 代码段之间可以共享

2. 让数据隔离,实现保护机制

  通过段管理机制,能够将连续的逻辑地址不同的内存块分别映射到物理地址中分散的内存块中:

 

   

 分段寻址方案

  仅仅通过软件来完成逻辑地址到物理地址的映射的开销是很大的,所以如文章开头所说的那样,需要硬件的支持。下面来介绍一下分段机制下的寻址方案。

  一个一维的逻辑地址是由不同的段组成的,这些段可以不连续。逻辑地址到物理地址的映射也就是对这些段进行寻址。对段的寻址可以分成两部分,一是段的寻址,二是段内偏移的寻址。所以程序访问内存地址需要一个二维的二元组(s,addr),s为段号,addr为段内偏移。根据段号和段内偏移是否分离,又可以将分段寻址方式分为单地址实现方案和段寄存器+地址寄存器的实现方案。单地址实现方案下,段号和段内偏移是合并管理的,段寄存器+地址寄存器方式则是用段寄存器管理段号,地址寄存器管理段内偏移。

 分段寻址硬件实现方案

  下面根据这幅示意图,对硬件如何实现分段寻址做一个讲解。P是一个正在运行的程序,正通过CPU执行某条指令。CPU此时要根据逻辑地址进行寻址。首先先对段号进行处理,通过段可以找到该段所在物理内存的起始地址。硬件为此设置了一种叫段表的机制,来存储逻辑地址段号和物理地址段号的对应关系。除映射之外,段表还存储了每段长度的限制。段表的索引项即为逻辑地址的段号,因此,通过逻辑地址的段号可以在段表中找到该段对应物理内存的起始地址和长度限制。之后,CPU会对查询的结果进行检查,判断逻辑地址是否满足段表中的限制。如果满足,代表寻址合法,则将起始地址和逻辑地址中的偏移量相加,形成一个物理地址,CPU根据该物理地址获取数据,进行进一步处理。否则则是一次非法访问,CPU产生异常,交给操作系统来处理。

  段表由操作系统在寻址之前建立,具体的建立方式和硬件有着紧密地联系。

分页

  在现代操作系统中,比分段管理机制用的更多的是分页管理机制。分页和分段的区别在于,段的长度不确定,但页帧的大小是固定不变的。在分页机制中,将物理内存划分为固定大小的帧(frame),大小为2的幂,将逻辑地址空间划分为相同大小的页(page)。分页机制要建立页和帧之间的转换。

 帧(Frame)

 

  在分页机制中,帧代表了物理内存的地址。一帧分为两部分,可以用二元组(f,o)表示。其中,f代表帧号(F位,共用2F帧),o代表帧内偏移(S位,每帧有2S字节)。根据帧的信息,可以计算出物理地址:

物理地址 = $2^{S}\times f+o$

 

  下面通过一个例子进一步说明页帧的寻址方式。设有16bit的地址空间,9bit(512 byte)大小的页帧,有一页帧的物理地址被表示为(3,6),则它实际的物理地址可以被计算出,过程如下:

 页(Page)

  接着在介绍一下逻辑地址,也就是页(page)的寻址方式。这个过程和帧的寻址方式大体相同。页号和帧号不一定相同,但业内偏移和帧内偏移大小相等。一页同样可以表示为二元组,用(p,o)表示,p代表页号(P位,共有2P页),o代表业内偏移(S位,每页有2S个字节)。根据帧的信息,可以计算出物理地址:

逻辑地址 = $2^{S}\times p+o$  

 页寻址机制

  

  页寻址机制和段寻址机制是类似的。程序运行时,CPU会根据逻辑地址(p,o)去寻址。CPU将页号p作为索引,在页表(page table)中查询(页表的内容会在后文进一步介绍)对应的帧号。得到帧号后,可根据上文介绍的物理地址计算公式,得到物理地址。这个过程要注意两点,第一,要进行查询,除了页号之外,还需要知道页表的基地址。第二,物理地址和逻辑地址的偏移量是相同的。

   与分段机制不同的是,分页机制页内偏移的大小是固定的。一般逻辑地址空间的大小是大于物理地址空间的,所以不是所有的页都有对应的帧。在介绍虚拟内存时会对这个问题有进一步讲解。页是连续的虚拟内存,帧是非连续的物理内存,页寻址机制的核心,就是将页映射为帧,这样带来的好处是减少内存碎片。

 页表

  每个运行的程序都有一个页表。页表属于程序的运行状态,会动态变化。

  页表结构

 

 

  页表在寻址之前,由操作系统建立完成。页表内最重要的信息是帧号,帧号和帧内偏移一起组成了物理地址。页表除了存储帧号以外,还有一些特殊的bit。这些bit都有一些特殊的用途,例如记录页在内存当中是否存在,该页是否被读写等。

    地址转换的实例

  下面来看一个地址转换的实例。有一个16位地址的系统,32KB的物理内存,每页大小为1024byte。程序运行时,CPU根据逻辑地址,去页表中查找对应的帧号。这时可能会遇上两种情况。第一种情况,如逻辑地址为(4,0)的页,该页的页号为4,查找页表发现,它的驻留位为0,这代表逻辑地址对应的帧不在内存中,这时程序会产生异常,杀死进程。第二种情况,如逻辑地址为(3,1023)的页,查找页表发现,它的驻留位为1,这代表对应的帧确实在物理内存当中,这时可以查找到该页对应的帧号为4,从而得到对应的物理地址(4,1023)。这就是分页机制下,逻辑地址到物理地址的映射过程。

   分页机制的性能问题

 分页机制会带来空间和时间的开销问题。

    • 访问一个内存单元需要两次内存访问
      • 一次用于获取页表项,一次用于访问数据
    • 页表可能相当大
      • 64位机器如果每页1024字节,那么一个页表的大小护士多少?(254)
      • CPU中可能会运行多个应用程序,每个应用程序的地址是隔离的,所以同时会有多份页表存在

 处理方法

    • 缓存(Caching),将常用数据缓存到离CPU很近的地方
    • 间接(Indirection)访问,把大空间拆成小空间

   Translation Look-aside Buffer(TLB)

 

 

  Translation Look-aside Buffer,简称TLB或者快表,可用来解决页表查询的时间开销问题。TLB存在与CPU中的内存管理单元(MMU)当中,它起到的作用相当于缓存了页表当中的内容。如图所示,Key和Value组成了TLB的表项,分别存储页号和帧号。TLB的表项通过关联内存(assocative memory)实现,具备快速访问能力,但容量有限。TLB可以存储一些常用的页表项。在查询帧号是,CPU会根据页号首先查询TLB,如果TLB命中,则可以直接获取帧号,不需要再去查页表,如果TLB未能命中,则再次查找页表,并将对应的表项更新到TLB中(更新的方式取决于CPUDE 类型)。

    为了尽量提高TLB命中率,编写程序时需要尽量满足局部性原理。

 二级页表和多级页表

 

  多级页表可用来解决页表查询时的空间开销问题。多级页表将页的页号分成多部分,从而将一个大的页表拆分为多个小的页表。我们先从二级页表入手。二级页表的寻址中,页的页号可分为p1,p2两部分。p1为一级页表的页号,p2为二级页表的页号。CPU寻址过程中,先根据一级页表的基地址和p1,查找一级页表的页表项。一级页表的页表项中存储了二级页表的起始地址,之后结合p2,可以找到二级页表中的页表项,其中存放了帧号。

  二级页表可以使某些不存在映射关系的页表项不再占用内存。假设一级页表的某页表项的驻留位为0,则它指向的二级页表不需要存储与内存中,相比之下,一级页表中,无论页表项是否存在映射关系,页表项都存储在页表中,占用了内存。所以,二级页表与一级页表相比,节省了大量空间。

  根据这种思路,可以进一步将页号分成多个部门,来实现多级的间接页表。

 反向页表

  无论是单级页表还是多级页表,页表的大小和逻辑地址的大小有直接的关系,逻辑空间越大,寻址的空间也就越多。那么,在大地址空间下,前向映射页表变得繁琐。因为逻辑(虚拟)地址空间增长速度快于物理地址空间,那么,有没有一种方法,不是让页表与逻辑地址空间的大小相对应,而是让页表与物理地址空间的大小相对应,这样就可以降低查询页表时的时间开销。

  反向页表就是解决这个问题的一种手段,和一般页表的查询过程相反,反向页表以帧号作为索引,反过来查询页号。下面介绍三种反向页表的设计方案:

  基于页寄存器的方案

  第一种方案通过页寄存器实现反向页表。每个帧和一个页寄存器相关联。寄存器内容包括:

    • Residence bit:是否被占用
    • Occupier:对应的页号p
    • Proteetion bits:保护位

  页寄存器中同样存储了映射关系,但是它是以帧号为索引来查找页号。

  寄存器的容量只与物理地址空间的大小相关,与逻辑地址空间的大小无关,这就限制了页表项的数量。假设物理内存的大小为16MB,页面大小为4KB,页帧数为4K,一个页寄存器占8bytes,那么整个页寄存器仅占32KB的空间,仅为物理内存的0.2%,

  基于寄存器方案最大的问题是,因为CPU知道的是逻辑地址,知道的是页号,而页寄存器的索引是帧号,所以要找到页号只能在寄存器里搜索,增加了时间的开销。

 

  基于关联内存的方案

 

  关联存储器是一种特殊的存储器,支持并行地查找页号对应的页帧号。关联存储器的缺陷在于它的硬件逻辑很复杂。如果帧数较少,则可以使用关联存储器,否则的话,关联存储器就显得不够实用。

 

  基于哈希查找的方案

 

  哈希查找是关联存储器的另一种实现方式,即将用页号查找帧号的过程用哈希表实现。哈希表或哈希函数的输入为页号,输出为帧号。使用哈希查找的开销要小于使用关联存储,但是它也还存在缺陷,在哈希查找时,会发生哈希碰撞,为了提高效率,可以为哈希函数添加当前进程的PID作为参数,以便于设计哈希函数。另外,进行哈希计算时,也需要到内存中去取数计算,有一定的时间成本。

  反向页表不受制于逻辑地址的大小,比之前提到的单级页表,多级页表更加节省空间。此外,不像每一个进程都有一个单独的页表,一个系统反向页表只需要一个反向页表。实现反向页表的代价是需要高效的映射机制来保证访问效率,这需要硬件和软件的配合。目前在一些比较高端的CPU当中,实现了反向页表机制。