内存管理
第三章_内存管理
3.1 内存管理的概念
3.1.1 内存的基础知识
3.1.1.1 进程运行的基本原理
本小节思维导图如下图所示:
首先明晰,一个程序 从被写到被执行 会经历以下步骤,在计算机组成原理中也有类似的说明:

3.1.1.2 程序的链接
链接程序的作用是将编译生成的目标模块及其所需的库函数合并,形成一个完成的装入模块。
链接的三种方式:

3.1.1.3 程序的装入
本小节思维导图如图所示:
装入的三种方式:
- 绝对装入: 主要适用于 单道程序环境,编译器 在程序进行 编译/链接 的时候就已经确定该程序在内存中的地址。
- 可重定位装入(静态重定位): 可适用于 多道程序环境,简单来说,就是系统根据内存情况提前选定一个能够 装下全部程序的内存区域,一次性将程序中涉及到的所有逻辑地址转换为真实物理地址。程序在内存中的地址由 装入程序 在 装入时 确定。一旦装入,程序运行期间不可再改变,如果要改变位置,就必须重新进行重定位。
- 动态运行时装入: 可适用于 多道程序环境,装入程序将模块载入内存后,不立即转换地址,而是设置一个 重定位寄存器,以方便多个模块在内存中移动(动态运行时装入并不意味着每次都必须把整个程序一次性装入内存)。程序涉及的真实物理地址在 运行时 计算确定(寄存器 + 逻辑地址)。

3.1.1.4 进程的内存映像
进程的内存映像如图所示:

| 内存区域 | 主要存放内容 | 典型例子 |
|---|---|---|
| 代码段(Text) | 程序的机器指令 | main()、printf() 调用相关代码、其他函数代码 |
| 只读数据段(ROData) | 只读常量 | 字符串常量 "hello"、const 数据 |
| 静态/全局数据区 | 已初始化的全局变量、静态变量 | int a = 10; static int b = 20; |
| BSS 段 | 未初始化或初始化为 0 的全局/静态变量 | int a; static int b; |
| 堆(Heap) | 动态分配的数据 | malloc()、new |
| 栈(Stack) | 函数调用产生的栈帧、局部变量、参数、返回地址等 | main() 中的 int x; |
注意:用户态 / 内核态 是 CPU 的运行状态,是为了解决权限控制问题;而 用户空间 / 内核空间(用户区 / 内核区) 是内存空间的划分,是为了解决地址隔离问题
3.1.1.5 覆盖与交换*
本小节思维导图如图所示:
覆盖技术 如图所示:
注意:覆盖技术的单位是 程序模块。
交换技术 如图所示:
注意:交换技术的单位是 整个进程。

覆盖技术:主要用于扩充 单个程序(进程)的逻辑地址空间。 交换技术: 主要用于提高 内存利用率和并发度,不是为了扩充单个进程的逻辑地址空间。
3.1.1.6 内存保护
需要特别说明一下 存储保护的两种方法:
-
在 CPU 中设置一对 上、下限寄存器,分别存放用户进程在内存中的下限和上限地址,每当 CPU 访问一个地址时,硬件自动将其与这两个寄存器的值进行比较,以判断是否越界。
-
采用 重定位寄存器(也称基地址寄存器)和 界地址寄存器(也称限长寄存器)实现越界检查。重定位寄存器存放进程在内存中的 起始物理地址,界地址寄存器存放进程 地址空间的长度(逻辑地址最大值)。内存管理部件首先将逻辑地址与界地址寄存器的值进行比较,若未越界,则将逻辑地址加上重定位寄存器的值,得到物理地址,并据此访存。
如下图所示:

3.2 内存空间的分配与回收
首先明晰:内存空间的分配指的是怎么将内存块分配给进程,而程序的装入指的是,程序装入后的实际物理地址怎么确定。这是两个相像而又不同的概念。
3.2.1 连续分配管理方式(基于顺序搜索)
前面 “内存管理的基本概念” 小节已经说过,内存管理主要就是四个方面,而本小节 “连续分配管理方式” 属于 内存空间的分配与回收。
本小节思维导图如图所示:

3.2.1.1 单一连续分配
单一连续分配如图所示:
注意这种方式 没有外部碎片。
3.2.1.2 固定分区分配
固定分区分配:
提前将内存空间划分为若干大小相同或者不同的区域,进程到来时,若进程大于所有分区大小则无法装入,若小于分区大小,装入后即会占用整个分区,未被利用的内存空间即 内部碎片。无法支持内存共享。
如图所示:

