本文主要根据 “Operating Systems: Three Easy Pieces” 第16章总结而来。
在本头条号的上一篇文章中,我们知道通过base/bounds 寄存器,操作系统可以把进程放到可用的物理地址内,让进程认为自己是独享的内存,而且操作系统还能保证进程间互不干扰。进程的地址空间示意图如下所示:
但是我们看到,上面的示意图中,堆和栈之间有很大一块空白区域,就算那块区域永远不会被进程用到,它也不会分配给别的进程。这样造成的问题首先是内存资源的浪费,其次,如果一个进程占用的内存太大,它甚至都找不到一块足够大的内存空间放置它。
可以看到,通过内存分段技术,没有内存空间被浪费了,而且内存地址翻译的方法也跟以前一样,只是说现在对应不同的段,有不同的基地址和界地址。我们平常写程序可能碰到“segmentation fault”这样的错误,一般指的就是访问的地址非法(比如超出了界地址的范围)。就算现在很多硬件不是使用这样的技术,这个错误信息还是被保存了下来并沿用至今。
但是上面的示意图的分段技术也引入了两个新的问题:
A. 如何判断需要翻译的虚拟地址是哪个段的?
我们说过,进程中的地址都是虚拟地址,虚拟地址要经过硬件被翻译成物理地址。由于内存被分段了,给定一个虚拟地址,硬件怎么知道该虚拟地址是哪个段的呢?这决定了用哪个段的base/bounds寄存器做地址翻译。 这里面有两种方法可供选择:
-
常用的是显式的方法:可以使用虚拟地址的前两位当做段的标志位。比如对于一个16位的地址,用最开头的 00 表示代码段,01 表示堆,11 表示栈,(注意这种假设里10没被用到),后面的14位地址当做偏移值。程序写起来也非常简单,示意代码如下所示:
-
隐式的方法:通过检测虚拟地址的生成方式。比如地址是PC指针,一般是代码段的地址,如果是SP栈指针,一般是栈地址,其他的就是堆地址。这种方法不太常用。
B. 怎么处理地址反向增长的问题?
通过内存分段的示意图我们看到,栈地址是反向增长的,偏移值越大它变的越来越小。对于这种情况,操作系统首先需要硬件MMU的帮助。在MMU中除了base/bounds寄存器,还有一个地方标记了地址增长的方向(比如一个标志位,存储这一段是正向增长还是反向增长)。然后做地址翻译的时候,如果是逆向增长,就用偏移量减去段空间的最大值,得到真正的负向偏移。界寄存器检查负向偏移的绝对值是否在范围之内。
代码段共享
随着计算机的发展,操作系统人员发现使用了分段技术以后,不同进程可以共享某些内存段,比如最常见的代码段。
当然了,为了支持代码段共享,操作系统也需要得到硬件的支持,主要是内存保护的标志位。为了让代码段共享变得安全,系统需要有一个标志位,标示该段是只读的,还是可读可写的。通过标记代码段是只读的,不同进程就可以安全地共享同一个代码段,而不用担心代码段被其他进程修改。虽然物理内存是共享的,但是对于单个进程来说,还是像它们独占了那个代码段一样。
操作系统算法也需要加入一段逻辑,即在修改内存区域值的时候,需要先判断该内存是否是只读的。
操作系统的作用
目前为止,我们已经看到了分段技术的基本原理,以及硬件在里面起到的作用。那么操作系统有什么问题需要解决呢?
-
首先是我们曾经讨论过的一个古老的问题:进程上下文切换。由于引入了分段,在进行上下文切换的时候,操作系统需要保存3对base/bounds寄存器的值,还需要保存地址增长方向的标志位,以及内存保护的标志位。
-
第二个是更重要也是更难的问题,可用的内存空间列表怎么维护。由于把内存分成了三段,每段的大小又不一样,当一个新进程开始运行的时候,操作系统需要寻找三块足够大小的内存区域放置这些段。随着进程越来越多,整个物理内存就会被分成大大小小很多段,每段之间会有一些空余的“洞”。对比与不分段进程内部的碎片,内存分段产生的“洞”被叫做外部碎片。
对于外部碎片的问题,有很多方法被用来尝试解决它。比如操作系统可以定时通过“压缩”整理外部碎片,操作系统把所有进程“停”下来,把它们的数据拷贝到一块连续的区域去,并同时改变它们的base/bounds地址。通过这种方式,操作系统可以有连续的更大的内存供分配使用。但是这种方式的代价比较大,需要让进程暂时停止运行。
一种更简单的方式是系统通过维护一个可用内存的列表,在列表中找可用的内存用于分配。查找的方法有很多,比如“best-fit”的方式是查找与要分配的内存大小最接近的内存块,“first-fit”是用列表中第一个找到的足够大小的内存块,其他的还有”worst-fit” 或者更复杂的比如 buddy 算法。
然而,正如有很多方法可以用,没有一个方法能完美解决外部碎片的问题,这些方法也只是一定程度缓解碎片化的问题。
总结
内存分段技术能帮助操作系统解决进程内空间浪费的问题,同时也让代码段共享成为可能,但是它也引入了外部碎片的问题。
另外内存分段技术还是不够灵活,它还有一个非常重要的问题没有解决。就是当有一大片内存,被用到的部分其实很少,但是它还是必须整个都放到物理内存中,这样也是一种浪费。而且,当进程虚拟空间的大小大于物理内存的时候,分段也放不下进程的全部大小,这时候分段技术也起不了作用了。
这些是后面我们会继续关注的内容!欢迎大家订阅我的头条号,第一时间收到更新,谢谢!
,