try ai
科普
编辑
分享
反馈
  • 分段与分页:一种混合式内存管理方案

分段与分页:一种混合式内存管理方案

SciencePedia玻尔百科
核心要点
  • 分段与分页结合了分段的逻辑组织性与分页的物理分配效率,解决了外部碎片和内部碎片问题。
  • 地址转换是一个两阶段过程,其中内存管理单元 (MMU) 首先根据段的界限验证访问,然后使用页表查找物理帧。
  • 双重检查机制创建了分层安全性,使用段进行粗粒度保护(如特权级别),使用页进行细粒度控制(如保护页)。
  • 系统性能严重依赖转换后备缓冲区 (TLB) 来缓存地址转换,但频繁的跨段访问可能导致 TLB 未命中和性能下降。

引言

在复杂的计算机体系结构世界中,内存管理是操作系统执行的最关键和最具挑战性的任务之一。历史上,有两种截然不同的理念主导着这一领域:分段,提供了一种符合逻辑、对程序员友好的内存视图;以及分页,提供了一种物理上高效的内存分配方式。然而,每种方法都有其显著的缺点——分段导致了浪费空间的外部碎片,而纯分页则丧失了宝贵的逻辑结构。本文通过探讨这两种思想的强大结合——分段与分页结合,来满足对更优模型的需求。

在接下来的章节中,我们将揭示这种混合式内存管理方案。在“原理与机制”一章中,我们将剖析其核心概念,探讨这种组合如何在保留逻辑分离的同时消除碎片,并详细介绍使其成为可能的复杂的两步式地址转换过程。随后,在“应用与跨学科联系”一章中,我们将看到这一理论的实际应用,考察其在实现共享库、增强系统安全以及优化从高性能计算到现代编程语言等领域性能方面的关键作用。让我们首先从理解那些使这个复杂系统成为现代计算基石的原理开始。

原理与机制

要真正欣赏一台复杂机器的精巧,我们必须首先理解它旨在解决的问题。在计算机内存的世界里,存在过两种简单而优雅的思想,每一种本身都很优美,但都带有一个致命的缺陷。这两种思想——​​分段​​和​​分页​​——的结合是一个关于妥协、综合并创造出一个远比其各部分之和更强大的系统的故事。

两全其美

让我们首先思考这两种对立的哲学。​​分段​​以程序员的方式看待内存:将其视为逻辑单元的集合。你有一个代码块、一个数据块、一个用于存放临时变量的栈等等。这些单元中的每一个都是一个段。这是一个非常直观的模型。它允许操作系统对这些逻辑单元施加保护——例如,将代码段设为只读以防止错误破坏它,或确保栈段在增长时不会与数据段冲突。

但这个优美的逻辑模型遇到了一个棘手的物理问题:​​外部碎片​​。想象一下你计算机的内存是一排长长的书架。当一个程序启动时,它会为自己的段请求几块连续的书架空间。当它结束时,这些书架空间就空闲了。随着时间的推移,在许多程序进进出出之后,你书架上的空闲空间不再是一个大的整块,而是一堆分散的小空隙。现在,一个新的程序来了,需要一个大的连续空间来存放它的代码——比如说,一个 17 KiB17 \text{ KiB}17 KiB 的段。你可能总共有 29 KiB29 \text{ KiB}29 KiB 的空闲空间,分散在大小为 12 KiB12 \text{ KiB}12 KiB、8 KiB8 \text{ KiB}8 KiB 和 9 KiB9 \text{ KiB}9 KiB 的块中。尽管你总的内存足够,但没有一个单独的空闲块足够大。这个程序无法运行! 这种内存浪费不是在任何已分配的内存块内部,而是在它们之间,这就是外部碎片。

另一种哲学,​​分页​​,为这个问题提供了一种简单粗暴的解决方案。它宣告所有的内存,无论是程序看到的逻辑空间(虚拟内存)还是物理芯片(物理内存),都将被切成小的、固定大小的块。一个逻辑块是一个​​页​​;一个物理块是一个​​帧​​。操作系统维护一组称为​​页表​​的映射,用来记录哪个虚拟页存放在哪个物理帧中。由于任何页都可以被放置在任何可用的帧中,外部碎片问题便消失了。那 29 KiB29 \text{ KiB}29 KiB 的空闲内存被切成 4 KiB4 \text{ KiB}4 KiB 的帧后,可以轻松满足新程序的需求。