3.2.1.3 动态分区分配
动态分区分配就是在进程进入内存时,根据进程实际需要的内存大小,动态地从空闲内存中划出一块连续空间给它。
与固定分区分配的区别 在于:固定分区分配的分区是开始就固定好的,而动态分区分配的分区是进程装入时动态确定的。
了解什么是动态分区分配,只需要回答下面三个问题即可:
- 系统用什么样的数据结构记录内存的使用情况?

- 当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?

- 如何进行分区的分配与回收操作?

总结一下:

3.2.1.4 动态分区分配算法
首次适应算法:
注意:空闲分区 按 地址递增次序排列。
邻近适应算法:
注意:空闲分区 按 地址递增次序排列。
最佳适应算法:
即每次分配找到最佳(最小)的可分配分区。
注意:由于 空闲分区 按照 容量递增次序排列,每次分配出去一个之后,分区表可能还要 重新排个序。
最坏适应算法:
即每次分配找到最坏(最大)的可分配分区。
注意:由于 空闲分区 按照 容量递减次序排列,每次分配出去一个之后,分区表可能还要 重新排个序。
四种算法对比 如下图所示:

3.2.2 连续分配管理方式(基于索引搜索的分配算法)
当系统规模较大时,空闲分区链可能很长,采用顺序搜索算法效率较低。为此,大中型系统常采用基于索引的分配算法,索引分配算法的思想是:根据空闲分区的大小进行分类,对 每一类大小相同的空闲分区建立独立的空闲分区链,并通过一张索引表统一管理这些链。分配时,依据请求大小在索引表中定位对应链表,获取其头指针,从而快速取得一个空闲分区。
3.2.2.1 快速适应算法
快速适应(Quick Fit)算法,根据系统中进程常用的空间大小,预先将空闲分区划分为若干固定尺寸的类别。分配过程分为两步:
- 根据作业长度,在索引表中找到能容纳它的最小类别链表;
- 从该链表头部取下第一个空闲分区进行分配。
优点是查找效率高、不会产生内存碎片;缺点是回收时难以有效合并分区,算法比较复杂,开销较大。
3.2.2.2 伙伴系统
规定所有分区大小均为 $2^k$($k$ 为正整数)。当需要为进程分配大小为 $n$ 的内存时(满足 $2^{k-1}<n\le2^k$),首先在大小为 $2^k$ 的空闲分区链中查找。若存在,则直接分配;否则,依次在大小为 $2^{k+1},2^{k+2},\dots$ 的链中查找,直至找到一个可用分区。随后,将该分区不断二分,直至得到所需大小的块,其余部分按大小加入对应的空闲链。
在伙伴系统中,每个大小为 $2^k$ 的分区都有一个唯一的伙伴分区:与它大小相同、地址相邻(首尾相接)的另一个分区。回收时,若某空闲分区与其伙伴也为空闲,则合并为一个大小为 $2^{k+1}$ 的分区,并继续向上尝试合并,直至无法合并为止。
3.2.2.3 哈希算法
哈希算法。以空闲分区大小为关键字,建立哈希函数,构建一张哈希表,每个表项记录对应空闲分区链的头指针。分配时,根据所需分区大小,通过哈希函数计算出其在哈希表中的位置,从而快速获取相应的空闲分区链。
3.2.3 非连续分配管理方式
首先明晰非连续分配管理方式和连续分配管理方式的区别: 非连续与连续其实指的都是程序在内存中连不连续,在连续分配管理方式中,假设程序大小为 1GB ,即使内存中存在大于 1GB 的空间,但不是连续的空间,该程序便无法装入运行,这就是外部碎片导致的问题。而非连续分配管理方式便克服了这一限制。
3.2.3.1(1) 基本分页存储管理
本小节思维导图如下图所示:
首先需要知道 什么是分页存储:
对于 页表 这个重要概念,用几个问题来熟悉:
注意:是系统会为 每个进程都建立一张页表(这只是简化的说法,实际中是每个进程建立一套页表),每个 页表项 记录对应页面所驻留的 物理页框号 以及相关信息(有效位、访问位等)。
查询页表的过程 如下图所示:
注意:在慢表中由于页表项按顺序存放,因此 慢表中的页号是隐含的,还可以通过页号直接定位慢表中的页表项,不必挨个比对。

