Linux之Kernel(1)系统基础理论(5)
Author: Once Day Date: 2025年3月4日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: Linux内核知识_Once-Day的博客-CSDN博客
参考文章:
文章目录
1. 内存管理
1.1 介绍
内存管理是操作系统的核心功能之一,其主要目标是支持多个应用程序同时运行,并提供内存保护和重定位机制。内存保护确保各个应用程序之间相互隔离,防止非法访问;而重定位使得程序可以在内存中自由移动,而无需修改代码。
操作系统引入了地址空间的抽象概念。每个进程都拥有独立的虚拟地址空间,其中包含了代码、数据、堆栈等区域。这种抽象屏蔽了物理内存的细节,简化了程序的编写和内存管理。程序可以在自己的地址空间内自由访问,而不必关心具体的物理内存分配。
为了实现地址空间的映射和转换,硬件芯片提供了基址寄存器和变址寄存器。基址寄存器存储进程地址空间的起始地址,变址寄存器用于计算相对偏移量。二者结合,可以将虚拟地址转换为物理地址,从而访问真实的内存单元。
此外,为了突破物理内存的容量限制,操作系统还采用了虚拟内存技术。虚拟内存通过交换(Swapping)和分页(Paging)机制,将暂时不使用的内存页面交换到外部存储设备(如硬盘),需要时再加载回内存。这种方式使得程序可以使用超过物理内存大小的地址空间,提高了内存利用率和系统性能。
内存管理的主要功能包括:
- 内存空间的分配与回收:操作系统需要记录内存的使用状态,跟踪哪些区域已分配给进程,哪些处于空闲状态。当进程运行结束时,操作系统要及时回收其占用的内存空间,以供其他进程使用。
- 内存空间的扩充:通过虚拟内存技术或自动覆盖技术,操作系统可以在逻辑上扩充内存容量,使得系统能够运行比物理内存大得多的程序。这种方式突破了物理内存的限制,提高了系统的灵活性和适应性。
- 地址转换:为了简化程序编写,程序员通常使用逻辑地址来访问指令和数据。操作系统负责将逻辑地址转换为实际的物理地址,这个过程称为地址重定位。地址重定位有三种常见方式,操作系统根据系统架构和需求选择适当的方式实现。
- 内存保护:操作系统需要确保各个进程在各自的存储空间内运行,互不干扰。通过设置访问权限和边界检查等机制,操作系统防止进程越权访问其他进程的内存区域,保障系统的稳定性和安全性。
1.2 内存保护
内存保护是操作系统的一项重要功能,旨在确保每个进程都有独立的内存空间,防止进程之间的相互干扰和非法访问。内存保护机制主要有两种实现方式:使用上下限寄存器和使用重定位寄存器与界地址寄存器。
上下限寄存器:在CPU中设置一对上限寄存器和下限寄存器,分别存储用户进程在内存中的最高地址和最低地址。每当CPU访问一个内存地址时,会将该地址与上下限寄存器的值进行比较。如果地址超出了上下限寄存器规定的范围,就表示发生了越界访问,操作系统会进行相应的处理,如终止进程或报错。
重定位寄存器和界地址寄存器:重定位寄存器(基地址寄存器)存储进程内存空间的起始物理地址。界地址寄存器(限长寄存器)存储进程逻辑地址空间的最大值。进程使用逻辑地址访问内存时,内存管理机构会将逻辑地址与界地址寄存器的值进行比较。如果逻辑地址未超过界地址寄存器的值,则将逻辑地址加上重定位寄存器的值,得到对应的物理地址。如果逻辑地址超过了界地址寄存器的值,则表示发生了越界访问,操作系统会采取相应的措施。
使用重定位寄存器和界地址寄存器的优点在于:
- 重定位寄存器允许进程在内存中移动,而不需要修改进程内部的逻辑地址。操作系统可以通过修改重定位寄存器的值来改变进程的实际物理地址空间。
- 界地址寄存器提供了一种简单有效的方法来检查地址越界,防止进程访问其他进程的内存空间。
为了确保内存保护的有效性,加载重定位寄存器和界地址寄存器必须使用特权指令,只有操作系统内核才有权限执行这些指令。这样可以防止用户进程篡改这两个寄存器的值,确保内存保护机制的可靠性。
1.3 内存共享
内存共享是一种提高内存利用率和减少内存消耗的技术,它允许多个进程共享某些内存区域,而不是为每个进程分配单独的内存空间。但并非所有的内存区域都适合共享,只有那些只读的、不会被修改的代码和数据才能被安全地共享。
可重入代码(纯代码)是一种特殊的代码,它可以被多个进程同时访问,但不允许被任何进程修改。可重入代码通常是只读的,不依赖于全局变量或静态数据,只使用局部变量和函数参数来完成计算。这种特性使得可重入代码可以安全地在多个进程之间共享,而不会引起数据竞争或一致性问题。
在实际运行时,每个进程都有自己的私有数据段,用于存储进程特有的数据和变量。进程可以自由地读写自己的私有数据段,但不能修改共享的代码段。这种方式既保证了进程的独立性,又实现了内存的共享和节省。
以一个支持40个用户同时运行文本编辑程序的系统为例:
- 如果每个用户都需要一份独立的代码和数据副本,则总共需要8000KB的内存空间(160KB代码+40KB数据per用户)。
- 如果将160KB的代码设计为可重入代码并在所有用户之间共享,则总内存需求减少为1760KB(160KB共享代码+40KB私有数据per用户)。
对于分页系统:
- 假设页面大小为4KB,代码区占用40个页面,数据区占用10个页面。
- 为了实现代码共享,每个进程的页表都要建立40个指向共享代码区物理页面的页表项。
- 同时,每个进程还需要为自己的私有数据区建立10个指向相应物理页面的页表项。
对于分段系统:
- 由于分段是以段为单位进行分配和管理的,所以共享一个代码段非常简单。
- 无论代码段有多大,只需要在每个进程的段表中建立一个指向共享代码段起始地址和长度的段表项即可。
内存共享技术充分利用了程序的局部性原理和代码的只读特性,通过让多个进程共享相同的物理内存页面或段,减少了内存的重复占用,提高了内存的利用效率。同时,内存共享也简化了进程的创建和加载过程,因为共享的代码只需要加载一次,后续的进程可以直接使用,节省了时间和空间开销。
1.4 内存交换
内存交换(Swapping)是一种在多道程序环境下扩充内存的技术,其基本思想是将暂时不能运行的进程从内存移到辅存(如磁盘),以腾出内存空间给其他准备执行的进程使用。当被换出的进程需要重新执行时,再将其从辅存换入内存。
内存交换的过程可以描述为:
- 换出(Swap-out):将处于等待状态或被剥夺运行权的进程从内存移到辅存,释放内存空间。
- 换入(Swap-in):将准备竞争CPU运行的进程从辅存移到内存,占用之前释放的内存空间。
举个例子,假设有一个采用时间片轮转调度算法的多道程序系统。当一个进程的时间片用完时,内存管理器将其换出内存,同时将另一个进程换入刚刚释放的内存空间。这样,CPU调度器就可以将时间片分配给其他已在内存中的进程,实现并发执行。理想情况下,如果内存管理器的交换速度足够快,总会有进程在内存中可以执行,从而提高CPU的利用率。
内存交换技术需要考虑以下几个问题:
- 备份存储:需要足够大的辅存空间(通常是磁盘)来存储换出的进程,并提供对这些内存映像的直接访问。
- 执行时间:为了有效利用CPU,每个进程的执行时间应该比交换时间长。
- 进程状态:换出进程时,必须确保该进程完全处于空闲状态,以避免数据一致性问题。
- 交换空间:通常作为磁盘的一整块独立于文件系统,以提高交换速度。
- 启动时机:交换通常在系统负荷较高、内存紧张时启动,在负荷降低时暂停。
需要注意的是,内存交换主要发生在不同进程或作业之间,而覆盖技术则用于同一个程序或进程内部。对于主存无法完全容纳用户程序的问题,现代操作系统通过虚拟内存技术来解决,覆盖技术已成为历史。但交换技术的某些变体在许多现代系统(如UNIX)中仍发挥着重要作用。
1.5 内存分配
内存分配是操作系统中非常重要的一部分,它决定了如何将内存空间分配给各个进程使用。连续分配方式是指为一个用户程序分配一个连续的内存空间,包括单一连续分配、固定分区分配和动态分区分配三种方式。
(1)单一连续分配:内存被划分为系统区和用户区两部分,系统区存放操作系统相关数据,用户区存放用户进程相关数据。内存中只能有一道用户程序,用户程序独占整个用户区空间。
优点:实现简单,无外部碎片,可采用覆盖技术扩充内存,无需内存保护。
缺点:只适用于单用户、单任务系统,有内部碎片,存储器利用率低。
(2)固定分区分配:将用户空间划分为若干个固定大小的分区,每个分区只装入一道作业。分区大小可以相等或不等,不等的情况下可划分为多个小分区、适量中等分区和少量大分区。使用分区使用表来管理各分区的起始地址、大小和状态(是否已分配)。
优点:实现简单,无外部碎片。
缺点:可能出现程序太大无法装入的情况,需要采用覆盖技术,会产生内部碎片,内存利用率低。
(3)动态分区分配:不预先划分内存分区,而是在进程装入内存时根据进程大小动态建立分区。系统分区的大小和数目是可变的,可以更好地适应进程的需求。随着时间推移,可能产生越来越多的外部碎片,需要通过紧凑技术来解决。
分配策略有首次适应、最佳适应、最坏适应和邻近适应等算法。
- 首次适应:从低地址开始查找,找到第一个满足大小的空闲分区。
- 最佳适应:优先使用更小的分区,以保留更多大分区。
- 最坏适应:优先使用更大的分区,以防止产生太小的不可用碎片。
- 邻近适应:每次从上次查找结束位置开始查找。
算法 | 算法思想 | 分区排列顺序 | 优点 | 缺点 |
---|---|---|---|---|
首次适应 | 从头到尾找适合的分区 | 空闲分区以地址递增次序排列 | 性能最好 算法开销小 | |
最佳适应 | 优先使用更小的分区 | 空闲分区以容量递增次序排列 | 保留更大分区 | 产生大量碎小的外部碎片;算法开销大 |
最坏适应 | 优先使用更大的分区 | 空闲分区以容量递减次序排列 | 减少难以利用的碎片 | 大分区容易被用完;算法开销大 |
邻近适应 | 每次从上次查找结束位置开始查找 | 空闲分区以地址递增次序排列(可排列成循环链表) | 空闲分区有相同概率被使用,算法开销小 | 使高地址大分区也被用完 |
这三种连续分配方式各有优缺点,适用于不同的场景。单一连续分配简单易实现,但只适用于单用户、单任务系统;固定分区分配相对简单,但可能出现内存利用率低的问题;动态分区分配灵活性更高,但需要采取措施应对外部碎片问题。
2. 程序执行
2.1 程序执行流程
在操作系统中,程序的执行需要经过一系列步骤,包括编译、链接、装入和地址重定位。这个过程将用户源代码转换为可在内存中执行的机器指令,并为程序分配合适的内存空间。
编译阶段:编译程序将用户编写的高级语言源代码转换为机器语言的目标模块。每个目标模块都有自己独立的逻辑地址空间,起始地址为0。
链接阶段:链接程序将编译生成的目标模块以及所需的库函数链接在一起,形成一个完整的可执行模块。链接程序按照各个模块的相对地址,构建统一的逻辑地址空间。
装入阶段:装入程序将链接后的可执行模块加载到内存中,准备执行。不同进程的逻辑地址可能相同,但它们会映射到内存的不同物理位置。
地址重定位:在程序执行时,需要将逻辑地址转换为实际的物理地址。地址重定位可以在装入时完成,也可以推迟到程序真正执行时进行(动态重定位)。地址重定位确保程序可以正确访问内存中的指令和数据。
在这个过程中,用户程序和程序员只需关注逻辑地址,无需了解内存管理的具体机制。操作系统负责管理内存空间,为每个进程分配独立的内存区域,并通过地址重定位将逻辑地址映射到物理地址。
2.2 程序的链接
程序链接是将编译后的目标模块和所需的库函数组合成一个完整可执行文件的过程。根据链接的时间和方式,可以将链接分为三种类型:静态链接、装入时动态链接和运行时动态链接。
静态链接:在程序运行之前,将各目标模块及其所需的库函数连接成一个完整的可执行文件(装入模块)。链接后的可执行文件是一个独立的整体,不再依赖于外部的目标模块或库函数。静态链接的优点是执行效率高,因为所有的代码都在同一个文件中。缺点是可执行文件较大,且更新或修改库函数需要重新编译和链接整个程序。
装入时动态链接:将目标模块的链接过程推迟到程序装入内存时进行。在装入内存时,边装入边进行链接,将目标模块和所需的库函数连接起来。装入时动态链接的优点是可以减小可执行文件的大小,因为库函数可以在多个程序之间共享。缺点是链接过程发生在程序运行之前,会增加程序的启动时间。
运行时动态链接:将链接过程进一步推迟到程序执行过程中。当程序执行到需要某个目标模块时,才对其进行链接。运行时动态链接的优点是程序可以动态地加载和卸载模块,提高了程序的灵活性和可扩展性。同时,由于模块可以在多个程序之间共享,内存利用率也得到提高。缺点是链接发生在程序运行期间,可能会影响程序的执行效率。
这三种链接方式各有优缺点,适用于不同的场景。静态链接适合于稳定的、独立的程序,装入时动态链接适合于需要共享库函数的程序,而运行时动态链接适合于需要动态加载和更新模块的程序。
2.3 程序的加载
程序装入是将编译和链接后的程序模块加载到内存中,准备执行的过程。根据装入的时间和方式,可以将程序装入分为三种类型:绝对装入、可重定位装入和动态运行时装入。
绝对装入:在编译和链接阶段,程序模块已经直接使用了绝对地址。装入时,程序模块按照预定的绝对地址直接加载到内存中,绝对装入的优点是装入速度快,执行效率高,因为不需要进行地址转换。缺点是程序必须装入到指定的内存位置,不够灵活,且不同程序之间可能发生地址冲突。
可重定位装入:在装入时对程序模块的地址进行重定位,将逻辑地址转换为实际的物理地址。地址转换在装入时一次性完成,之后程序使用物理地址执行。可重定位装入的优点是不同程序可以装入到内存的不同位置,避免了地址冲突。
静态重定位的特点:程序装入内存时,必须分配其要求的全部内存空间。如果内存不足,程序无法装入。程序装入后,在运行期间不能移动或申请新的内存空间。
动态运行时装入:装入程序将程序模块装入内存后,并不立即进行地址转换,而是将地址转换推迟到程序实际执行时进行。程序装入后,所有的地址仍然是逻辑地址,需要在运行时通过重定位寄存器进行动态转换。
动态重定位的特点:程序可以分配到不连续的内存区域中。程序运行前只需装入部分代码即可开始执行。在程序运行期间,可以根据需要动态申请和分配内存。有利于程序段的共享,提供更大的地址空间。允许程序在内存中移动。
现代操作系统通常采用动态运行时装入的方式,结合虚拟内存和分页机制,提供更加灵活和高效的内存管理。
2.4 内存映像
进程的内存映像是指程序在内存中的布局和组织方式。当一个程序被调入内存并开始运行时,它的各个部分被映射到内存的不同区域,构成了进程的内存映像。一个典型的进程内存映像包括以下几个部分:
代码段(Code Segment):包含程序的二进制代码,即编译后的机器指令。代码段是只读的,可以被多个进程共享,节省内存空间。代码段包括程序的初始化代码(.init)、用户程序代码(.text)和只读数据(.rodata)。
数据段(Data Segment):包含程序运行时使用和修改的数据,如全局变量和静态变量。数据段在程序加载时分配,大小固定。数据段包括已初始化的全局变量和静态变量(.data)以及未初始化或初始化为0的全局变量和静态变量(.bss)。
堆(Heap):用于存储程序运行时动态分配的内存。通过malloc、free等函数动态申请和释放内存。堆在内存中从低地址向高地址增长。
栈(Stack):用于实现函数调用和局部变量的存储。每次函数调用时,栈会向低地址方向增长,存储函数的返回地址、局部变量等。函数返回时,栈会收缩,释放不再使用的内存空间。
共享库(Shared Libraries):存储进程使用的共享函数库代码,如C标准库函数(printf等)。共享库可以被多个进程共享,减少内存的消耗。
进程控制块(Process Control Block, PCB):存储在系统区,用于操作系统对进程的控制和管理。PCB包含进程的状态信息、资源分配情况、优先级等元数据。
进程的内存映像分布如下图所示:
High Address
+-----------------------+
| Stack |
+-----------------------+
| Heap |
+-----------------------+
| Uninitialized Data |
+-----------------------+
| Initialized Data |
+-----------------------+
| Read-only Data |
+-----------------------+
| Text |
+-----------------------+
| Shared Libraries |
+-----------------------+
Low Address
这种内存布局方式有助于实现模块化、安全性和高效的内存管理。代码段和只读数据可以被多个进程共享,节省内存空间;数据段和堆允许进程存储和修改数据;栈支持函数调用和局部变量的使用;共享库提供了代码复用的机制。
操作系统通过进程控制块来管理和调度进程,合理分配内存资源。同时,不同的内存区域有不同的访问权限,如代码段是只读的,防止进程意外修改代码,提高了系统的安全性和稳定性。
3. 内存管理实现
3.1 分页式内存管理
分页存储管理是为了解决固定分区和动态分区内存管理中的碎片问题而提出的一种内存管理方式。它将主存空间和进程的地址空间都划分为大小相等且固定的块,称为页帧和页面。通过建立页表来记录进程页面和实际存放的内存块之间的映射关系,实现了进程的离散分配和内存的高效利用。
分页存储管理的基本概念:
页面(Page):进程中的块,是进程地址空间的基本单位。
页框/页帧(Page Frame):内存中的块,是主存的基本分配单位。
块或盘块(Block):外存也以同样的单位进行划分,直接称为块或盘块。
页表(Page Table):记录进程页面和实际存放的内存块之间映射关系的数据结构。
页内偏移量(Page Offset):页内的相对地址,与页号一起构成逻辑地址。
页框=页帧=内存块=物理块=物理页面
分页存储管理消除了外部碎片,只在最后一页产生少量的内部碎片(页内碎片)。
为方便地址转换,页面大小应是2的整数幂。同时页面大小应该适中,
- 页面太小会使进程的页面数过多,这样页表就会过长,占用大量内存,而且也会增加硬件地址转换的开销,降低页面换入/换出的效率。
- 页面过大又会使页内碎片增多,降低内存的利用率。
分页存储管理的逻辑地址结构如下所示:
| 31 12|11 0|
| 页号P | 页内偏移量W |
地址结构包含两个部分:前一部分为页号,后一部分为页内偏移量 W。
在上图所示的例子中,地址长度为 32 位,其中 0 ~ 11位 为页内偏移量(或称页内地址),即每页大小为 4KB;12~31 位为页号,进程地址空间最多允许 2^20 页。
为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。页表通常存在 PCB (进程控制块,在操作系统的内核地址空间)中。页表记录进程页面和实际存放的内存块之间的映射关系。
地址结构和地址转换:
- 逻辑地址由页号和页内偏移量两部分组成。
- 通过页号在页表中查找对应的块号,再加上页内偏移量得到物理地址。
- 页号 = 逻辑地址 / 页面长度。
- 页内偏移量 = 逻辑地址 % 页面长度。
3.2 分页式内存地址转换
分页式内存管理中,地址转换是将进程的逻辑地址转换为实际的物理地址的过程。这个过程由操作系统和硬件结合完成,通过查询进程的页表实现。
(1)准备工作,系统为每个进程建立一张页表,记录进程页面和内存块的对应关系。设置页表寄存器(PTR),存放页表在内存中的起始地址F和页表长度M。确定页面大小L,即进程地址空间和内存空间的划分粒度。
(2)计算页号P和页内偏移量W,逻辑地址A可以划分为页号P和页内偏移量W两部分。页号P=A/L,表示逻辑地址A所在的页面编号。页内偏移量W=A%L,表示逻辑地址A在该页面内的相对位置。计算机硬件可以根据逻辑地址的二进制表示快速得到P和W。
(3)判断页号是否越界:比较页号P和页表长度M,判断P是否小于M。若P≥M,则产生越界中断,停止地址转换过程。若P<M,则继续执行后续步骤。
(4)查页表,获取内存块号:根据页号P计算对应页表项的地址:F+P*页表项长度
。从该地址取出页表项内容b,即为页号P对应的内存块号。
(5)计算物理地址E:物理地址E=b*L+W
,即内存块号b和页内偏移量W的组合。若b和W均为二进制表示,则直接拼接得到最终物理地址。
(6)访问目标内存单元:使用物理地址E去访问内存,完成一次地址转换过程。
快表(TLB)是一种用于加速地址转换的硬件机制,它通过缓存最近访问的页表项来减少访问内存页表的次数,从而提高地址转换的效率。
快表查找时的处理,将逻辑地址的页号与快表中的所有页号进行比较。如果找到匹配的页号,说明要访问的页表项在快表中有副本,称为快表命中(TLB hit)。如果没有找到匹配的页号,说明要访问的页表项不在快表中,称为快表缺失(TLB miss)。
快表命中时的处理,从快表中读取匹配页号对应的页表项,获得该页面在物理内存中的块号。将块号与页内偏移量拼接,形成物理地址。使用物理地址直接访问内存,完成一次数据存取。快表命中时,数据存取只需一次访存。
快表缺失时的处理,访问内存中的页表,查找逻辑地址对应的页表项。从页表项中获取该页面在物理内存中的块号。将块号与页内偏移量拼接,形成物理地址。使用物理地址访问内存,完成一次数据存取。同时,将访问的页表项添加到快表中,以便后续可能的再次访问。快表缺失时,数据存取需两次访存(一次访问页表,一次访问数据)。
快表替换时的处理,当快表已满,又需要添加新的页表项时,需要按照一定的替换算法选择一个页表项进行替换。常见的替换算法有随机替换、先进先出(FIFO)、最近最少使用(LRU)等。替换算法的目标是尽可能保留近期频繁访问的页表项,提高快表的命中率。
快表机制可以显著减少访问内存页表的次数,加速地址转换过程。由于快表的访问速度远快于内存,因此快表命中时的地址转换开销很小。只有在快表缺失时,才需要访问内存中的页表,而页表项被访问后也会被添加到快表中,供后续使用。
多级页表也是一种优化内存管理的技术,它通过将页表划分为多个层级,解决了单级页表存在的一些问题。在单级页表中,整个页表需要连续存储在内存中,当页表很大时,会占用大量连续的内存空间,造成浪费。同时,整个页表常驻内存,但进程可能只需要访问其中的一部分页面,也会导致内存的低效利用。
为了解决这些问题,多级页表引入了页目录表作为一级页表,用于索引二级页表。二级页表存储实际的页表项,可以离散地分布在内存中。这样,页表不再需要占用连续的内存空间,而且可以根据需要将部分页表调入内存,提高了内存的利用率。
在多级页表中,逻辑地址被划分为多个部分,每一部分对应一级页表。通过逐级查询页表,最终得到目标页面在物理内存中的位置。例如,在两级页表中,逻辑地址被划分为一级页号、二级页号和页内偏移量三部分。通过一级页号查询页目录表,得到二级页表的位置;再通过二级页号查询二级页表,得到最终的物理页面号;最后结合页内偏移量,得到完整的物理地址。
多级页表的引入确实增加了地址转换的复杂性,需要多次访问内存才能完成一次地址转换。为了加快这个过程,通常会使用快表(TLB)缓存最近访问的页表项,减少实际访问内存的次数。
3.3 分段式内存管理
分段式内存管理是一种常见的内存管理方式,它根据程序的逻辑结构,将程序和数据划分为若干个段(Segment),每个段都有自己的名字、大小和属性等,并分别装入内存的不同区域。
分页管理方式是从计算机的角度考虑设计的,目的是提高内存的利用率,提升计算机的性能。分页通过硬件机制实现,对用户完全透明。 分段管理方式的提出则考虑了用户和程序员,以满足方便编程、信息保护和共享、动态增长及动态链接等多方面的需要。
分段管理以程序的自然段为单位进行划分和管理,比如主程序、子程序、栈段、数据段等,这更符合程序的逻辑结构,也方便程序员进行模块化设计。段内要求地址连续,而段间则不要求连续,所以每个作业的地址空间是二维的,由段号和段内偏移量共同决定,提供了更灵活方便的寻址方式。
在页式系统中,逻辑地址的页号和页内偏移量对用户是透明的,但在段式系统中,段号和段内偏移量必须由用户显式提供,在高级程序设计语言中,这个工作由编译程序完成。
通过在段表中为每个段设置存取权限等属性,可以很容易地实现对程序和数据的保护与共享。多个进程可以共享某些代码段或数据段,只需在它们的段表中让相应表项指向同一物理块即可。而对于不能共享的数据段,则禁止其他进程访问。同时,由于每个段都有自己的长度属性,也很容易实现越界检查,防止进程访问本段以外的存储空间。
分段管理还能方便地支持动态链接、动态增长等机制。比如当某个段不够用时,操作系统可以为其另行分配一个更大的存储区域,只需修改段表,而不影响程序的执行。再比如当某个模块需要调用另一个独立编译的模块时,链接器只需将被调模块的段号填入调用方的段表即可,非常灵活。
分段、分页管理的对比:
存储信息 | 地址空间 | 信息保护 | 访存次数 | |
---|---|---|---|---|
分页管理 | 页是信息的物理单位 对用户透明 系统行为 | 一维 记忆符(A) | 不易 | 分页(单级页表)需两次访问 页表+目标内存单元 |
分段管理 | 段是信息的逻辑单位 对用户可见 用户需求 | 二维 段名+段内地址([D]|) | 容易 纯代码 | 分段需两次访问 段表+目标内存单元 |
3.4 段页式管理
段页式内存管理是一种结合了分段和分页两种方式的存储管理技术,综合了两者的优点。它首先将程序按照逻辑结构划分为若干个段,每个段再进一步划分为大小固定的页。相应地,内存空间也被划分为大小相等的块,用于存放各个页面。
这样,逻辑地址就由三部分组成:段号、页号和页内偏移量。段号的位数决定了每个进程最多可以分几个段,页号位数决定了每个段最大有多少页,页内偏移量决定了页面大小、内存块大小是多少。
在地址转换时,需要先通过段表查询得到该段对应的页表起始地址,再通过页表和页内偏移量最终得到物理地址。具体过程如下:
- 根据逻辑地址得到段号、页号和页内偏移量。
- 判断段号是否越界,如果越界则触发中断。
- 用段号查询段表,得到对应的页表起始地址。如果段表项中设有访问权限位,还需进行权限检查。
- 判断页号是否越界,如果越界则触发中断。
- 用页号查询页表,得到对应的内存块号。
- 将内存块号与页内偏移量拼接,得到最终的物理地址。
- 访问目标内存单元。
可见,段页式系统的地址转换过程比较复杂,需要访问3次内存(段表、页表、数据)。为了提高效率,通常会引入快表机制,用于缓存最近访问过的段表项和页表项。快表的关键字由段号和页号组成,值为对应的内存块号,从而可以一步到位地完成地址映射。
段页式管理集分段与分页的优点于一身。分段管理便于实现内存共享和保护,而分页管理使得每个段的长度可以不等,且不会产生内存碎片。但另一方面,它也继承了二者的缺点,即段表和页表都需要占用内存空间,管理比较复杂。
此外,由于段的长度不确定,而页的大小又是固定的,因此会不可避免地产生一些零头。为了避免过多的内存浪费,通常需要合理地划分段和页的大小。在现代操作系统中,段的大小一般比页要大得多,而页的大小常见的有4KB、8KB等。
分段对用户是可见的,程序员编程时需要显式地给出段号、段内地址。而将各段分页对用户是不可见的。系统会根据段内地址自动划分页号和页内偏移量。因此,段页式管理的地址结构是二维的。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~