但纯粹的分页是盲目的。它为一个程序创建了一个单一、巨大、线性的地址空间,失去了分段提供的逻辑结构。如果所有东西都只是一大堆无差别的页,你如何与另一个进程只共享一个“代码库”?此外,分页引入了它自己的浪费:​​内部碎片​​。如果你的程序需要 27 KiB27 \text{ KiB}27 KiB 的内存,而页大小是 4 KiB4 \text{ KiB}4 KiB,系统必须分配 ⌈27/4⌉=7\lceil 27/4 \rceil = 7⌈27/4⌉=7 个页,总共 28 KiB28 \text{ KiB}28 KiB。最后一个页有 1 KiB1 \text{ KiB}1 KiB 的未使用空间,这部分空间被分配了但却被浪费了。

这就是伟大综合的用武之地。如果我们能将分段的逻辑优雅性与分页的物理灵活性结合起来会怎么样?这正是​​分段与分页结合​​所做的。操作系统向程序员呈现一个分段的视图,但在“底层”,它通过将每个段划分为页来实现。这集合了两者的优点:既有用于编程和保护的逻辑结构,又有避免碎片的物理分配方案。

转换机制:从逻辑概念到物理现实

那么,计算机是如何将程序员的抽象地址概念——比如,“我的数据段内的第12,000个字节”——转换为内存芯片上的具体位置呢?这个神奇的过程由​​内存管理单元(MMU)​​执行,它像一出两幕剧一样展开,有两层安全警卫。

一个程序的地址最初是一个逻辑对:(段标识符,段内偏移量)。对于我们的例子,这可能是(段3,偏移量12000)。

​​第一幕:分段检查​​

MMU做的第一件事是查询​​段表​​,这是由操作系统维护的一个特殊列表。它使用段标识符 s=3s=3s=3 来找到相应的​​段描述符​​。这个描述符就像段的护照;它包含重要信息,最重要的是它的大小,即​​界限(limit)​​。

在任何其他事情发生之前,第一个守卫介入了。MMU会检查请求的偏移量是否在段的合法边界内。假设段3的界限是 150001500015000 字节。检查为 12000<1500012000 < 1500012000<15000。这是成立的,所以允许访问继续进行。但如果程序请求的是偏移量 150001500015000 呢?由于有效的偏移量是从 000 到 149991499914999,偏移量 150001500015000 就越界了。MMU会立即中止该进程,并向操作系统发出一个​​段错误(segmentation fault)​​信号。这个检查是绝对的,并且最先发生。不管该位置的物理内存是否存在,只要违反了段自身的规则,访问就会被当场拒绝。

​​第二幕:分页转换​​

通过了第一个守卫后,偏移量现在被转换为分页系统所用的形式。MMU使用系统的页大小,比如 P=4096P=4096P=4096 字节,将偏移量分解为一个页号 ppp 和一个页内偏移量 ddd。 p=⌊offset/P⌋=⌊12000/4096⌋=2p = \lfloor \text{offset} / P \rfloor = \lfloor 12000 / 4096 \rfloor = 2p=⌊offset/P⌋=⌊12000/4096⌋=2 d=offset(modP)=12000(mod4096)=3808d = \text{offset} \pmod P = 12000 \pmod{4096} = 3808d=offset(modP)=12000(mod4096)=3808 所以,偏移量12000实际上是该段第2页内的第3808个字节。

来自第一幕的段描述符还包含了另一个关键信息:一个指向该段私有​​页表​​基址的指针。MMU使用我们计算出的页号 p=2p=2p=2,在该页表中查找第2页的条目。这个​​页表项(PTE)​​是最后一步的关键。

第二个守卫现在出现了。MMU检查PTE。它是否设置了“有效”位,表示该页确实在物理内存中?如果没有,就会发生​​缺页中断(page fault)​​,操作系统必须介入从磁盘加载该页。假设该页是有效的,PTE提供了谜题的最后一块:物理​​帧号​​,比如说帧 252525。

最后,组装物理地址: Physical Address=(frame_number×page_size)+d\text{Physical Address} = (\text{frame\_number} \times \text{page\_size}) + dPhysical Address=(frame_number×page_size)+d Physical Address=(25×4096)+3808=106208\text{Physical Address} = (25 \times 4096) + 3808 = 106208Physical Address=(25×4096)+3808=106208 至此,从逻辑概念到物理现实的旅程完成了。对段3中第12,000字节的请求被安全且正确地转换为了计算机主内存中的第106,208字节。

架构师的困境:划分地址空间

