3. 内存管理(Memory Management)

计算机有几兆字节的非常快速,昂贵,且易变的 cache memory,几千兆字节的中速,中等价格,易变的 main memory,以及几千兆字节缓慢,便宜,非易变的磁盘/固态硬盘存储,不涉及可移除的存储,例如 DVDs 和 USB 存储器。这被称为 memory hierarchy

操作系统管理内存层级的部分被称为 memory manager,它追踪处于使用中的内存,在需要时向进程分配内存,不需要时解分配内存。

3.1 无内存抽象

早期的操作系统没有内存抽象,程序直接使用物理内存,这样多个进程无法同时运行,因为它们可能访问同一物理地址,造成冲突。
在这里插入图片描述
无内存抽象具有以下缺点:

  1. 用户程序可以访问任意物理地址,包括操作系统所在的地址,可能会有意或无意的破坏操作系统。
  2. 难以支持多个进程同时运行,它们可能访问同一物理地址。

3.2 内存抽象:地址空间

3.2.1 地址空间的概念

Address space 是进程用来进行内存寻址的地址集。每个进程都有自己的地址空间,共享内存除外。

Base and Limit Register

该方法使用一个 dynamic relocation 及其简单的版本,即以一种简单的方式将进程的地址空间映射到物理内存中的不同部分。典型的方法是 CPU 中持有两个特殊的硬件寄存器,通常称为 baselimit 寄存器。当这些寄存器被使用时,程序被加载到一段具有足够空间的连续的内存。当进程运行时,base 寄存器使用程序在内存中的起始点的物理地址进行加载,limit 寄存器使用程序的长度进行加载。

每一次进程引用内存,无论是抓取一个指令读写数据字,CPU 硬件在将该地址访问内存总线前自动为进程生成的地址加上 base 的值。类似的,它也检查提供的地址是否等于或大于 limit 寄存器中的值,这种情况下会产生一个错误,访问会被中断。

使用 base 和 limit 寄存器进行重定位的缺点是对于每个内存引用都需要执行一次加法和一次比较。比较能很快完成,但是加法因为 carry-propagation 时间会比较慢,除非使用额外的电路。

Swapping

所有进程所需的 RAM 的总数量通常不能被内存所满足。解决这一问题最简单的方案是 swapping,包括完整地引入每个进程,运行一段时间,将它放回硬盘中。空闲的进程大多存储在硬盘上,所有当它们没有运行时不会占据任何内存 (它们之中有一些被周期性的唤醒,来执行它们的工作)。另一个策略被称为 virtual memory,甚至允许程序只有部分在主存中,仍然能运行。

交换系统的操作如图 3-4 所示。开始时,只有进程 A 在内存中。然后进程 B 和 C 被创建或从硬盘被交换到内存中。在图 3-4(d) 中,A 被交换出硬盘。然后 D 进入 B 被交换出。最终 A 再次进入。因为 A 现在位于不同的位置,包含在其中的地址必须被重定位,要么是在被换入时软件进行重定位,要么 (大概率) 是在程序执行过程中由硬件进行重定位。例如,使用 base 和 limit 寄存器。
在这里插入图片描述
当交换在内存中创建了多个空洞,可能通过尽可能地将进程向下移动将这些空洞结合为一个大地空洞,这一技术被称为 memory compaction。该操作通常不会被执行,因为会耗费大量的 CPU 时间。例如,16-GB 的机器能在 8 nsec 内拷贝 8 个字节,压紧内存将耗费 16 秒。

值得考虑的一点是进程在被创建或被换入时应当被分配多大的内存空间。对于在运行期依然保持使用固定内存大小的进程,分配其固定大小的内存就足够了。对于在运行期可能出现内存增长的进程,比如在堆上分配内存,对其内存的分配也必须是动态的。当进程邻近一个空洞时,可以将空洞的内存分配给该进程,但是当进程邻近另一个进程,则需要将邻近的进程交换到硬盘中,或者将进程移动到满足其增长后的内存大小的空洞中。如果这两者都不能满足,进程将被挂起直到某些空间被释放 (或者可能被杀死)。

