目录
一、相关问题
1. 什么是虚拟内存?为什么需要虚拟内存?
(1)内存扩展
(2)内存隔离
(3)物理内存管理
(4)页面交换
(5)内存映射文件
2. 局部性原理
(1)时间局部性
(2)空间局部性
3. 什么是内存分段和分页?作用是什么?
(1)内存分段
(2)内存简单分页
(3)多级页表
那么 为什么不分级的 页表就 做不到这样 节约内存 呢?
(4)段页式内存管理
(5)分页分段的作用?
① 逻辑隔离
② 内存保护
③ 虚拟内存
④ 内存共享
⑤ 内存管理
(6)分段与分页的区别?
① 分段
② 分页(Paging)
(7)分段与分页优缺点?
4. 解释一下内存页面置换算法?
补充:快表
(1)最佳页面置换算法
(2)先进先出置换算法
(3)最近最久未使用的置换算法
(4)时钟页面置换算法
一、相关问题
1. 什么是虚拟内存?为什么需要虚拟内存?
虚拟内存 在 每一个 进程 创建加载的 过程中,会 分配一个 连续 虚拟地址空间,它 不是 真实 存在的,而是 通过映射 与 实际地址空间 对应,这样 就可以 使 每个进程 看起来 都有 自己独立的 连续地址空间,并 允许 程序访问 比物理内存 RAM 更大的 地址空间,每个程序 都可以 认为它 拥有 足够的 内存来 运行。
需要 虚拟内存的 原因:
(1)内存扩展
虚拟内存 使得 每个程序 都可以 使用 比实际可用 内存 更多的 内存,从而 允许运行 更大的程序 或 处理更多的 数据。
(2)内存隔离
虚拟内存还 提供了 进程之间的 内存隔离。每个进程 都有自己的 虚拟地址 空间,因此 一个进程 无法 直接访问 另一个 进程的 内存。
(3)物理内存管理
虚拟内存 允许 操作系统 动态地 将数据 和 程序的部分 加载到 物理内存中,以 满足当前 正在运行的 进程的 需求。当 物理内存不足时,操作系统 可以将 不常用的 数据或程序 暂时移到 硬盘上,从而 释放内存,以便 其他进程 使用。
(4)页面交换
当 物理内存 不足时,操作系统 可以 将一部分 数据从 物理内存 写入到 硬盘的 虚拟内存中,这个过程被称为 页面交换。当需要时,数据 可以 再次 从虚拟内存中 加载到 物理内存 中。这样 可以保证 系统可以 继续运行,尽管 物理内存 有限。
(5)内存映射文件
虚拟内存 还可以 用于 将文件映射到 内存中,这使得 文件的 读取和写入 可以 像访问 内存一样 高效。
2. 局部性原理
在一段时间内,程序倾向于 多次访问 相同的 数据 或 接近的 数据,而不是 随机地 访问 内存中的 各个位置。局部性原理 通常分为 两种类型:时间局部性 和 空间局部性。
(1)时间局部性
如果 一个数据 被访问,那么在 不久的 将来它 很可能会 再次 被访问。这意味着 程序 在短时间内倾向于 反复使用 相同的 数据项。
例如在 循环中 反复访问 数组的 元素。通过 利用 时间局部性,程序可以 将频繁 使用的 数据存 储在 缓存中,从而 减少访问 主内存的 次数,提高程序的 执行速度。
(2)空间局部性
如果一个 数据被 访问,那么 它附近的 数据也 很可能会 被访问,这意味着 程序在 访问一个 数据时,通常会 在接近 该数据的 附近访问 其他数据。
例如遍历数组时,往往会 访问 相邻的 元素。
文件系统在 磁盘上 存储数据时,通常会 将相关的 数据块 放在相邻的 磁盘扇区上,以便在 访问一个数据块 时能够 快速地 访问相邻的 数据块。
3. 什么是内存分段和分页?作用是什么?
(1)内存分段
内存分段是 将一个 程序的 内存空间 分为不同的 逻辑段 segments,每个 段 代表程序的 一个功能模块 或 数据类型,如 代码段、数据段、堆栈段 等。每个段 都有其 自己的大小 和 权限。
分段机制下的 虚拟地址 由两部分组成,段选择因子 和 段内偏移量(二维结构)。
- 段选择子就 保存在 段寄存器 里面。段选择子 里面 最重要的是 段号,用作 段表的 索引。段表 里面保存的是 这个段的 基地址、段的界限 和 特权等级 等。
- 虚拟地址 中的 段内偏移量 应该 位于 0 和 段界限 之间,如果 段内 偏移量是 合法的,就将 段基地址 加上 段内偏移量 得到 物理内存地址。
知道了 虚拟地址 是 通过 段表 与 物理地址 进行 映射的,分段机制 会把 程序的 虚拟地址分成 4 个段,每个段 在段表中 有一个项,在 这一项 找到 段的 基地址,再加上 偏移量,于是就能 找到 物理内存 中的 地址,如下图:
如果要 访问 段 3 中 偏移量 500 的虚拟地址,我们 可以 计算出 物理地址为,段 3 基地址 7000+偏移量 500 = 7500。
(2)内存简单分页
分段的好处 就是能 产生 连续的 内存空间,但是会 出现「外部内存碎片 和 内存交换的空间 太大」的问题。要 解决 这些问题,那么就要 想出 能少出现一些 内存碎片 的办法。另外,当 需要 进行内存交换的 时候,让 需要 交换写入 或者 从磁盘装载的 数据 更少一点,这样就 可以 解决问题了。这个办法,也就是 内存分页(Paging)。
分页是 把 整个 虚拟和物理 内存空间 切成一段段 固定尺寸的 大小。这样 一个 连续并且 尺寸固定的 内存空间,我们叫 页(Page)。在 Linux 下,每一页的大小为 4KB。虚拟地址与物理地址 之间通过 页表 来映射,如下图:
页表 是 存储在 内存里的,内存管理单元(MMU)就做 将 虚拟内存地址 转换成 物理地址 的工作。而当 进程访问 的 虚拟地址 在 页表 中查不到时,系统会 产生一个 缺页异常,进入系统内核空间 分配 物理内存、更新 进程 页表,最后再 返回 用户空间,恢复进程的 运行。
内存分页 由于 内存空间 都是 预先划分 好的,也就 不会像 内存分段 一样,在 段与段 之间 会产生间隙 非常小的 内存,这正是 分段 会产生 外部内存碎片 的原因。
采用了 分页,页与页 之间 是紧密排列的,所以不会有外部碎片。但是,因为 内存分页机制 分配内存的 最小单位是 一页,即使 程序 不足 一页大小,我们 最少 只能 分配一个页,所以 页内 会出现 内存浪费,所以 针对 内存分页 机制 会有 内部内存碎片 的现象。
如果 内存空间 不够,操作系统 会把 其他 正在运行的 进程 中的「最近 没被使用」的内存 页面 给释放掉,也就是 暂时 写在 硬盘上,称为 换出(Swap Out)。一旦 需要的 时候,再 加载进来,称为 换入(Swap In)。所以,一次性 写入磁盘 的 也只有 少数的 一个页 或者 几个页,不会 花太多时间,内存交换的效率就 相对比较高。
更进一步地,分页的方式 使得 我们 在加载程序的 时候,不再需要 一次性 都把 程序 加载到 物理内存中。我们 完全可以 在进行虚拟内存 和 物理内存的 页之间的 映射之后,并不 真的 把页 加载到 物理内存 里,而是 只有在 程序运行中,需要 用到对应 虚拟内存页 里面的 指令 和 数据时,再加载到 物理内存 里面去。
在 分页 机制下,虚拟地址 分为 两部分,页号和页内偏移。页号 作为 页表的索引,页表 包含 物理页,每页 所在物理内存的 基地址,这个 基地址 与 页内偏移的 组合 就形成了 物理内存地址,见下图。
总的来说,对于一个内存地址转换,其实 就是三个步骤:
- 把 虚拟内存地址,切分成 页号和偏移量;
- 根据 页号,从 页表 里面,查询 对应的 物理页号;
- 直接拿 物理页号,加上 前面的 偏移量,就得到了 物理 内存地址。
下面 举个例子(单个进程),虚拟内存中的 页 通过 页表 映射 为了 物理内存中的 页,如下图:
缺陷
有空间上的缺陷。因为操作系统是可以同时运行非常多的进程的,这就意味着页表会非常的庞大。
在 32 位 的环境下,虚拟地址空间 共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100万(2^20)个页,每个「页表项」需要 4 个字节大小来 存储,那么整个 4GB 空间的 映射 就 需要有 4MB 的 内存来 存储页表。
每个进程 都是有 自己的 虚拟 地址空间的,也就说 都有自己的 页表。那么,100 个 进程的话,就需要 400MB 的 内存来 存储页表,这是 非常大的 内存了,更别说 64 位 的环境了。
(3)多级页表
要解决 上面的 问题,就 需要 采用一种 叫作 多级页表(Multi-Level Page Table)的解决方案。上述 对于 单页表 的实现方式,在 32 位 和 页大小 4KB 的环境下,一个进程的 页表 需要 装下 100 多万个「页表项」,每个 页表项 占用 4 字节 大小,相当于 每个 页表 需占用 4MB 大小的空间。
我们把 这个 100 多万个「页表项」的 单级页表 再分页,将 页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成 二级分页。如下图所示:
每个进程 都有 4GB 的 虚拟地址空间,而 显然 对于 大多数 程序 来说,其 使用到的 空间 远未 达到 4GB,因此 会 存在 部分 对应的 页表项 都是空的,根本 没有分配,对于 已分配的 页表项,如果 存在 最近一定时间 未访问的 页表,在 物理内存 紧张的 情况下,操作系统会 将页面 换出到 硬盘,也就是说 不会占用 物理内存。
如果 使用了 二级分页,一级页表 就可以 覆盖 整个 4GB 虚拟 地址空间,但如果 某个 一级页表的页表项 没有被 用到,也就 不需要 创建这个 页表项 对应的 二级页表 了,即 可以在 需要时 才创建 二级页表。
做个简单的计算,假设 只有 20% 的 一级页表项 被用到了,那么 页表 占用的 内存空间 就只有 4KB(一级页表)+20%*4MB(二级页表)= 0.804MB,这对比 单级页表 的 4MB 是一个 巨大的节约。
那么 为什么不分级的 页表就 做不到这样 节约内存 呢?
从 页表的性质 来看,保存在 内存中的 页表 承担的 职责是 将 虚拟地址 翻译成 物理地址。假如 虚拟地址 在 页表中 找不到 对应的 页表项,计算机系统 就不能 工作了。所以 页表一定 要 覆盖全部 虚拟地址空间,不分级的页表 就 需要有 100 多万个 页表项 来映射,而 二级分页 则 只需要 1024 个 页表项(此时 一级页表 覆盖到了 全部 虚拟地址空间,二级页表在 需要时 创建)。
我们把 二级分页 再推广到 多级页表,就会 发现 页表 占用的 内存空间 更少了,这一切都要 归功于 对 局部性原理的 充分应用。
对于 64 位 的系统,两级分页肯定不够了,就变成了四级目录,分别是:
- 全局页目录项 PGD(Page Global Directory);
- 上层页目录项 PUD(Page Upper Directory);
- 中间页目录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry);
(4)段页式内存管理
内存分段 和 内存分页 并不是 对立的,它们是 可以 组合起来 在同一个 系统中 使用的,组合起来后通常称为 段页式 内存管理。
段页式内存管理 实现的 方式:
- 先 将程序 划分为 多个 有逻辑意义的 段,也就是 前面提到的 分段机制;
- 接着 再把 每个段 划分为 多个页,也就是 对分段 划分出来的 连续空间,再划分 固定大小的 页;
这样,地址结构 就由 段号、段内页号 和 页内位移 三部分 组成。
用于 段页式 地址变换 的数据结构是 每一个程序 一张段表,每个段 又建立 一张页表,段表中的地址 是页表的 起始地址,而 页表中的 地址则为 某页的 物理页号,如图所示:
段页式 地址变换 中要得到 物理地址 须经过 三次内存访问:
- 第一次 访问段表,得到 页表起始地址;
- 第二次 访问页表,得到 物理页号;
- 第三次 将物理页号 与 页内位移 组合,得到 物理地址。
可用 软、硬件 相结合的方法 实现 段页式 地址变换,这样虽然 增加了 硬件成本 和 系统开销,但提高了 内存的 利用率。
(5)分页分段的作用?
① 逻辑隔离
内存 分段和分页 都实现了 程序的 逻辑隔离,使不同的 功能模块 或 数据类型 能够被 单独管理和保护,提高了 程序的 可靠性和安全性。
② 内存保护
通过 将不同的 段或页面 设置为 只读、可读写、不可执行等 权限,操作系统可以 确保 程序 不会越界访问 或 修改 其他段的 内容,从而 提高了 系统的 稳定性。
③ 虚拟内存
分段和分页 都 有助于 实现 虚拟内存 的概念,允许 应用程序 认为它们 在使用的是 一个 比实际物理内存 更大的 内存空间。
④ 内存共享
通过 分页,操作系统 可以实现 内存页面的 共享,从而 节省 内存空间,多个进程 可以 共享 相同的代码 或 数据页面。
⑤ 内存管理
分页 更加灵活,允许 操作系统 将 不同进程的 页面 分散 存放在 物理内存 中,从而 提高 内存利用率。分段 则 更适用于 管理不同的 逻辑模块。
(6)分段与分页的区别?
① 分段
- 基本单位:地址空间 被划分为 不同的 逻辑段,每个段 具有 独立的含义,如 代码段、数据段等。
- 段的长度:每个段的 长度可以 动态变化,不同段 的长度 可以不同。
- 内部碎片:由于 每个段的 长度 可以 动态变化,可能会 导致 内部碎片,即 段内部的 未使用空间。
- 外部碎片:可能会 导致 外部碎片,即 段之间的 未使用空间。
- 逻辑地址:逻辑地址 由 两部分组成,一个是 段号,另一个是 段内偏移。
② 分页(Paging)
- 基本单位:地址空间 被划分为 固定大小的 页面,物理内存 也被 划分为 相同大小的 页面框。
- 页面的长度:页面的长度 是固定的,由 操作系统 定义。
- 内部碎片:由于 页面长度 固定,可能会 导致 内部碎片,即 页面内部的 未使用空间。
- 外部碎片:由于 页面长度 固定,不同 页面 之间的 未使用 空间 无法利用,可能 导致 外部碎片。
- 逻辑地址:逻辑地址 由 两部分组成,一个是 页号,另一个是 页内偏移。
综述:
- 分页 对用户 不可见,分段 对用户 可见。
- 分页的 地址空间是 一维的,分段的 地址空间是 二维的。
- 分页(单级页表)、分段 访问一个 逻辑地址都 需要 两次访存,分段存储 中可以 引入 快表机制。
- 分段 更容易 实现 信息的 共享和保护(纯代码 或 可重入代码 可以共享)。
(7)分段与分页优缺点?
分页管理:内存 空间利用率高,不会 产生 外部碎片,只会有 少量的 页内碎片。但是 不方便 按照 逻辑模块 实现信息的 共享和保护。
分段管理:很方便 按照 逻辑模块 实现 信息的 共享和保护。但是如果 段长 过大,为其 分配 很大的 连续空间会 很不方便,段式管理会 产生 外部碎片。
补充:
分页管理将内存划分为固定大小的页,这些页并不直接反映程序的逻辑结构。因此,很难根据程序的逻辑模块来划分页,也很难直接通过页来实现信息的共享和保护。
分段管理将程序的地址空间划分为若干段,如代码段、数据段、堆栈段等。这些段与程序的功能模块紧密相关,反映了程序的逻辑结构。由于段是按照程序的功能模块划分的,因此很容易确定哪些段需要共享,哪些段需要保护。
4. 解释一下内存页面置换算法?
先说一下 缺页异常(缺页中断)。
当 CPU 访问的 页面 不在 物理内存 时,便会 产生一个 缺页中断,请求 操作系统 将所 缺页 调入到 物理内存。那它 与一般 中断的 主要区别 在于:
- 缺页中断 在指令执行「期间」产生 和 处理 中断信号,而 一般中断在 一条指令 执行「完成」后 检查和处理 中断信号。
- 缺页中断 返回到 该指令的 开始 重新执行「该指令」,而 一般 中断 返回 回到 该指令的「下一个指令」执行。
缺页中断的处理流程如下图:
- 在 CPU 里访问 一条 Load M 指令,然后 CPU 会去找 M 所对应的 页表项。
- 如果 该页表项 的状态位是「有效的」,那 CPU 就可以 直接去 访问 物理内存 了,如果 状态位 是「无效的」,则 CPU 则会 发送缺页 中断请求。
- 操作系统 收到了 缺页中断,则会 执行 缺页中断 处理函数,先会 查找 该页面在 磁盘中的 页面的位置。
- 找到 磁盘中 对应的 页面后,需要把 该页面 换入到 物理内存 中,但是在 换入前,需要在 物理内存中 找空闲页,如果 找到 空闲页,就把 页面 换入到 物理内存 中。
- 页面 从 磁盘 换入到 物理内存 完成后,则把 页表项中的 状态位 修改为「有效的」。
- 最后,CPU 重新执行 导致 缺页异常 的指令。
第 4 步 如果 找不到空闲页的话,就说明 此时 内存 已满了,这时候,就需要「页面置换算法」选择一个 物理页,如果 该物理页有 被 修改过(脏页),则 把它 换出到 磁盘,然后 把该 被置换 出去的 页表项的 状态 改成「无效的」,最后 把 正在访问的 页面 装入到 这个物理页 中。页表项通常有如下图的字段:
- 状态位:用于 表示 该页 是否 有效,也就是说 是否 在物理内存中,供 程序访问时 参考。
- 访问字段:用于 记录该页 在一段时间 被访问的 次数,供 页面置换算法 选择出 页面时 参考。
- 修改位:表示 该页在 调入内存后 是否有被 修改过,由于 内存中的 每一页 都在 磁盘上 保留 一份副本,因此,如果 没有修改,在 置换该页时 就不需要 将该页 写回到 磁盘上,以 减少 系统的 开销;如果 已经 被修改,则将 该页 重写到 磁盘上,以 保证磁盘中 所保留的 始终是 最新的副本。
- 硬盘地址:用于 指出 该页在 硬盘上的 地址,通常是 物理块号,供 调入 该页时 使用。
所以,页面置换算法的 功能是,当 出现 缺页异常,需 调入 新页面 而内存 已满时,选择 被置换的 物理页面,也就是说 选择一个 物理页面 换出到 磁盘,然后把 需要访问 的页面 换入到 物理页。
补充:快表
快表是 一种 特殊的 高速缓冲存储器(Cache),内容是 页表 中的 一部分 或 全部内容。在操作系统中 引入 快表是 为了 加快 地址映射速度。在 虚拟页式 存储管理 中设置了 快表,作为 当前 进程页表的 Cache。通常快表处于 MIMU 中。
- 按照 逻辑地址 中的 页号 查快表。
- 若该页 已存在 快表 中,则由 快表中 记录的 映射信息 和 页内偏移量 形成 物理地址。
- 若该页 不在快表中,则 再查 主存页表,与 页表中 记录的 映射信息 和 页内偏移量 形成物理地址,同时 将该 页表项 登记到 快表中。
- 当 快表 填满后,又要 登记 新页时,则需要 按照一定 替换策略 淘汰一个 旧的 登记项。
(1)最佳页面置换算法
最佳页面置换算法 基本思路是,置换在「未来」最长时间 不访问的 页面。所以,该算法 实现 需要 计算 内存中 每个 逻辑页面的「下一次」访问时间,然后比较,选择 未来 最长时间 不访问的 页面。
举个例子,假设 一开始 有 3 个 空闲的 物理页,然后 有请求的 页面序列,那 它的 置换过程 如下图:
在 这个 请求的 页面序列 中,缺页 共 发生了 7 次(空闲页 换入 3 次+最优页面置换 4 次),页面置换 共发生了 4 次。这很理想,但是 实际系统中 无法实现,因为 程序 访问页面 时是 动态的,我们是 无法 预知 每个 页面在「下一次」访问前的 等待时间。
所以,最佳页面置换算法 作用是 为了衡量 你的算法的 效率,你的 算法 效率 越接近 该算法的 效率,那么 说明你的 算法是 高效的。
(2)先进先出置换算法
无法 预知页面在 下一次访问 前 所需的 等待时间,那可以 选择在 内存驻留 时间 很长的页面 进行中 置换,这个就是「先进先出置换」算法的 思想。
以前面的 请求的 页面序列作为例子,假设使用先进先出置换算法,则过程如下图:
在这个 请求的 页面序列中,缺页共发生了 10 次,页面置换 共发生了 7 次,跟 最佳页面 置换算法 比较起来,性能 明显差了 很多。
(3)最近最久未使用的置换算法
最近最久未使用(LRU)的置换算法的 基本思路 是,发生缺页时,选择 最长时间 没有 被 访问的页面 进行置换,也就是说,该算法 假设 已经 很久 没有 使用的 页面 很有可能 在未来 较长的 一段时间内 仍然不会 被使用。
这种算法 近似 最优置换算法,最优置换算法是 通过「未来」的 使用情况 来 推测 要淘汰的 页面,而 LRU 则是 通过「历史」的 使用情况 来推测 要淘汰的 页面。
以前面的 请求的 页面序列作为例子,假设 使用 最近最久未使用的 置换算法,则过程如下图:
在这个 请求的 页面序列 中,缺页 共 发生了 9 次,页面置换 共 发生了 6 次,跟 先进先出 置换算法比较 起来,性能 提高了一些。虽然 LRU 在理论上是 可以实现的,但 代价很高。
为了 完全 实现 LRU,需要 在内存中 维护一个 所有 页面的 链表,最近 最多使用 的 页面在 表头,最近最少 使用的 页面在 表尾。困难的是,在 每次访问 内存时 都 必须要 更新「整个链表」。在 链表中 找到一个 页面,删除它,然后把 它移动到 表头是 一个 非常 费时的 操作。所以,LRU 虽然看上去 不错,但是 由于开销 比较大,实际 应用中 比较少 使用。
(4)时钟页面置换算法
那 有没有一种 即能 优化置换的 次数,也能 方便实现的 算法呢?
时钟页面置换算法 就可以 两者兼得,它跟 LRU 近似,又是对 FIFO 的 一种改进。
该算法的 思路是,把 所有的 页面都 保存在 一个类似 钟面的「环形链表」中,一个 表针 指向 最老的 页面。当 发生缺页 中断时,算法 首先 检查 表针 指向的页 面:
- 如果 它的 访问位 是 0,表示该页面最久未被访问,可以选择进行置换,然后 淘汰 该页面,并 把新的 页面 插入 这个位置,然后 把表针 前移 一个位置。
- 如果 访问位 是 1,表示该页面最近被访问过,它仍然处于活跃状态,然后 清除 访问位,并把 表针 前移一个 位置,重复 这个 过程 直到 找到了 一个访问位 为 0 的 页面 为止。
时钟页面置换算法的 工作流程图如下: