Linux深入理解内存管理4(基于Linux6.6)---虚拟内存管理
一、覆盖技术
覆盖技术主要用于早期的 OS 中(内存<64K), 可用于存储空间受限, 某些大作业不能一次全
部装入内存, 产生了大作业和小内存的矛盾。 而覆盖技术就是解决这一矛盾, 在较小的可用
内存中运行较大的程序。 其基本的思路如下:
- 1.对于进程, 不需要一开始就把程序的全部指令和数据装入内存中执行, 常用功能的代码和数据常驻内存, 不常用功能放在其他程序模块中,只在需要用到时装入内存。
- 2.程序划分为若干个功能上相对独立的程序段, 按照程序逻辑结构让那些不需要同时执行的程序共享同一块内存区。
- 3.当有关程序段的先头程序已经执行结束后, 再把后续程序段从外存调入内存覆盖前面的程序段。
程序员必须把一个程序划分成不同的程序段, 并规定好它们的执行和覆盖顺序, 例如如下实例中, 设某进程的程序正文段由 A, B, C, D, E 和 F 等 6 个程序段组成。
由于程序段 B 不调用 C, 程序段 C 也不调用 B, 因此程序段 B 和 C 无需同时驻留在内存, 它
们可以共享同一内存区。
同理, 程序段 D、 E、 F 也可共享同一内存区。 所以对于程序实际需要的内存空间是 190K,
而采用覆盖技术后, 实际的地址空间只需要最少 100K 就可以了。
虽然覆盖技术可以解决小内存, 大应用的问题, 但是也存在它自身的局限性:
- 增加编程困难: 需程序员划分功能模块, 并确定模块间的覆盖关系, 增加了编程的复杂度
- 增加执行时间: 从外存装入覆盖模块, 时间换空间
- 只能发生在没有调用关系的模块间, 程序员须给出模块间的逻辑覆盖结构
- 发生在运行程序的内部模块间
这个技术对于编程人员的能力要求很高, 同时如果程序过于复杂, 出错可能性很大, 最重要
的一点是, 这个发生在运行程序的内部, 那如果是程序间的空间大于内存空间, 那么就会导
致无法解决小内存, 大应用的问题。
二、交换技术
怎么更好地利用内存, UNIX 让 OS 管理而不是程序员管理, 以运行的程序为单位。 多道程序
在内存中时, 让正在运行的程序或需要运行的程序有更多的内存资源。 其基本思路如下:
- 可将暂时不能运行的程序送入外存中, 从而获得空闲内存空间
- 换出(swap out): 把一个进程的整个地址空间保存到外存
- 换入(swap in): 将外存中某进程的地址空间读入到内存
- 换入换出的基本单位: 整个进程的地址空间
对于交换技术, 需要思考几下几个问题
- 交换时机的确定: 何时需要发生交换? -----只有当内存空间不够或者空间有不够使用的时候
- 交换区的大小: 必须足够大以存放所有用户进程的所有内存映像的拷贝, 必须能对这些内存映像进行直接存储
- 程序换入时重定位: 必须采用动态地址映射的方法, 保证程序换出后, 再次换入的内存位置不需要在原来的位置。
总之, 交换技术是以在内存中程序大小为单位进行, 它不需要程序员给出各个模块间逻辑覆
盖结构, 是由操作系统来完成。
三、局部性原理
那么什么是程序的局部性原理呢?
程序局部性原理: 是指程序在执行时呈现出局部性规律, 即在一段时间内, 整个程序的执行
仅限于程序中的一部分。 相应地, 执行所访问的存储空间也局限于某个内存区域, 具体的来
说, 局限性通常有两种形式:
时间局限性: 被引用过一次的存储器位置在未来会被多次引用(通常在循环中), 一条指令
的一次执行和下次执行, 一个数据的一次访问和下次访问都集中在一个较短时期内。
空间局限性: 如果一个存储器的位置被引用, 那么将来他附近的位置也会被引用, 当前指令
和邻近的几条指令, 当前访问的数据和邻近的几个数据都集中在一个较小区域内。
从理论上来说, 虚拟存储技术是能够实现的, 而且可取得满意的效果。 下面我们以一个例子
不同程序编写方法的局部性特征, 页面大小为 4K, 分配给每个进程的物理页面数为 1。 在一
个进程中, 定义了如下的二维数组 int A[1024][1024], 采用下面两种方式:
对于如果内存空间不足, 就可以采用这种分页加载的方式。
四、虚拟存储技术
随着技术的发展, 覆盖和交换技术已经不能满足现实生活中的应用, 虚拟存储技术就是优化
覆盖技术, 系统自动完成, 综合交换技术, 进程的部分内容交换, 并且利用程序局部性原理。
4.1、虚拟存储技术的概念
虚拟内存是指在具有层次结构存储器的计算机系统中,采用自动实现部分装入和部分交换功
能, 为用户提供一个比物理主存容量大的多的可寻址的一种“主存储器”。 它使用逻辑存储
器与物理存储器分离, 是操作系统给用户提供的一个比真实内存空间大的多的地址空间。 虚
拟存储的基本思路: 将不常用的部分内存块暂存到外存, 其基本原理如下:
- 基于局部性原理, 装载程序时, 只将当前指令执行需要的部分页面或段装入内存
- 指令执行中需要的指令或数据不在内存(称为缺页或缺段) 时, 处理器通知操作系统将相应的页面或段调入内存
- 如果内存空间不足, 操作系统将内存中暂时不用的页面或段保存到外存
那么基于虚拟内存的原理, 可以有以下三个主要特征:
- 多次性: 无需再作业运行时一次性全部装入内存, 而是允许被多次调入内存。
- 对换性: 在作业运行时无需一直常驻内存, 而是允许在作业运行过程中, 将作业换入、 换出
- 虚拟性: 从逻辑上扩充了内存的容量, 使用户看到的内存容量, 远大于实际容量
那讲了那么多虚拟内存技术的概念,我们肯定会想怎么实现这个虚拟内存技术啊? 虚拟内存
技术, 允许一个作业分多次调入内存。 如果采用连续分配方式, 会很不方便实现(进程的地
址连续)。 因此, 虚拟内存的实现需要建立在离散分配的内存管理方式基础上, 结合之前的
分段, 分页, 段页式管理, 有以下几种方式
- 虚拟页式存储, 基于传统分页存储管理
- 虚拟段式存储, 基于传统分段存储管理
- 虚拟段页式存储, 基于传统的段页式存储管理
4.2、虚拟页式存储管理
在页式存储管理的基础上,增加请求调页和页面置算法,就形成了现在的虚拟页式存储管理。
其基本的思路如下:
- 1.基于局部性原理, 在程序装入时, 可以将程序中很快会用到的部分装入内存, 暂时用不到的部分留在外存, 就可以让程序开始执行
- 2.在程序在执行过程中, 当所需要的访问代码或数据不在内存时, 则向系统发出缺页异常请求, 操作系统在处理缺页异常时, 将外存中相应的页面调入内存, 使得进程能继续运行
- 3.当内存空间已满, 而又需要装入新的页面时, 则根据某种算法置换出某个页面, 装入新的页面
在使用虚拟页式存储管理时需要在页表中增加以下表项:
- 页号—页面的编号。
- 有效位—又称驻留位、 存在位或中断位, 表示该页是在内存还是在外存。 如果为 1, 表示在内存中, 页表项有效, 可以访问, 如果式, 表示在外存, 就需要产生缺页来将外存的数据替换到内存中
- 页框号—页面在内存中时所对应的内存块号。
- 访问位—又称引用位或参考位, 表示该页在内存期间是否被访问过, 页面置换算法时候会用到。
- 修改位—表示该页在内存中是否被修改过。 回收时候, 需要用此位来判断, 是否要将内存的数据写回到外存
- 保护位—表示该页的访问方式, 是否能读写执行。
- 禁止缓存位—采用内存映射 I/O 的机器中需要的位。
其基本的表示如下图:
其转换关系如下图:
对于转换关系, 与页式存储管理的转换关系基本类似, 只是加了一个步骤, 通过页表完成逻辑页号到物理帧号的转换后, 需要通过有效位来检测, 该访问数据是否是有效的, 如果该位有效, 说明数据在内存中, 直接访问就可以了; 如果有效位为 0, 说明该数据在外存中, 就会产生一个缺页异常。
4.3、缺页异常处理流程
对于缺页异常的处理流程, 还是结合上一章的例子来梳理下整个缺页异常的过程, 当我们访问一个地址的时候, 首先通过逻辑地址在页表中找, 如果找不到或者找到了, 但是其有效位是 0, 那么就会产生一个缺页异常, 其处理框图如下, 我们来梳理下缺页异常需要做那些操作。
当产生缺页异常后, 硬件就会通过 MMU 的机制在外存中寻找对应的页面, 那么就会存在两
种情况, 一种是页面中存在空闲页, 一种情况是不存在空间页。
- 存在空闲页: 在内存中有空闲物理页面时, 分配一物理页帧 f, 将需要访问的页 p 装入到物理页面 f, 修改 p 的页表项驻留位为 1,物理页帧号为 f
- 不存在空闲页: 依据页面置换算法选择将被替换的物理页帧 f, 对应逻辑页 q, 如 q 被修改过, 则把它写回外存, 将需要访问的页 p 装入到物理页面 f, 修改 p 的页表项驻留位为 1,物理页帧号为 f
其处理流程如下图:
- 只有’ 写指令’ 才需要修改“修改位”, 并且, 一般来说只需要修改快表中的数据, 只有将快表项删除时, 才需要写回内存中, 这样可以减小访问存储的次数
- 和普通的中断处理一样, 缺页中断依然需要保留 CPU 现场
- 需要用某种置换算法来决定一个换出页面
- 换入/换出页面都需要启动慢速的 I/O 操作, 可见, 如果换入换出太频繁, 会由很大的开销页面调入内存后, 需要块表项
五、总结
1. 虚拟内存基本概念
- 虚拟地址空间:每个进程认为自己拥有从零开始的地址空间,操作系统通过将虚拟地址映射到物理地址来实现虚拟内存的管理。虚拟内存使得每个进程都能看到一个完整且连续的内存空间,避免了物理内存的限制和不同进程间的干扰。
- 物理内存与虚拟内存:操作系统通过硬件(如 MMU,内存管理单元)将虚拟地址映射到物理地址。虚拟内存的大小通常大于物理内存,允许进程超出实际物理内存的限制运行。
2. 分页机制(Paging)
分页机制是虚拟内存管理的核心,通过将内存划分为固定大小的“页面”进行管理,使得进程的虚拟地址空间可以灵活地映射到物理内存中。
- 页面和页框:虚拟内存和物理内存都被划分为固定大小的块(通常为 4KB)。虚拟内存的页面与物理内存的页框之间有着一一映射关系。
- 页表:每个进程都有一个页表,记录虚拟页和物理页框之间的映射。页表项(PTE)中通常包含是否存在、权限、修改标志等信息。
- 多级页表:为了优化内存使用,虚拟地址可以分为多个部分,用多级页表来表示,减少内存占用。例如,x86_64 系统使用 4 级页表结构。
3. 内存分配与回收
操作系统采用不同的算法来管理内存的分配和回收。两种主要的分配策略是:
- 伙伴系统:它将物理内存分割为大小相同的块,并通过“伙伴”算法来动态分配和回收内存。这种算法能够有效减少内存碎片。
- Slab 分配器:用于分配内核对象内存(如进程控制块)。Slab 分配器通过缓存预分配的内存块来提高效率,减少碎片化。
4. 交换空间(Swap)
当物理内存不足时,操作系统可以将部分内存内容(如不活跃的页面)移动到硬盘上的交换空间(Swap Space)中,以腾出更多的内存给活跃进程。
- 交换分区/交换文件:可以在磁盘上创建专门的交换分区或使用普通文件作为交换空间。
- 交换行为:当物理内存压力过大时,操作系统会将部分内存页面换出到磁盘,换入新的页面以满足内存需求。这会导致“交换”(Swapping)操作,可能会影响性能。
5. 内存保护
虚拟内存机制为每个进程提供了内存保护,避免了进程间的相互干扰。通过页表中的权限位,操作系统可以限制进程对内存的访问。
- 访问权限:页表项可以设置页面的访问权限,如只读、可写、可执行等。这样可以保护内存不被非法访问或修改。
- 段保护:除了基于分页的保护,系统还可以基于段的方式进行保护,每个段可能具有不同的属性和访问权限(如代码段、数据段等)。
6. TLB(Translation Lookaside Buffer)
由于虚拟地址到物理地址的映射需要频繁查找页表,TLB 缓存提供了加速虚拟地址到物理地址转换的机制。
- TLB 快速查找:TLB 是一个缓存,用于存储最近使用的虚拟地址到物理地址的映射。如果在 TLB 中找到了映射,则可以快速返回物理地址,避免每次都访问页表。
- TLB 失效(TLB Miss):当访问的地址不在 TLB 中时,发生 TLB 失效,操作系统需要访问页表进行映射。
7. 缺页异常与页面置换
当进程访问一个不在内存中的虚拟页面时,发生 缺页异常(Page Fault)。操作系统会根据缺页异常的类型进行处理:
- 缺页异常处理:如果缺少的页面存在于磁盘的交换空间或文件中,操作系统会将该页面加载到内存中。此时,操作系统需要选择一个内存中的页面进行换出(页面置换)。
- 页面置换算法:操作系统使用不同的算法来选择哪个页面被换出。例如,最常见的算法包括 FIFO(先进先出)、LRU(最久未使用) 等。
8. 内存碎片
内存碎片分为两种类型:
- 外部碎片:物理内存中存在许多不连续的空闲块,这些块的总大小可能足够,但无法满足较大的内存请求。
- 内部碎片:由于内存块分配的方式不完全匹配进程请求的内存大小,导致分配的内存块中有部分无法使用。
操作系统通过 伙伴系统 和 slab 分配器 等机制来减少碎片化,并在内存回收时进行合并。
9. 内存映射(Memory Mapping)
Linux 操作系统支持内存映射机制,允许文件和设备的内容映射到进程的虚拟地址空间。内存映射使得应用程序可以直接在内存中操作文件数据,从而提高 I/O 性能。
- mmap() 系统调用:通过
mmap()
,进程可以将文件或设备的内容直接映射到其虚拟内存空间。 - 共享内存:多个进程可以通过内存映射共享一段内存区域,通常用于进程间通信(IPC)。