如果大部分的进程在运行时其内存都会增长,在创建或交换时多分配一些额外的内存将会降低在内存增长时移动进程或交换其他进程的开销。然而,在交换进程到硬盘时,只会交进程实际占据的内存,交换额外分配的内存是对硬盘资源的浪费,在被换入时重新分配额外空间即可。图 3-5(a) 展示了增长的内存已经被分配了的内存配置。
在这里插入图片描述
如果进程有两个增长的段 – 例如,数据段作为堆以动态分配和释放某些变量,以及用于普通局部变量和返回地址栈段。图 3-5(b) 是一种可选的安排,在其被分配的内存的顶部有一个栈,它向下增长,程序 .text 段之上有一个数据段向上增长。两个段之间的内存能被用于两者中的任何一个。如果它被耗尽,进程将被移入一个有足够空间的空洞,或换出其他进程的内存以创建足够大的空洞,或者被 killed。

3.2.3 管理未使用的内存

当内存被动态分配时,操作系统必须管理它。一般来说,有两种追踪内存使用的方法:位图未使用内存链表

使用位图管理内存

使用位图,内存能以分配单元被分割,小到几个字,大到几千字节。位图中每个分配单元都对应一位,当分配单位为被使用时其值为 0,被使用时其值为 1。图 3-6 展现内存的一部分以及对应的位图。

分配单元的大小是一个重要的设计问题。分配单元越小,位图越大。然而,即使分配单元为 4 字节,32 位的内存在位图中仅需要 1 位。32n 位的内存将使用 n 个位图中的位,所以位图只会占据 1/32 的内存。如果分配单位越大,位图将越小,但是如果进程的大小不是分配单元的整数倍,进程的最后一个单元中的内存可能会被浪费。

位图提供了一种简单的方法以追踪固定大小内存中按分配单元分配的内存分配情况,位图的大小依赖内存的大小以及分配单元的大小。位图的主要问题时,当准备位 n 个分配单元的进程分配内存时,内存管理器必须搜索位图,寻找连续的 n 个值为 0 的位,这一搜索过程是一个缓慢的操作。

使用链表管理内存

管理内存的另一方式是使用元素为允许的或未使用的内存段的链表,链表中的内存段要么是一个进程,要么是两个进程间的空洞。图 3-6(a) 中的内存如果以链表表示则如图 3-6© 所示,其中 H 表示空洞P 表示进程,后跟起始物理地址,以及内存段的长度,最后是指向下一项的指针

本例中,内存段列表按地址排序,这有助于内存段链表的更新。当进程终止或被交换到硬盘上时,会出现 4 种情况,如图 3-7 所示。
在这里插入图片描述
(a) 将 P 替换为 H;
(b) 将原分配给进程 X 的内存段与相邻的空洞合并,更新地址和长度,链表移除 X 对应的内存段项;
(c) 同上;
(d) 同上,但链表移除 X 对应的内存段项以及 X 之后的空洞内存段项。

当进程和空洞按地址顺序链接在链表上,内存管理器可以使用一些算法来搜索合适的空洞,将其分配给新创建的进程会从硬盘中被换入的进程。最简单的算法是 first fit。内存管理器沿着内存段的链表扫描,找到第一个空间足够大的空洞,将其空间一分为二,满足进程内存大小的哪部分分配给进程,另一部分则是新的空洞,除非空洞的大小正好等于进程所需的内存大小。first fit 算法是一个快速算法,因为它执行尽可能少的搜索。

first fit 的一个最小的变体是 next fit。它与 first fit 的工作方式相同,除了它记录上次分配的空洞的位置,下一个的搜索将从上次分配的空洞处开始。

另一个为人熟知且广泛使用的算法是 best fit。Best fit 搜索整个列表,从开始到结束,选择最小的满足要求的空洞。不是对一个大的空洞进行分割,best fit 尝试寻找最接近实际需要的大小的空洞。Best fit 慢于 first fit,因为它需要遍历整个内存段链表。Best fit 还会造成更多的内存浪费,因为会产生很多小的空洞,这些小的空洞很难分配给其他程序。

为了解决将几乎完全匹配的空洞分割为进程使用的内存和另一个小的空洞,worst fit 算法被提出来,该算法总是选择最大的可用空洞,产生的新的空洞将足够大能够被分配给其他进程。模拟显示出 worst fit 还不是一个合适的内存分配算法。

上述 4 种算法都能通过分别维护进程和空洞的内存段列表进行加速。使用该方法,它们将致力于检查空洞,而非包括进程。对应的代价是额外的复杂度和解分配内存变慢,因为未使用的内存段必须从进程列表种移除,并插入到空洞列表中。

当空洞保存在一个单独的列表中时,可以采用一个小的优化。不必使用一组单独的数据结构维护空洞列表,信息能被直接存储在空洞中。每个空洞的第一个字将是空洞的大小,第二个字指向下一个条目。空洞所在的地址和 H 标志位不再需要了。

还有另一种分配算法,称为 quick fit,它位一些更常见的请求大小维护一个列表。例如,它可能具有一个 n 个条目的表,第一个条目是指向 4 KB 空洞链表的头的指针,第二个条目是指向 8 KB 空洞链表的指针,第三个条目指向 12 KB 的空洞。21 KB 的空洞能被放入 20 KB 空洞的列表中,或者放入一个奇数大小空洞的特殊列表中。

使用 quick fit,找到所需大小的空洞将很快,但是它也有所有使用空洞大小排序的方案共有的缺陷,当进程终止或被换出时,找到它的邻居以确定是否需要合并变得十分昂贵。如果没有执行合并,内存将被快速分割为大量的小空洞,不能满足任何进程。

3.3 虚拟内存

虽然 base 和 limit 寄存器能被用来创建地址空间的抽象,但是还有另一个问题需要解决:软件的所占据的内存随着软件的发展在快速增长,出现了程序运行时需要占据的内存无法被满足的程序的需求,此外系统还需要支持多个进程同时运行,它们中的每一个所需的内存都能被满足,但是所有进程所需的内存之后超过了现有内存的大小。交换进入硬盘并不是一个合适的选项,因为典型的 SATA 磁盘的峰值传输率是几百 MB/sec,这意味着换出或换入 1-GB 的程序将耗费数秒。

Virtual memory 被提出来用于解决软件所需内存的大小大于现有内存这一问题,其基本概念是每个进程都有自己的地址空间,地址空间被划分成块,称为 pages。每一页都是连续的地址范围。这些页被映射到物理内存中,但是并不是所有的页在程序运行时都必须存在于内存中。当进程访问其地址空间中的某一部分时,如果该部分存在于物理内存中,硬件则会执行必要的映射以获取正确的物理内存地址;如果该部分不在物理内存中,则会提醒操作系统加载该部分到物理内存中,然后重新执行失败的指令。

某种意义上说,虚拟内存时 base-and-limit-register 概念的泛化。使用虚拟内存,可以将进程的地址空间以相当小的单元映射到物理内存中。

虚拟内存在多程序系统中依然工作的很好,内存中能同时存在多个程序的页。当进程等待它自己的页被读入到物理内存时,CPU 可以被其他进程使用

3.3.1 分页

大部分的虚拟内存使用一种称为 paging 的技术。在任何计算机上,程序应用一组内存地址。当程序执行某个指令例如:

MOV REG, 1000

它拷贝内存地址为 1000 的数据到 REG 中 (或者反过来,取决于操作系统)。地址可能使用 indexing,base register,segment register,和其他方式生成。
在这里插入图片描述
程序生成的地址被称为 virtual address,它们组合在一起形成了 virtual address space。在没有虚拟内存的计算机上,虚拟地址被直接传输到内存总线上,将其作为物理内存地址进行读写。在使用虚拟内存的计算机上,虚拟地址通过内存管理单元 (MMU) 映射到对应的物理内存地址上,将转换后的地址发送到内存总线上,作为访问的物理内存地址,如图 3-8 所示。
在这里插入图片描述
图 3-9 展示了虚拟地址如何被映射为物理地址的一个例子。如图 3-9 所示,虚拟地址为 16 位,虚拟地址空间范围从 0 到 64K-1,支持 64 KB 的程序运行,但是实际的物理地址空间范围为 0 到 32 KB,为虚拟地址空间的一半,这意味着 64 KB 的程序不能被完全加载到物理内存中,仅能加载部分进程的虚拟内存到物理内存中,其他的虚拟内存数据必须存储在硬盘上,这就要求程序的完整映像必须存储在硬盘中,以便随时按需加载入物理内存。

虚拟内存空间的组成单元被称为 pages物理内存空间的组成单元被称为 page frames。页和页帧的大小通常是相同的,本例中使用的大小是 4KB。在实际的系统中,页和页帧的大小支持从 512 字节到 1 GB。RAM 与硬盘通常以页帧作为单位进行交换。许多处理器支持多个页的大小,可以按照操作系统的需求选取甚至混合使用。比如 x86-64 架构支持 4-KB,2-MB 和 1-GB 的页,所以我们可以对用户应用程序使用 4-KB 的页,对内核应用程序使用 1-GB 的页。

我们来看看虚拟内存地址如何被映射到物理内存地址,假设有下述指令:

MOV REG, 0

其中,0 表示虚拟内存地址为 0,它位于虚拟内存地址范围为 0 - 4095 的页中,该页映射物理页帧 2,所以其物理内存地址为 (8192 + 0),该转换后的地址被发往内存总线作为访问的物理内存地址。内存对 MMU 的存在一无所知,它唯一知道的就是传入的物理内存地址。

MMU 将虚拟内存地址映射为物理内存地址,并没有解决软件所需内存大于实际内存的问题,MMU 仅能将 16 个虚拟内存页中的 8 个映射到 8 个物理内存页帧上,其他 8 个则处于未被映射的状态,在实际的硬件中,我们使用 Present/absent bit 追踪已映射和未被映射的虚拟内存页。当 MMU 发现进程尝试访问未被映射的页,会致使 CPU 将此情况上报操作系统,这被称为 page fault。操作系统随后选择一个不常使用的页帧,将其内容存入硬盘,然后将发送页错误的指令所引用的页从硬盘中读入刚刚释放的页帧中,并修改映射关系,然后重新执行失败的指令。

那么 MMU 如何将虚拟内存地址映射为物理内存地址呢?16 位的虚拟内存地址被划分为两部分,高 4 位表示 16 个页其中之一的页编号,低 12 位表示在该页中的偏移量,至多为 4095,即页上的最后一个地址。页编号作为 page table 中的索引使用,依据该索引可以得到页所对应的页帧的编号。该页帧的编号作为输出寄存器的高 3 位,剩下的低 12 直接使用上述的页的偏移量,它们一起组成 15 位的物理内存地址,传入内存总线中。

3.3.2 页表

在简单的实现,虚拟内存地址如上述被划分为 2 部分:高位部分表示虚拟页编号低位部分表示虚拟内存地址在该页中的偏移。虚拟页编号用于在页表中查找对应的虚拟页条目。从虚拟页条目中,能得到对应的页帧编号,将页帧编号作为高位与页偏移进行结合,替换原来的页编号,就得到了实际需要访问的物理内存地址。因此,页表的作用就是映射虚拟页到对应的页帧。
在这里插入图片描述

页表条目的结构

页表条目的大小和布局因系统而言,但其包含的内容大致相同,典型的页表条目如图 3-11 所示:
在这里插入图片描述
其中最重要的字段是 Page frame number

与之相邻的是 Present/absent 位,如果该位被置为 1,则该条目合法并被使用,如果为 0,则表示该条目所对应的虚拟页当前没有加载到物理内存中,对其进行访问会导致页错误。

Protection 位表示对该页允许那种类型的访问。最简单的形式下该字段只包含 1 位,0 表示允许读写,1 表示只读。成熟的设计则包含 3 位,读写和执行各占一位。

ModifiedReferenced 位追踪页的使用情况。当页被写入时,硬件自动设置 Modified 位。当操作系统决定回收某个页时,如果该位被设置,则表示该页有修改,是 “脏” 的,需要被写回硬盘;如果没有设置该位,则表示该页没有修改,是 “干净” 的,不需要写回硬盘,直接丢弃即可。该位有时被称为 dirty bit,因为它反映了页的状态。

Referenced 位在页被引用的时候被设置,无论是读或者写。该值被用来帮助操作系统在发生页错误选择被逐出的页。未被使用的页相较被使用的页是更好的候选人,该位在几种页替换算法中扮演者重要的角色。

最后一位允许该页的缓存被禁用。该特性对于映射到设备寄存器而非内存中的页十分重要。如果操作系统正在一个紧密的循环中等待一些 I/O 设备对器刚刚给出的命令进行响应,那么硬件持续抓取来自设备的字,而不是使用旧的缓存拷贝就十分必要了。使用该位,缓存则会被关闭。有单独的 I/O 空间不使用内存映射的 I/O 的机器不需要该位。

注意,用于保存不在内存中的页的硬盘地址不是页表的一部分。理由很简单,页表仅保存硬件将虚拟内存地址翻译位物理内存地址所需的信息。操作系统在处理页错误时所需的信息被保存在操作系统的软件表中。硬件并不需要它。

在进一步讨论实现相关的问题前,值得再次指出虚拟内存本质上创建了一个抽象,一个地址空间,是物理地址的抽象,正如进程是物理处理器的抽象一样。虚拟内存能通过将虚拟地址划分位页,并将其映射到物理内存的某些页帧中,或者让其暂时未被映射。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值