C++基础教程
——
C语言部分卷1C语言基础语法
补充04虚拟内存的核心技术

最新版本V2.0
王道C++团队
COPYRIGHT ⓒ 2021-2024. 王道版权所有

概述

Gn!

虚拟内存对于程序员编码而言,只需要了解地址/地址值等概念就足够了,关于它的实现细节即便不了解也完全不影响写出良好的代码。

但考虑到虚拟内存是面试比较常见的话题,这里简要介绍一下虚拟内存的一些核心技术。

若大家感兴趣,可以上网自行查阅更详细的资料。

分段(Segmentation)

Gn!

分段(Segmentation)是虚拟内存的核心技术之一,它将进程的虚拟内存空间划分为多个不同的逻辑区域(段),每个段表示程序的一个特定部分。

分段是根据进程的结构和需求来划分的,不同的进程分段可能不同,分段能提供更灵活的内存保护和管理。

常见的段类型包括:

  1. 代码段(Text Segment):存放程序的可执行代码。

  2. 数据段(Data Segment):存放静态数据,如全局变量、常量等。

  3. 堆(Heap):用于动态内存分配(如 malloc 在 C 中分配的内存)。

  4. 栈(Stack):用于函数调用时保存局部变量、返回地址等信息。

实际上我们讲的虚拟内存空间模型,就是分段技术的体现。使用分段技术后,还涉及到一个将虚拟地址转换成物理地址的过程。

虚拟地址到物理地址的转换

Gn!

在分段系统中,每个虚拟地址由两个部分组成:段号段内偏移。具体地说,虚拟地址的形式是 (segment_number, offset),这两部分用于在内存中定位具体的物理地址。

虚拟地址格式

  1. segment_number: 表示虚拟地址所属的段。

  2. offset: 表示在段内的具体位置。

地址转换步骤

  1. 段号查找:虚拟地址中的段号首先会被用来查找段表。段表是操作系统维护的一个数据结构,存储了每个段的基地址(即段的起始物理地址)和段的大小(即段的界限)。

  2. 查找段表:操作系统通过段号在段表中找到对应段的基地址。段表通常存储在内存的某个固定位置,段表中的每一项包含:

    1. 基地址:段的起始物理地址。

    2. 界限:该段的最大长度,若访问地址超出了界限,则会触发一个“段越界”异常。

    3. 访问权限:该段的访问权限,例如是否可读、可写或可执行。

  3. 计算物理地址:通过段的基地址和虚拟地址中的偏移量,计算出物理地址: Physical Address = Base Address + Offset 其中,Base Address是段的基地址,Offset是虚拟地址中的偏移量。

  4. 访问检查:操作系统会根据段表中的访问权限字段检查是否允许对该段进行读、写或执行操作。如果权限不符,系统会触发异常(如“段权限错误”)。

可以看出,段表是地址转换过程中的核心概念,那么什么是段表?

段表(Segment Table)

Gn!

段表是分段系统中的核心数据结构,保存了每个段的关键信息,主要包括:

  1. 基地址(Base Address):段在物理内存中的起始位置。

  2. 界限(Limit):段的大小或有效范围,即段的结束位置。段内访问的偏移量必须小于段的界限,否则会发生越界错误。

  3. 访问权限(Access Rights):指定该段的访问权限,包括是否可读、可写、是否可执行等。

  4. 段标识:有些系统会为段提供标识,用于区分不同类型的段(如代码段、数据段等)。

例如,假设某段的基地址是 0x1000,界限是 0x200,那么该段在物理内存中的地址范围是从 0x10000x11FF,偏移量必须小于 0x200

段内存保护与权限

Gn!

分段技术的一个重要特点是它可以为不同的段提供不同的访问权限,这样可以有效地控制对内存的访问。每个段表项通常包含访问控制信息,来指定该段的操作权限。

  1. 可读/不可读:控制程序是否能够读取该段。

  2. 可写/只读:控制程序是否可以写入该段。

  3. 可执行/不可执行:控制程序是否可以执行该段中的代码。

这样如果程序试图对代码段进行写操作,或者试图执行数据段中的数据,操作系统可以通过硬件(如内存管理单元 MMU)检测并触发保护错误,阻止非法操作。

段越界检测

Gn!

每个段都有一个界限(limit),它定义了该段的有效大小。在程序访问虚拟内存时,操作系统需要确保偏移量不超过段的界限。若访问超出界限,操作系统会产生一个“段越界异常”(Segment Fault),阻止不合法的内存访问。

