分段
根据前面介绍到的基址+界限寄存器对的方式,虽然很好的解决了地址转换的问题,但是可以看到,它也带来了一个问题:内存浪费。
根据前面介绍到的那种内存分配处理方式,堆和栈之间会有大量的空闲空间,而前面的介绍中,这些空间都会被一次性装入内存中,那在程序运行的初期,就会有大量没有被使用的内存被强行占用,造成物理内存极大浪费。
泛华的基址/界限寄存器
这里的解决方案就是引入了“段”的概念,这个想法很简单,在 MMU 中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段。一个段只是地址空间中一个连续定长的区域。
根据前面的介绍,这里可以分为三种类型的段:代码段、堆段和栈段。
下面给一个简单是示例图,其中左边是虚拟地址的分段逻辑,右侧则是采用这种分段机制后在物理内存中实际存放的情况,可以看到对于没有使用的物理地址,就无需在物理地址中对其进行分配了,大大节约了物理内存的浪费问题。
段 | 基址 | 大小 |
---|---|---|
代码 | 32KB | 2KB |
堆 | 34KB | 2KB |
栈 | 28KB | 2KB |
举例来说,假如有一个堆内的虚拟地址4200,这里在多基址+界限寄存器对的模式下,就不能是简单的直接加减进行操作,需要先计算改地址在对应段内的偏移量;这里地址是4200,而根据上面的图中看到的效果,堆地址的虚拟地址基址是4KB(4096),所以这里需要先计算:4200 - 4096 = 104;然后可以看到物理地址中,堆的基址为34KB,此时加上104偏移量,得到真正的物理地址:34920。
引用哪个段
引入了分段后,紧接着就面临一个问题:如何区分哪种分段对应哪种类型呢?这里有一个方案是可以在虚拟地址中前两位进行标识,比如:00表示代码段、01则表示堆这种。
还是以前面4200地址为例,对应的二进制数据为:
01 0000 0110 1000
可以看到,前面两位01表明当前地址是堆内地址,剩余的为偏移量:104.
上面的例子中,标识位用了两位,有些系统中会将堆和栈使用同一个标识,所以可以实现只用一个位就能标识出不同类型的段。
还有些硬件可以使用其他方案来确定地址所在分段,比如在隐式的方式中, 可以根据地址产生的方式来确定:比如,地址由程序计数器产生(即获取指令),那么地址就在代码段。如果是基于栈或者指针产生的地址,那地址一定就属于栈段;其他情况属于堆段。
如果访问超过了规定的段界限,就可能出现“段异常/段错误”。
栈怎么办?
前面一直没有说栈的问题,栈比较特殊,它属于反向增长,比如根据前面的表格中可以看到,栈的基址为28KB,大小为2KB,所以它的界限地址为26KB。那针对这种特殊的增长模式,需要一点硬件支持,同时需要引入一个特殊的位来表示到底是从小到大增长还是反过来增长。
假设此时需要访问虚拟地址为15KB的栈内地址,栈的虚拟基址为16KB,所以此时的偏移量就是:15KB - 16KB = -1KB
栈的物理基址地址为28KB,28KB + (-1KB) = 27KB。
段 | 基址 | 大小 | 是否反向增长 |
---|---|---|---|
代码 | 32KB | 2KB | 1 |
堆 | 34KB | 2KB | 1 |
栈 | 28KB | 2KB | 0 |
支持共享
随着分段的出现,逐渐发现,可以再通过增加一点硬件的支持,就能实现新的效率提升。具体一点就是:有时需要节省内存,会在地址内存空间共享某些内存段,尤其是现今的代码共享,非常常见。
为了达到这种效果,需要硬件引入一个保护位,也就是说为每个段增加几个位,标识该段是否能够被读写、被执行。
有了保护位,前面描述的硬件算法也必须改变。除了检查虚拟地址是否越界,硬件还需要检查特定访问是否允许。如果用户进程试图写入只读段,或从非执行段执行指令,硬件会触发异常,让操作系统来处理出错进程。
段 | 基址 | 大小 | 是否反向增长 | 保护 |
---|---|---|---|---|
代码 | 32KB | 2KB | 1 | 读-执行 |
堆 | 34KB | 2KB | 1 | 读-写 |
栈 | 28KB | 2KB | 0 | 读-写 |
细粒度与粗粒度的分段
到目前为止,大多的例子中,只是将地址空间分为了三大类:代码段、堆、栈。可以认为这种分段是粗粒度的(coarse-grained),因为它将地址空间分成较大的、粗粒度的块。
有一些早期的系统可能更加灵活一点,允许将地址空间划分为大量较小的段,这被称为细粒度(fine-grained)分段。
如果要支持这种许多段的划分,需要内存中引入段表来进行记录。
例如,像 Burroughs B5000 这样的早期机器可以支持成千上万的段,有了操作系统和硬件的支持,编译器可以将代码段和数据段划分为许多不同的部分。当时的考虑是,通过更细粒度的段,操作系统可以更好地了解哪些段在使用哪些没有,从而可以更高效地利用内存。
操作系统支持
上下文切换
各个段寄存器中的内容必须保存和恢复。显然,每个进程都有自己独立的虚拟地址空间,操作系统必须在进程运行前,确保这些寄存器被正确地赋值。
空间碎片问题
一般会遇到的问题是,物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation)。
该问题的一种解决方案是紧凑(compact)物理内存,重新安排原有的段。
例如,操作系统先终止运行的进程,将它们的数据复制到连续的内存区域中去,改变它们的段寄存器中的值,指向新的物理地址,从而得到了足够大的连续空闲空间。
这样做成本很大,因为这一段逻辑是内存密集型操作,可能非常耗时。
一种更简单的做法是利用空闲列表管理算法, 试图保留大的内存块用于分配。相关的算法可能有成百上千种,包括传统的最优匹配(best-fit,从空闲链表中找最接近需要分配空间的空闲块返回)、 最坏匹配(worst-fit)、 首次匹配(first-fit) 以及像伙伴算法(buddy algorithm)这样更复杂的算法。
但遗憾的是,无论算法多么精妙,都无法完全消除外部碎片,因此,好的算法只是试图减小它。
标签:07,基址,导论,虚拟地址,地址,内存,2KB,分段,操作系统 From: https://www.cnblogs.com/StillLoving/p/segmentation.html