在设计一个同时使用分段和分页的系统时,架构师面临一个根本性的权衡。硬件看到的虚拟地址必须被分解为几个字段:段选择符、页号和页内偏移量。对于一个32位地址,我们有 s+p+d=32s + p + d = 32s+p+d=32,其中 sss、ppp 和 ddd 分别是段选择符、页号和页内偏移量的位数。

偏移量的位数 ddd 由页大小固定(例如,一个 2122^{12}212 字节的页需要 d=12d=12d=12 位)。这就留下了一个固定的位数,比如说 s+p=20s+p = 20s+p=20,需要在段选择符和页号之间进行分配。困境就在于此。

  • 如果我们为 sss 分配更多位(例如,s=12,p=8s=12, p=8s=12,p=8),一个进程可以拥有大量的段(2122^{12}212 个),但每个段只能相对较小(只有 282^828 页)。这种架构偏爱具有许多小型、独立组件的高度模块化的程序。
  • 如果我们为 ppp 分配更多位(例如,s=8,p=12s=8, p=12s=8,p=12),一个进程只能有少数几个段(282^828 个),但每个段都可以非常巨大(2122^{12}212 页)。这更适合大型、单体的应用程序。

这不仅仅是一个技术细节;这是一个架构决策,反映了关于软件应如何构建的哲学,即在逻辑单元的数量和每个单元的最大尺寸之间进行权衡。

回报:效率、灵活性和保护

这种精巧的两级机制提供了一系列强大的好处,证明了其复杂性的合理性。

​​效率与稀疏性:​​ 分页允许段的物理帧分散在内存中的任何地方,从而消除了外部碎片。但它还做了一件更深刻的事情:它实现了​​稀疏分配​​。一个段可以被定义为具有非常大的逻辑地址范围,但操作系统只需为实际使用的那些页分配物理帧。想象一个 256 KiB256 \text{ KiB}256 KiB 的段,它由64个 4 KiB4 \text{ KiB}4 KiB 的页组成。如果一个程序只访问其中18个页的数据,那么操作系统就只分配18个物理帧。其余的46个页作为逻辑概念存在,但消耗的物理内存为零,节省了超过 70%的潜在内存占用。这对于像栈这样的数据结构来说非常高效,它们被分配了很大的潜在空间,但在任何给定时间可能只使用其中的一小部分。

​​灵活性与模块化:​​ 因为每个段都是一个独立的、分页的实体,所以它可以在不干扰虚拟地址空间其余部分的情况下增长或缩小。考虑一个由多个软件模块构建的程序。如果一个模块需要增长一页,使用“分段与分页结合”的方法很简单:只需在该模块段的页表中添加一个新条目。而在一个纯分页系统中,所有模块都紧密地打包在一个线性地址空间里,增长一个模块将迫使操作系统虚拟地“移动”所有后续模块,这是一个复杂且昂贵的操作,涉及到更新数千个页表项。这种隔离使得管理像共享库这样的独立组件变得微不足道,共享库可以作为一个新段映射到进程的地址空间中,无需任何麻烦。

​​保护与安全:​​ 双重检查机制为安全创建了一个分层堡垒。分段提供基于数据逻辑角色的粗粒度保护。分页提供逐页的细粒度控制。这种机制最强大的实现是在强制执行​​特权级别​​方面。在像 Intel IA-32 这样的架构上,CPU可以在不同的“环”中运行,从最特权的内核(环0)到最不特权的用户应用程序(环3)。

  • 一个处于环3(CPL=3CPL=3CPL=3)的用户进程,分段硬件会禁止其加载内核数据段的选择子(该段的描述符特权级别为 DPL=0DPL=0DPL=0)。
  • 即使分段检查以某种方式被绕过,分页硬件也提供了第二道防线。用户进程无法访问其页表项中标记为“仅超级用户”(U/S=0U/S=0U/S=0)的内存页。
  • 跨越这些边界的唯一方法是通过高度受控的网关,比如​​调用门(call gate)​​,它允许用户程序向内核请求服务。通过调用门时,CPU的特权级别变为 CPL=0CPL=0CPL=0,授予其访问完成工作所需的受保护段和页的权力,然后安全地将控制权返回给用户应用程序。这种分段和分页之间错综复杂的协作是现代操作系统安全的基础。

强大的代价:性能问题

然而,这个复杂的转换过程并非没有代价。在最坏的情况下,每一次内存访问都可能需要多次访问主存:一次访问段表,几次遍历多级页表,最后一次访问实际数据。如果一个系统为每个段都使用2级页表,那么一次数据访问就可能触发 1+2+1=41 + 2 + 1 = 41+2+1=4 次内存读取。这将是毁灭性的缓慢。

