3.7 分段

到目前为止我们讨论的虚拟内存都是一维的,虚拟地址从0到最大地址,一个地址接着另一个地址。对许多问题来说,有两个或多个独立的地址空间可能比只有一个要好得多。比如,一个编译器在编译过程中会建立许多表,其中可能包括:

1)被保存起来供打印清单用的源程序正文(用于批处理系统)。

2)符号表,包含变量的名字和属性。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

3)包含用到的所有整型量和浮点常量的表。

4)语法分析树,包含程序语法分析的结果。

5)编译器内部过程调用使用的堆栈。

前4个表随着编译的进行不断地增长,最后一个表在编译过程中以一种不可预计的方式增长和缩小。在一维存储器中,这5个表只能被分配到虚拟地址空间中连续的块中,如图3-31所示。

阅读 ‧ 电子书库
图 3-31 在一维地址空间中,当有多个动态增加的表时,一个表可能会与另一个表发生碰撞

考虑一下如果一个程序有非常多的变量,但是其他部分都是正常数量时会发生什么事情。地址空间中分给符号表的块可能会被装满,但这时其他表中还有大量的空间。编译器当然可以简单地打印出一条信息说由于变量太多编译不能继续进行,但在其他表中还有空间时这样做似乎并不恰当。

另外一种可能的方法就是扮演侠盗罗宾汉,从拥有过量空间的表中拿出一些空间给拥有极少量空间的表。这种处理是可以做到的,但是它和管理自己的覆盖一样,在最好的情况下是一件令人讨厌的事,而最坏的情况则是一大堆单调且没有任何回报的工作。

我们真正需要的是一个能够把程序员从管理表的扩张和收缩的工作中解放出来的办法,就像虚拟内存使程序员不用再为怎样把程序划分成覆盖块担心一样。

一个直观并且通用的方法是在机器上提供多个互相独立的称为段(segment)的地址空间。每个段由一个从0到最大的线性地址序列构成。各个段的长度可以是0到某个允许的最大值之间的任何一个值。不同的段的长度可以不同,并且通常情况下也都不相同。段的长度在运行期间可以动态改变,比如,堆栈段的长度在数据被压入时会增长,在数据被弹出时又会减小。

因为每个段都构成了一个独立的地址空间,所以它们可以独立地增长或减小而不会影响到其他的段。如果一个在某个段中的堆栈需要更多的空间,它就可以立刻得到所需要的空间,因为它的地址空间中没有任何其他东西阻挡它增长。段当然有可能会被装满,但通常情况下段都很大,因此这种情况发生的可能性很小。要在这种分段或二维的存储器中指示一个地址,程序必须提供两部分地址,一个段号和一个段内地址。图3-32给出了前面讨论过的编译表的分段内存,其中共有5个独立的段。

阅读 ‧ 电子书库
图 3-32 分段存储管理,每一个段都可以独立地增大或减小而不会影响其他的段

需要强调的是,段是一个逻辑实体,程序员知道这一点并把它作为一个逻辑实体来使用。一个段可能包括一个过程、一个数组、一个堆栈、一组数值变量,但一般它不会同时包含多种不同类型的内容。

除了能简化对长度经常变动的数据结构的管理之外,分段存储管理还有其他一些优点。如果每个过程都位于一个独立的段中并且起始地址是0,那么把单独编译好的过程链接起来的操作就可以得到很大的简化。当组成一个程序的所有过程都被编译和链接好以后,一个对段n中过程的调用将使用由两个部分组成的地址(n,0)来寻址到字0(入口点)。

如果随后位于段n的过程被修改并被重新编译,即使新版本的程序比老的要大,也不需要对其他的过程进行修改(因为没有修改它们的起始地址)。在一维地址中,过程被一个挨一个紧紧地放在一起,中间没有空隙,因此修改一个过程的大小会影响其他无关的过程的起始地址,而这又需要修改调用了这些被移动过的过程的所有过程,以使它们的访问指向这些过程的新地址。在一个有数百个过程的程序中,这个操作的开销可能是相当大的。

分段也有助于在几个进程之间共享过程和数据。这方面一个常见的例子就是共享库(shared library)。运行高级窗口系统的现代工作站经常要把非常大的图形库编译进几乎所有的程序中。在分段系统中,可以把图形库放到一个单独的段中由各个进程共享,从而不再需要在每个进程的地址空间中都保存一份。虽然在纯的分页系统中也可以有共享库,但是它要复杂得多,并且这些系统实际上是通过模拟分段来实现的。

因为每个段是一个为程序员所知道的逻辑实体,比如一个过程、一个数组或一个堆栈,故不同的段可以有不同种类的保护。一个过程段可以被指明为只允许执行,从而禁止对它的读出和写入;一个浮点数组可以被指明为允许读写但不允许执行,任何试图向这个段内的跳转都将被截获。这样的保护有助于找到编程错误。

读者应该试着理解为什么保护在分段存储中有意义,而在一维的分页存储中则没有。在分段存储中用户知道每个段中包含了什么。例如,一般来说,一个段中不会既包含一个过程又包含一个堆栈,而是只会包含其中的一个。正是因为每个段只包含了一种类型的对象,所以这个段就可以设置针对这种特定类型的合适的保护。图3-33对分段和分页进行了比较。

阅读 ‧ 电子书库
图 3-33 分页与分段的比较

页面的内容在某种程度上是随机的,程序员甚至察觉不到分页的事实。尽管在页表的每个表项中放入几位就可以说明其对应页面的访问权限,然而为了利用这一点,程序员必须跟踪他的地址空间中页面的界限。当初正是为了避免这一类管理工作,人们才发明了分页系统。在分段系统中,由于用户会认为所有的段都一直在内存中,也就是说他可以当作所有这些段都在内存中那样去访问,他可以分别保护各个段,所以不需要关心覆盖它们的管理工作。

3.7.1 纯分段的实现

分段和分页的实现本质上是不同的:页面是定长的而段不是。图3-34a所示的物理内存在初始时包含了5个段。现在让我们考虑当段1被淘汰后,比它小的段7放进它的位置时会发生什么样的情况。这时的内存配置如图3-34b所示,在段7与段2之间是一个未用区域,即一个空闲区。随后段4被段5代替,如图3-34c所示;段3被段6代替,如图3-34d所示。在系统运行一段时间后内存被划分为许多块,一些块包含着段,一些则成了空闲区,这种现象称为棋盘形碎片或外部碎片(external fragmentation)。空闲区的存在使内存被浪费了,而这可以通过内存紧缩来解决。如图3-34e所示。

阅读 ‧ 电子书库
图 3-34 a)~d)棋盘形碎片的形成;e)通过紧缩消除棋盘形碎片