(补充)若页面大小是 2 的整数次幂,会有一个非常良好的特性:
这样逻辑地址的前多少位表示的就全部是页面大小,系统能够使用 “位运算快速拆地址 + 硬件直接分段”,而不用除法。所以页面大小通常都取 2 的整数次幂。

3.2.3.1(2) 基本地址变化机构
本小节思维导图如图所示:
基本地址变化机构过程 如图所示:
注意:基本地址变换机构由 硬件 实现。
通过一个题目来熟悉考研会怎么考察:
对 页表项的进一步探讨 如图所示:

3.2.3.1(3) 具有快表的地址变换机构
快表 是一个 CPU 里的高速缓冲存储器,是一个单独的 硬件,不是一个高速缓存器中的一部分!
支持快表的地址变换机构,将逻辑地址转变成物理地址的 流程图 如图所示:
注意:TLB 无论采用单级页表还是多级页表存储的始终是 完整虚拟页号 → 页框号,只要 TLB 命中就能够直接得到对应的物理块号。
通过甘特图来理解考研会如何进行考察:
与 基本地址变换机构对比 如下:

3.2.3.1(4) 两级页表
本小节思维导图如下图所示:
首先我们来看 单页表存在的问题:
于是我们自然而然的想到了用 二级页表 来解决这个问题:
采用二级页表之后,CPU 给出 逻辑地址结构 也会发生变化,一个页号”拆成了“一级页号 + 二级页号”,但是就像是多模块存储器的地址一样。CPU 给出的同样的页号在 纯一级页表 和 二级页表模式 中表示的页号是相同的。
注意:将一级页表拆成二级页表并不是随意的拆,通常一个页表的大小对应一个物理块的大小,即一个页表的页表项个数通常是 物理块大小/页表项长度。
关于 二级页表如何寻址 如下所示:
注意基址寄存器中存放的是一级页表的物理地址,页号都是隐含的。
继续拓展为 多级存储,需要注意以下细节:

3.2.3.2 基本分段式存储管理
本小节思维导图如下图所示:
基本分段式存储管理概念如下图所示:
分页 是操作系统将逻辑地址空间划分为固定大小的页.物理内存空间划分为同样大小的页框,是自动划分,对程序员来说是 透明 的。
而 分段 是对进程代码的划分,分段是需要程序员编程的时候自己分段的,因此对程序员来说的 不透明 的
与分页存储类似,分段存储的地址寻址流程 如下所示:
分段分页管理对比 如下:

3.2.3.3 段页式管理方式
本小节思维导图如下:
我们首先来看下段式管理方式和页式管理方式各自的 优缺点:
不难想到,段式管理方式和页式管理方式是能够结合起来使用的。于是 段页式管理方式 应运而生,如下图所示:
系统为每个进程建立一张段表,每个段建立一张页表。
关于段页式管理方式的 寻址流程 如下所示:

3.3 内存空间的扩充
3.3.1 虚拟内存技术
内存管理方式的发展过程:
传统存储管理
├── 连续分配
│ ├── 单一连续
│ ├── 固定分区
│ └── 动态分区
│
└── 离散分配
├── 基本分页
├── 基本分段
└── 基本段页式
(特点:进程运行前必须全部装入内存)
↓ 为解决内存不足
覆盖技术
交换技术
↓ 进一步发展
虚拟内存
├── 请求分页
├── 请求分段
└── 请求段页式
(特点:运行时不必全部装入内存)
本小节 思维导图 如图所示:
传统的内存管理方式具有如下的特征和缺点:
而虚拟存储是基于局部性原理进行实现,具有以下特征:
注意:虚拟地址空间的大小 由底层的 虚拟内存管理机制 和 操作系统 决定,通常在不同的操作系统中有所不同。而 虚拟存储空间实际可用容量的最大值 是由 内存和硬盘大小 决定的。
具体怎么 实现虚拟存储 如下图所示:

3.3.2 请求分页的管理方式
请求分页存储管理与基本分页存储管理的主要区别:
- 在程序执行过程中,当所 访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。
- 若内存空间不够,由操作系统负责 将内存中暂时用不到的信息换出到外存。
本小节思维导图如下图所示:
与基本分页不同,请求分页的管理方式 对页表会做一些结构上的改动:
若是发生缺页,需要 缺页中断机构 来进行处理:
注意:缺页中断属于内中断(异常)中的 故障中断。
而关于 请求分页的地址变换,与基本分页的地址变换也是大同小异:
书上也给出了地址变换的具体过程,这个 过程图要求牢记!

3.3.3 页面置换算法
五种页面置换算法的总结与对比如下图所示:

3.3.3.1 最佳置换算法(OPT)

3.3.3.2 先进先出置换算法(FIFO)
注意:Belady 异常和抖动不是一回事,所有页面调度策略都有可能会引起抖动,但是 Belady 异常只有先进先出置换算法(FIFO)出现。
3.3.3.3 最近最久未使用算法(LRU)

3.3.3.4 时钟置换算法(Clock)

3.3.3.5 改进型时钟置换算法

3.3.4 页面分配
本小节思维导图如图所示:

3.3.4.1 页面分配的基本概念
首先了解一些页面分配策略的基本概念:
抖动现象:
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为 抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)。
为进程分配的物理块 太少,会使进程发生抖动现象。为进程分配的物理块 太多,又会降低系统整体的并发度,降低某些资源的利用率。
为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程 “工作集” 的概念。
3.3.4.2 内存分配策略
在请求分页系统中,可采取两种内存分配策略,即 固定和可变分配 策略。在进行置换时,也可采取两种策略,即 全局和局部置换。于是可以组合出一下 三种适用的策略:
分配分配的肯定是物理块,至于虚拟地址空间有多少页,由进程大小决定。
其中 可变分配全局置换 和 可变分配局部置换 区别如下:
注意:可变分配局部置换策略中进程的内存块增加是系统分配的 空闲内存块,而可变分配全局置换中进程的内存块增加是系统分配的可能 从别的进程“抢来”的内存块 或者 空闲的内存块。

固定分配策略: 若是固定分配,系统在进程分配页框时有三种不同的固定分配策略(记住名字即可):
- 平均分配算法,将系统中所有可供分配的页框平均分配给各个进程。
- 按比例分配算法,根据进程的逻辑地址空间大小按比例分配页框。
- 优先权分配算法,为重要或紧迫的进程分配更多页框。通常的做法是将所有可分配页框分为两部分:一部分按比例分配给各进程,另一部分则依据进程的优先权动态分配。
3.3.4.3 何时调入页面

3.3.4.4 何处调入页面
请求分页系统中的外存分为两部分:存放文件的 文件区,采用 离散分配 的方式。用于存放换出页面的 对换区,采用 离散分配 的方式。

3.3.4.5 如何调入页面
当进程访问的页面不在内存中时(页表项的存在位为 0),CPU 会触发缺页中断。中断响应后,系统转入缺页中断处理程序。该程序首先通过页表项获取该页在外存中的地址,然后判断内存是否已满:
- 若内存未满: 则分配一个空闲页框,发起磁盘 I/O 将所缺页面调入,并更新页表项:填写物理块号,置存在位为 1;
- 若内存已满: 则先按某种置换算法选出一页准备换出。若该页的修改位为 0,则直接丢弃;若修改位为 1,则需先将其写回对换区,再释放该页框。随后,将所缺页面调入该页框,并更新页表项,置存在位为 1。调入完成后,进程即可通过更新后的页表生成正确的物理地址。
整个页面调入过程对用户完全透明,由操作系统自动完成。
3.3.5 页框回收
本小节思维导图如图所示:
普通的页框回收 如图所示:
为了使页框回收更加有效率,出现了 缓冲式回收 的方法:

3.3.6 内存映射文件
内存映射文件是操作系统向应用程序提供的一种系统调用机制,它在 磁盘文件与进程的虚拟地址空间之间建立直接的映射关系,与虚拟内存机制紧密相关。
传统的文件访问方式 如图所示:
采用 内存映射文件的文件访问方式 如图所示:
传统文件访问 是通过 read/write 将文件内容复制到用户缓冲区后访问;而 内存映射文件 并不会立即把整个文件读入内存,而是在虚拟地址空间中建立 “文件页→虚拟页”的映射关系。当程序访问某个映射页时,若该页尚未在内存中,则触发缺页异常,由操作系统将对应文件页调入页框,因此具有 按需加载 的特点。