拯救这一切的英雄是​​转换后备缓冲区(TLB)​​。TLB是CPU上的一个小型、极快的缓存,用于存储最近使用的地址转换。在开始缓慢的查表过程之前,MMU首先检查TLB。

  • ​​TLB命中时:​​ 转换会立即找到。总成本仅仅是访问数据本身所需的一次内存访问。
  • ​​TLB未命中时:​​ MMU必须执行完整的、缓慢的查找过程。一旦找到物理地址,该转换就会被存储在TLB中,以期不久后能再次被用到。

整个系统的性能取决于TLB命中率 hhh。对于一个使用2级页表的系统,每次访问的预期内存引用次数是一个加权平均值:E=(1×h)+(4×(1−h))=4−3hE = (1 \times h) + (4 \times (1-h)) = 4 - 3hE=(1×h)+(4×(1−h))=4−3h。在典型的99%命中率(h=0.99h=0.99h=0.99)下,平均访问成本仅为 4−3(0.99)=1.034 - 3(0.99) = 1.034−3(0.99)=1.03 次内存引用——几乎是理想状态。

然而,段的逻辑结构仍然会影响性能。当一个程序从访问一个段切换到另一个段时(例如,从数据段到代码段),这种​​跨段边界访问​​可能会导致性能下降。处理器可能需要重新验证保护,更重要的是,旧段的TLB条目对于新段可能不再有用,从而导致一连串必然的TLB未命中。例如,在一个包含六次内存访问的序列中,两次跨段访问所造成的总执行时间可能比在单个段内访问的两倍还要多,这是由于由此产生的TLB未命中成本高昂。这说明了最后的权衡:虽然分段提供了优美的逻辑结构,但频繁地在这些结构之间跳转会带来切实的性能成本。

应用与跨学科联系

我们现在已经走过了分段与分页结合的复杂机制之旅,理解了逻辑地址如何找到其在内存中的物理位置。但是,了解游戏规则是一回事,亲眼目睹大师对弈则完全是另一回事。这种内存管理方案的真正魅力不在于其图表,而在于它作为一种通用而强大的工具,用于塑造数字世界。它是支撑从你口袋里的智能手机到绘制宇宙图谱的超级计算机等各种系统的无形架构。分段与分页的结合是对计算领域一个根本性挑战的深刻回答:我们如何高效、安全地组织海量信息?

让我们来探讨这个抽象机制是如何在现实世界中发挥作用,解决计算机科学各个领域中的实际问题的。

高效组织的艺术:为性能塑造内存

从本质上讲,计算机在不断地移动信息。它移动信息的速度往往是主要的瓶颈。分段与分页结合提供了一个复杂的工具包来优化这一流程,确保数据在需要时出现在需要的地方,而不会浪费宝贵的资源。

共享库:不重复劳动的力量

想想你日常使用的应用程序。一个文字处理器、一个网页浏览器、一个音乐播放器——它们中的许多都执行类似的任务,比如打开文件或绘制窗口。如果每个应用程序都包含一份自己私有的用于这些通用功能的代码副本,那将是极大的浪费。这就是共享库发挥作用的地方。

分段为实现这一优雅思想提供了完美的机制。操作系统可以将一个共享库代码的单一物理副本——它是只读的——加载到内存中。然后,对于每个需要这个库的进程,它会创建一个“代码”段,这个段仅仅指向这个共享的物理副本。每个进程都获得了库的私有虚拟视图,但在底层,它们都共享相同的字节。当然,库为特定进程需要修改的任何数据(如配置设置或临时变量)都放在一个独立的、私有的“数据”段中。这种将共享的只读代码与私有的可写数据分离的做法是效率上的一大杰作。对于使用同一个大型库的 NNN 个进程,我们节省了近 N−1N-1N−1 个完整副本的内存,这是一个巨大的节省,使得现代多任务操作系统成为可能。

驯服I/O猛兽:预取与文件映射

任何现代计算机最慢的部分通常是与其次级存储(如固态硬盘或机械硬盘)的连接。访问磁盘比访问主内存(DRAMDRAMDRAM)慢数千倍。当一个程序需要读取一个大文件时,我们面临一个两难的境地。如果我们只在数据被立即需要时才获取它(一种称为“按需分页”的原则),我们可能会引发一场由缓慢磁盘访问组成的风暴,每个页一次。