例如,假设段表中的某段有一个基地址 0x1000 和界限 0x500,如果程序访问的偏移量大于 0x500,即使该地址是有效的虚拟地址,也会被操作系统阻止。

段的动态管理

Gn!

分段技术允许内存管理更加灵活,但也会遇到一些问题,例如外部碎片问题。由于每个段的大小是根据程序的需要动态分配的,因此不同段之间的空闲内存可能无法被有效利用,造成外部碎片。

  1. 外部碎片:段的大小不同,可能导致内存中出现无法利用的小空闲区域。

  2. 内存压缩和整理:为了避免外部碎片,操作系统可能需要定期进行内存整理(例如将内存中的段移动到一起,释放未使用的空间)。

分页(Paging)

Gn!

分页(Paging):

分页是一种将虚拟内存和物理内存划分成大小相同的小块的技术,虚拟内存空间和物理内存空间通过页(Page)和页框(Page Frame)来映射和管理。具体而言:

  1. 虚拟内存空间被分割成的大小相等的小块称之为页或者虚拟页(Page)

  2. 物理内存被分割成的大小相等的小块被称之为页框(Page Frame)

  3. 页和页框的大小是对应相等的,虚拟内存空间和物理内存之间的映射就是页和页框之间的映射。

由于分页技术的存在,虚拟内存的大小完全可以超过实际物理内存。

假设物理内存大小是8Kb,页的大小是4Kb,虚拟内存空间设定为16Kb。

此时虚拟内存就会被划分成4个页,物理内存显然无法直接容纳虚拟内存,只能分配2个页框参考下面表格:

虚拟内存(16KB)物理内存(8KB)
页1、页2、页3、页4页框1、页框2

假如进程需要访问虚拟页1中的数据,那么就需要知道虚拟页1实际映射到了哪个页框,为了管理它们之间的映射,就需要使用页表技术。

现代操作系统,选择的分页大小一般都是4Kb。

页表(Page Table)

Gn!

页表(Page Table):

操作系统用一个叫做 页表 的数据结构来记录虚拟页和物理页框之间的映射关系。页表就像是一个“转换表”,用来告诉操作系统哪个虚拟页对应哪个物理页框。

进程开始运行后,假如需要使用虚拟页1和虚拟页2中的数据内容,此时页框1和页框2都是空闲的。于是将虚拟页1和2中的数据从硬盘加载到物理内存中,并映射到页框1和页框2。

简单来说,可以理解成下面的表格:

虚拟页编号物理页框编号实际存储位置
114Kb,物理内存
224Kb,物理内存
3未分配物理内存数据仍存储在硬盘上
4未分配物理内存数据仍存储在硬盘上

从页表中我们可以看到,虚拟页1映射到物理页框1,虚拟页2映射到物理页框2。虚拟页3和4中的数据还未使用,仍然存储在硬盘上。

此时,当进程试图访问虚拟内存中的某个地址时,操作系统将通过页表查找这个虚拟地址对应的物理地址。

比如当进程访问虚拟页1中的数据时,操作系统会检查页表,找到虚拟页1映射到物理页框1,操作系统将虚拟地址转换为物理地址,从而访问物理内存,这就是进程访问物理内存的内部原理。

那如果要访问虚拟页3或4中的内存,怎么办?

缺页(Page Fault)

Gn!

缺页(Page Fault)

虚拟页3虚拟页4 这些虚拟页中的数据内容,尚未被映射到物理内存中的页框上,如果进程想要访问这两个虚拟页中的数据,就会引发缺页

一旦发生缺页,操作系统一般会执行换页操作,换页技术是虚拟内存大小可以超过实际物理内存大小的主要原因。

换页(Page Swap)

Gn!

换页(Page Swap):

当内存访问发生缺页,且物理内存已经没有空间继续分配新的页框时,操作系统会执行换页操作。

操作系统会根据换页算法(比如选择一个最早加载入内存的虚拟页)选择一个虚拟页,将其内容数据写回到磁盘,并将其所占的物理页框释放出来。这一过程被称为换页

接下来沿用之前的例子继续分析:

  1. 缺页发生:进程需要访问虚拟页3的数据,但物理内存没有足够的空间分配新的页框,这时就会触发缺页。操作系统会执行换页操作。

  2. 选择换出的虚拟页:假设操作系统选择最早加载入内存的虚拟页1来进行换页,于是将虚拟页1中的数据从物理内存写回到磁盘。

    1. 虚拟页1原本对应的页框1被空闲出来。

    2. 虚拟页1中的数据已被写回磁盘,不再存储在物理内存中。

  3. 加载虚拟页3:此时,页框1被空出来,操作系统将虚拟页3的数据从磁盘加载到物理内存的页框1,并更新页表。