在这里,系统可以变得很聪明。通过将文件映射到一个连续的段中,操作系统知道了文件的布局。如果它看到程序正在顺序读取文件,它可以做出一个有根据的猜测:你可能很快就会需要文件的下一部分。因此,当页 kkk 发生缺页中断时,操作系统不仅获取页 kkk,它还会预取页 k+1,k+2,…k+1, k+2, \dotsk+1,k+2,…。这将许多潜在的未来磁盘读取捆绑成一个单一、更高效的操作。通过一次性获取一个包含(比如说) r+1r+1r+1 个页的块而不是只获取一个页,我们可以将慢速I/O中断的总数减少大约 r+1r+1r+1 倍,从而显著提高流式工作负载(如播放视频或处理大型数据集)的性能。

为多核巨头优化:与高性能计算的联系

现代处理器不是孤独的天才;它们是由数十个,有时是数百个并行工作的处理核心组成的委员会。在高性能计算(HPC)中,这些核心必须以惊人的速度进行通信和协调。这个领域中一个隐藏的低效来源是转换后备缓冲区(TLB)。当一个核心修改页表时(例如,允许另一台计算机通过RDMA直接向其内存写入数据),它必须通知所有其他核心,使其缓存中与该页相关的、现已过时的转换失效。这个“TLB shootdown”过程是通过向其他每个核心发送一个处理器间中断(IPI)来完成的,这个过程就像在拥挤的房间里大喊“大家停下!”。

如果所有核心共享一个巨大的、无差别的地址空间,那么每一次页表更新都会引发一场IPI的广播风暴。但是,如果我们使用分段为每个计算任务或“MPI rank”提供其自己的私有内存段呢?由于每个rank都固定在自己的核心上,当它修改自己的内存时,操作系统知道这个变化只会影响那一个核心。TLB shootdown可以是外科手术式的,只针对相关的那个核心,而不是所有核心。在一个有64个核心的系统中,这种简单的逻辑分区行为可以将shootdown IPI的数量减少64倍,将一个可扩展性噩梦变成一台精细调校的性能机器。

垃圾回收与编程语言

内存组织的原则深深地延伸到编程语言的设计中。像Java、Python和C#这样的现代语言通过使用垃圾回收器(GC)将程序员从手动内存管理的负担中解放出来。一种常见且高效的GC策略是“分代回收”。这个想法基于一个简单的观察:大多数对象生命周期很短。

分代GC将堆分为“新生代”和“老年代”。新对象在新生代中诞生,新生代空间小,通过快速的“次要回收周期”(minor cycles)频繁回收。存活过几次次要回收周期的对象会被提升到老年代,老年代空间大得多,通过较慢的“主要回收周期”(major cycles)不频繁地回收。分段是这种软件设计的完美硬件匹配。我们可以将新生代放在一个段中,老年代放在另一个段中。这使得GC可以高效地集中其精力,频繁扫描小而不稳定的新生代段,而只是偶尔支付扫描巨大而稳定的老年代段的高昂成本。这种硬件-软件协同作用是一个美丽的例子,说明了架构特性如何支持高级编程抽象。

安全堡垒:在网络空间中筑墙

虽然性能至关重要,但没有安全性和稳定性,它就一文不值。分段最初也是最持久的目的是创建边界——建立墙壁,防止一个程序的错误导致整个系统崩溃,或让攻击者取得控制权。

探测灾难:保护页

考虑调用栈,这是一个随着函数调用和返回而增长和缩小的基本数据结构。一个常见的编程错误是“栈溢出”,即一个函数——通常是递归函数——调用自身次数过多,导致栈增长超出其分配的边界并覆盖其他重要数据。这可能导致奇怪的崩溃,或者更糟的是,导致安全漏洞。

分段与分页结合提供了一种优雅且自动的防御机制。操作系统将栈分配在它自己的段中,并为其大小设置一个 limit。关键的是,它在段地址范围的最末端留下一个特殊的页,在页表中标记为“不存在”。这是一个“保护页”(guard page)。如果栈增长过大并试图触及这个页,它会立即触发一个缺页中断。错误的访问被硬件捕获,而不是静默地破坏内存,操作系统可以安全地终止有问题的程序。这个简单的技巧将一个潜在的灾难性错误变成了一个可控的失败。

提高攻击者门槛:地址空间布局随机化(ASLR)

许多复杂的网络攻击,特别是“代码重用”攻击,都依赖于攻击者知道他们希望利用的代码片段的确切内存地址。为了阻止这一点,现代操作系统采用了地址空间布局随机化(ASLR),该技术在程序每次运行时都会打乱其内存关键部分的位置。这将攻击者的工作从一项精确的工程任务变成了一场令人沮丧的猜谜游戏。