此时页表更新为以下状态:

虚拟页编号物理页框编号实际存储位置
1未分配物理内存4Kb,磁盘(换页后)
224Kb,物理内存
314Kb,物理内存
4未分配物理内存数据仍存储在硬盘上

在这种情况下,操作系统将 虚拟页1 中的数据写回硬盘,而将 虚拟页3 中的数据从原本的磁盘上加载到物理内存的 页框1 中。

这样一番操作后,进程就可以正常访问虚拟页3中的数据了。

若继续想要访问虚拟页4中的数据,怎么办?继续执行换页即可。

将虚拟页2(此时虚拟页2是最早加载的)中的数据写回到硬盘,并把虚拟页4中的数据加载到页框2中,最后更新页表如下:

虚拟页编号物理页框编号实际存储位置
1未分配物理内存4Kb,磁盘(换页后)
2未分配物理内存4Kb,磁盘(换页后)
314Kb,物理内存
424Kb,物理内存

通过这样的换页操作,操作系统有效地管理了有限的物理内存空间,并根据需求加载和替换虚拟页,确保进程能够按需访问数据。

扩展几个问题:

若进程又需要访问虚拟页1或虚拟页2中的数据,怎么办呢?

答:虚拟页1和2中的数据并未加载到内存中,于是触发缺页,进行新一轮的换页、更新页表操作。

若进程需要同时并发访问4个虚拟页的数据,怎么办呢?

答:这个问题看起来像抬杠,但其实是一个好问题,并且操作系统设计者已经考虑过这个问题了。

由于物理内存不足,操作系统实际上无法实现真正的同时访问四个虚拟页,所以还是会依据顺序采用“换页”的方式来进行访问。

由于换页过程涉及磁盘 I/O,性能显然是不好的,所以频繁的换页会拖累系统整体性能。

为了减少换页频率,可以考虑两种手段:

  1. 增大物理内存,也就是花钱,最简单有效直接。

  2. 采用更好的换页算法。常见的换页算法,比如:

    1. 先进先出(FIFO, First-In-First-Out),总是优先换出最早加载到内存的虚拟页。优点是实现简单,只需要一个队列就能够实现。但缺点也很明显,若最早加载的虚拟页是最常用的呢?这样反而增加了换页频率。

    2. 最近最少使用(LRU, Least Recently Used),总是优先换出最近最少使用的虚拟页。LRU的优点很明显,可以有效减少换页频率,但实现起来可能会比较复杂。

    3. 最少使用(LFU, Least Frequently Used),为每一个虚拟页增加一个计数器,需要换页时换出访问次数最少的虚拟页。

    4. ...

实际操作系统中,通常不会仅依赖单一的换页算法。它们会根据不同的场景或系统负载,综合使用多种换页算法。这通常涉及权衡算法的复杂性和性能的平衡,甚至可能根据不同的进程或负载动态调整算法。

这比较复杂,就先不扩展了。(我确信我已经完全掌握了这些算法,但这里空间不足,无法写下)

总结

Gn!

下面我们来总结一下分段和分页两种虚拟内存的核心技术。

分段

Gn!

分段技术:

分段的内部原理涉及将虚拟地址分为段号和偏移量,通过段表将段号映射到物理内存中的起始地址,再根据偏移量计算最终的物理地址。

段表还包含访问控制信息,用于管理不同段的访问权限。同时,操作系统需要管理段的动态分配和保护,确保程序不会访问非法或越界的内存。

分段技术的优点:

  1. 符合程序的逻辑结构: 分段技术允许将程序的虚拟地址空间按照逻辑上的模块进行划分。例如,程序的代码段、数据段、堆栈段等分别对应不同的段,便于操作系统根据程序的实际需求来管理内存。

  2. 灵活的内存分配: 每个段的大小可以根据实际需求进行动态调整,避免了内存浪费。段的划分不仅是为了提高内存利用率,还可以将相关的程序部分组织在一起,简化内存管理。

  3. 提供更好的保护机制: 每个段可以有独立的访问权限设置,比如代码段可以设置为只读,堆栈段可以设置为读写。这样可以有效地保护内存,防止不同类型的内存区域之间发生非法访问。

  4. 提高程序可移植性: 由于分段划分的内存区域是按照程序的逻辑结构进行的,操作系统可以在不同的内存位置加载这些段,只要段的内容一致,程序就可以正常运行。