分段为随机化提供了另一个强大的调节旋钮。除了随机化一个段内部的页布局外,操作系统还可以随机化段本身的基地址。通过在虚拟地址空间的一个大区域内为代码段选择一个随机的起始点,我们引入了大量的不确定性。我们可以使用香农熵的概念来量化这种不确定性,它以比特为单位衡量“意外程度”。每增加一比特的熵,攻击者的搜索空间就会翻倍。将段基址随机化与其他技术相结合,可以增加许多比特的熵,使攻击者成功实施漏洞利用的难度呈指数级增加。

内存加密:一个现代前沿

在最高风险的安全场景中,我们甚至必须防范获得了对计算机内存芯片物理访问权限的攻击者。解决方案是透明内存加密,即数据在写入DRAM时自动加密,在读回处理器时自动解密。但是我们如何管理密钥呢?

同样,分段提供了一个自然的框架。我们可以为每个段关联一个唯一的加密密钥。当CPU在“安全”段内执行代码时,一个硬件加密引擎会使用该段的密钥来动态解密数据。这就提出了一些引人入胜的设计问题。密钥应该直接存储在段描述符内部以实现最快的访问速度吗?这能提高性能,但意味着如果攻击者能读取描述符表,他们就能得到密钥。或者,描述符是否应该只包含一个指向存储在独立的、高度受保护的密钥表中的密钥的指针?这样做更安全,但在段切换期间会增加额外的内存访问和延迟。这种性能与安全之间的权衡是系统架构师每天都要应对的核心挑战。

统一原则:抽象与虚拟世界

分段与分页结合的威力在于它能够将高级软件抽象映射到底层硬件上,为计算创造灵活且隔离的世界。

段作为模块和微服务

思考一下今天的大型软件系统是如何构建的——通常是作为一组通过明确定义的接口进行通信的独立组件、模块或“微服务”。分段是这种软件架构的天然硬件对应物。每个组件可以存在于其自己的段中,拥有自己的保护属性。这强制实现了强隔离;一个微服务中的错误被限制在内部,不容易导致另一个微服务崩溃。然而,这种隔离是有代价的。每当执行从一个组件跨越到另一个组件——即段切换——硬件可能需要执行额外的开销任务,比如刷新TLB。如果这些切换过于频繁,维持隔离的性能成本可能会变得非常显著,这揭示了系统设计者必须平衡的一个基本权衡。

虚拟化:世界中的世界

或许最令人费解的应用是在虚拟化中,我们将一个完整的操作系统(“客户机”)作为宿主机操作系统上的一个普通应用程序来运行。客户机操作系统认为它控制着机器,管理着自己的段和页表。但这完全是由虚拟机管理程序(hypervisor)维持的一个精心设计的幻觉。当客户机试图访问内存时,它的地址会经过一个多层转换。首先,硬件执行客户机的分段检查。如果通过,它会遍历客户机的页表,产生一个“客户机物理地址”。但旅程并未结束。这个客户机物理地址随后被送入另一组页表,即宿主机的扩展页表(EPT),最终产生真正的主机物理地址。这种转换和保护的分层允许在单个物理机上运行多个隔离的客户机操作系统。客户机操作系统中的段界限违例在虚拟机管理程序需要干预之前就被硬件捕获,这一事实证明了该设计的鲁棒性和层次性。

管理复杂性:抽象的成本

最后,让我们回到一个微妙但根本性的权衡。如果段对于逻辑组织如此出色,为什么不把每个数组或数据结构都放在它自己的段里呢?答案在于分页的开销。每分配一个页都需要一个页表项(PTE)。当你将许多小数组隔离到它们各自的段中时,每个数组很可能都有一个部分填充的最后一页。这些最后一页中的未使用空间是一种“内部碎片”。通过将所有这些数组合并到一个大的段中,这些零散空间可以被整合,从而可能减少所需的总页数,进而减少PTE的数量。这节省了内存,但牺牲了清晰的逻辑分离。这又是另一个经典的工程权衡:逻辑清晰度与资源效率。

总之,分段与分页结合不是一个单一的想法,而是一个强大的伙伴关系。分段提供了逻辑结构——我们数字书籍的章节和段落。分页提供了物理灵活性——能够将这些段落以任何顺序排列到物理页面上的印刷机。它们共同为我们提供了构建高效、安全、可扩展且极其复杂的计算机系统的基本工具。