分段技术的缺点:

  1. 外部碎片: 由于段的大小是动态的,可能导致不同段之间留下无法利用的小块空闲内存,从而产生外部碎片。外部碎片会导致内存利用率下降,尤其是在长时间运行时,内存碎片会逐渐增多。

  2. 地址转换开销大: 虽然分段通过逻辑划分内存有其优势,但每次访问时需要通过段表查找对应的段基地址,并加上偏移量进行转换。若段表非常大或访问频繁,可能导致较高的查找和转换开销。

  3. 管理复杂性: 分段技术需要对内存进行复杂的管理,操作系统必须跟踪每个段的基地址、界限和权限,增加了内存管理的复杂性。

分页

Gn!

分页技术的优点:

  1. 消除外部碎片: 分页技术将内存划分为固定大小的页和页框,每页大小相同,避免了由于动态分配不同大小块而产生的外部碎片。分页系统通过按页来管理内存,允许程序使用物理内存中任何空闲的页框,无论其物理位置如何。

  2. 简化内存管理: 分页技术使内存管理更加简单和高效。物理内存的管理不再依赖于程序的逻辑结构,可以使用简单的页表来进行地址转换,不需要像分段那样进行复杂的段管理。

  3. 提高内存利用率: 由于每页大小相同,分页可以高效地利用内存。当内存中存在小的空闲区域时,可以灵活地将这些区域分配给需要的程序,避免了内存浪费。

  4. 支持虚拟内存: 分页技术非常适合实现虚拟内存,因为它允许程序通过页表来映射到物理内存中的任意位置。操作系统可以将程序的某些页面从磁盘交换到内存,支持超大内存空间。

分页技术的缺点:

  1. 内部碎片: 尽管分页消除了外部碎片,但每个页的大小是固定的,如果程序的数据不能整齐地填满一页,可能会造成内部碎片,即每页剩余的空闲空间被浪费。

  2. 地址转换开销: 虽然分页技术能有效管理内存,但每次访问虚拟内存时都需要经过两次地址转换:首先通过页表查找页框地址,然后加上页内偏移量。这增加了额外的计算开销,尤其是在访问非常频繁的情况下。

  3. 页表的存储和管理: 操作系统需要维护页表,并且随着程序的增长,页表的大小可能会非常庞大。为了提高效率,现代系统通常使用多级页表,这样虽然能减少内存开销,但会增加管理的复杂性。

分段与分页的结合

Gn!

在现代操作系统中,分段和分页常常结合使用,形成分页-分段混合模式。这种模式试图结合两者的优点,弥补它们的缺陷。

结合方式:

  1. 分段后分页: 在这种模式下,首先将虚拟地址空间划分为多个段,每个段代表程序的一个逻辑单元(例如代码段、数据段等)。然后,对于每个段,使用分页技术进一步划分为固定大小的页。每个段会有一个段表,用来存储该段的基地址和访问权限等信息。而每个段内部又有自己的页表,负责将虚拟页面映射到物理页面。

  2. 两级地址转换: 当程序访问虚拟内存时,首先通过段表查找段的基地址,然后再通过段内的页表查找物理地址。即:

    1. 第一步:根据段号从段表中获取段的基地址。

    2. 第二步:根据段内的页号从页表中获取对应的物理页框地址,最后加上偏移量得到物理地址。

结合后的优势:

  1. 消除外部碎片:分页技术消除了内存中的外部碎片。分段技术提供灵活的内存划分和保护机制,允许操作系统以更合适的方式管理内存。

  2. 提高内存的使用效率:通过将分段和分页结合使用,可以充分利用内存空间。分段技术按逻辑结构划分内存,而分页技术则提供了一种统一的、固定大小的内存管理方式,避免了外部碎片并提高内存的使用效率。

  3. 提高地址空间的灵活性:分页-分段混合模式让操作系统能够灵活地管理虚拟内存和物理内存。每个程序的虚拟地址空间被分为多个段,段内再细分为固定大小的页,程序访问时,能够快速定位到物理地址。

  4. 便于保护和访问控制:分段提供了访问权限控制,每个段可以独立设置访问权限(只读、只写、可执行等)。同时,分页使得虚拟内存的访问更加灵活,可以将程序的各个部分映射到不同的物理位置,避免内存访问冲突。

The End