文章概述
本篇主要讲解第三章:操作系统的存储管理,其余章节请参考专栏其他篇幅。
目录
三、存储管理
存储管理是操作系统的核心功能之一,负责管理计算机的内存资源。内存是CPU直接访问的存储设备,其管理效率直接影响系统的整体性能。本章节将详细讲解存储管理的各个方面,包括内存分配、地址转换、虚拟存储等核心概念和技术。
3.1 存储管理概述
存储管理是操作系统对内存资源进行管理的功能模块。它负责内存的分配、回收、保护、共享和地址转换等工作,为进程提供运行所需的内存空间。
3.1.1 存储管理的功能
存储管理的定义
存储管理(Memory Management)是操作系统对计算机内存资源进行统一管理和调度的功能模块。它负责管理内存的分配、回收、保护、共享和地址转换等工作。
形象比喻:
如果把内存比作一个大型仓库:
- 内存空间 = 仓库的存储空间
- 进程 = 需要存储货物的客户
- 存储管理 = 仓库管理员(分配空间、记录位置、保护货物)
- 地址转换 = 将客户编号转换为实际货架位置
没有存储管理,内存就像没有管理的仓库,无法有效利用;有了存储管理,内存才能被高效、安全地使用。
存储管理的主要功能
存储管理需要完成以下主要功能:
存储管理功能:
1. 内存分配与回收:
- 为进程分配内存空间
- 进程结束时回收内存
- 管理空闲内存块
2. 地址转换:
- 将逻辑地址转换为物理地址
- 支持重定位
- 实现地址空间管理
3. 内存保护:
- 防止进程越界访问
- 保护系统区域
- 实现访问权限控制
4. 内存共享:
- 多个进程共享同一块内存
- 提高内存利用率
- 支持进程通信
5. 内存扩充:
- 虚拟存储技术
- 将外存作为内存的扩展
- 实现比物理内存更大的地址空间
1. 内存分配与回收
内存分配是存储管理的基本功能,负责为进程分配所需的内存空间。
内存分配与回收:
分配方式:
1. 静态分配:
- 程序编译时确定内存需求
- 程序运行前分配
- 运行期间不能改变
- 简单但不灵活
2. 动态分配:
- 程序运行时确定内存需求
- 运行期间可以申请和释放
- 灵活但复杂
分配策略:
- 连续分配:分配连续的内存块
- 非连续分配:分配不连续的内存块(分页、分段)
回收机制:
- 进程终止时自动回收
- 支持部分回收(动态分配)
- 合并空闲块(减少碎片)
2. 地址转换
地址转换是存储管理的核心功能,将程序的逻辑地址转换为物理内存地址。
地址转换:
逻辑地址(Logical Address):
- 程序中的地址
- 相对于程序起始地址的偏移
- 也称为虚拟地址、相对地址
- 例如:程序中的变量地址
物理地址(Physical Address):
- 内存中的实际地址
- 内存单元的物理位置
- 也称为绝对地址
- 例如:内存条上的实际位置
地址转换过程:
逻辑地址 → 地址转换机构 → 物理地址
转换方式:
1. 静态重定位:
- 程序装入时转换
- 转换后地址固定
- 不能移动程序
2. 动态重定位:
- 程序运行时转换
- 每次访问都转换
- 可以移动程序
- 现代系统使用
地址转换示例:
地址转换示例:
程序代码:
int x = 100; // 假设x的逻辑地址是0x1000
内存布局:
逻辑地址空间(程序视角):
0x0000: 程序起始
0x1000: 变量x ← 逻辑地址
...
物理地址空间(内存视角):
0x5000: 程序实际起始位置
0x6000: 变量x的实际位置 ← 物理地址
...
地址转换:
逻辑地址:0x1000
基址寄存器:0x5000
物理地址 = 逻辑地址 + 基址 = 0x1000 + 0x5000 = 0x6000
3. 内存保护
内存保护确保进程只能访问分配给它的内存区域,防止进程越界访问或破坏系统内存。
内存保护:
保护机制:
1. 界限寄存器:
- 基址寄存器:程序起始地址
- 限长寄存器:程序大小
- 检查地址是否在范围内
2. 访问权限:
- 只读、读写、执行等权限
- 页表或段表中的权限位
- 硬件检查权限
3. 保护模式:
- 用户模式:限制访问
- 内核模式:完全访问
- CPU模式切换
保护检查:
每次内存访问时:
1. 检查地址是否越界
2. 检查访问权限
3. 违规则产生异常
内存保护示例:
内存保护示例:
进程A的内存分配:
基址:0x1000
限长:0x2000
有效范围:0x1000 ~ 0x2FFF
访问检查:
访问地址0x1500:
✓ 0x1500 >= 0x1000(基址)
✓ 0x1500 < 0x3000(基址+限长)
→ 允许访问
访问地址0x5000:
✗ 0x5000 >= 0x3000(超出范围)
→ 产生越界异常,拒绝访问
4. 内存共享
内存共享允许多个进程访问同一块物理内存,提高内存利用率,支持进程间通信。
内存共享:
共享方式:
1. 代码共享:
- 多个进程共享程序代码
- 代码只读,可以共享
- 节省内存空间
2. 数据共享:
- 多个进程共享数据
- 需要同步机制
- 用于进程通信
共享实现:
- 页表映射到同一物理页
- 段表映射到同一物理段
- 共享内存机制
例子:
多个进程运行同一个程序:
进程1、进程2、进程3都运行Word
代码段共享(只读)
数据段独立(每个进程一份)
5. 内存扩充
内存扩充通过虚拟存储技术,使系统能够运行比物理内存更大的程序。
内存扩充:
虚拟存储:
- 将外存(磁盘)作为内存的扩展
- 程序可以比物理内存大
- 部分程序在内存,部分在磁盘
工作原理:
1. 程序分为多个部分(页或段)
2. 需要时装入内存
3. 不需要时换出到磁盘
4. 对用户透明
优点:
- 运行比物理内存大的程序
- 提高内存利用率
- 支持多道程序
缺点:
- 需要额外的地址转换
- 可能产生页面置换开销
存储管理的目标
存储管理目标:
1. 提高内存利用率:
- 减少内存碎片
- 充分利用内存空间
- 支持多道程序
2. 提高系统性能:
- 减少地址转换开销
- 优化内存访问
- 提高程序执行速度
3. 方便用户使用:
- 用户无需关心内存细节
- 自动管理内存
- 提供统一的接口
4. 保证系统安全:
- 内存保护
- 防止越界访问
- 隔离进程内存
3.1.2 程序的装入和链接
程序要运行,需要经过编译、链接和装入三个阶段。存储管理主要关注程序的装入过程,但理解链接过程有助于理解存储管理。
程序的编译、链接和装入
程序执行过程:
源代码(.c, .cpp等)
↓ 编译
目标文件(.o, .obj)
↓ 链接
可执行文件(.exe, .out)
↓ 装入
内存中的程序
↓ 执行
运行中的进程
1. 编译(Compilation)
编译将源代码转换为目标代码。
编译过程:
输入:源代码文件
例如:main.c, utils.c
处理:
1. 词法分析
2. 语法分析
3. 语义分析
4. 代码生成
输出:目标文件
例如:main.o, utils.o
特点:
- 生成相对地址(从0开始)
- 包含未解析的外部引用
- 需要链接才能执行
2. 链接(Linking)
链接将多个目标文件合并成一个可执行文件。
链接过程:
输入:目标文件
main.o, utils.o, lib.o
处理:
1. 地址重定位:
- 合并各个目标文件
- 调整相对地址
- 生成统一的地址空间
2. 符号解析:
- 解析外部引用
- 连接函数调用
- 连接变量引用
3. 库链接:
- 链接系统库
- 链接用户库
输出:可执行文件
program.exe
链接方式:
1. 静态链接:
- 链接时复制库代码
- 可执行文件包含所有代码
- 文件大但独立
2. 动态链接:
- 运行时链接库
- 多个程序共享库
- 文件小但依赖库
链接方式详解
静态链接(Static Linking)
静态链接:
定义:
在链接时将库代码复制到可执行文件中
可执行文件包含所有需要的代码
过程:
目标文件 + 静态库 → 可执行文件
main.o + lib.a → program.exe
特点:
优点:
- 可执行文件独立
- 不需要运行时库
- 执行速度快
缺点:
- 可执行文件大
- 多个程序重复包含库代码
- 更新库需要重新链接
例子:
Windows: .lib文件(静态库)
Linux: .a文件(静态库)
动态链接(Dynamic Linking)
动态链接:
定义:
在运行时链接库代码
多个程序共享同一个库
过程:
链接时:记录库信息
运行时:加载共享库
特点:
优点:
- 可执行文件小
- 多个程序共享库
- 更新库无需重新链接程序
缺点:
- 需要运行时库
- 依赖库版本
- 首次加载稍慢
例子:
Windows: .dll文件(动态链接库)
Linux: .so文件(共享对象)
3. 装入(Loading)
装入将可执行文件从磁盘加载到内存,创建进程。
装入过程:
输入:可执行文件
program.exe
处理:
1. 分配内存空间
2. 将程序代码和数据装入内存
3. 设置初始状态(寄存器、栈等)
4. 创建进程控制块(PCB)
输出:内存中的进程
进程开始执行
装入方式:
1. 绝对装入
2. 可重定位装入
3. 动态运行时装入
装入方式详解
1. 绝对装入(Absolute Loading)
绝对装入:
定义:
程序编译时就知道内存中的位置
装入时直接放到指定位置
特点:
- 地址在编译时确定
- 装入简单快速
- 不能移动程序
问题:
- 需要知道内存布局
- 程序不能重定位
- 不适合多道程序
使用场景:
- 单道程序系统
- 嵌入式系统
- 固定内存布局
例子:
程序编译时指定地址0x1000
装入时直接放到0x1000
不能改变位置
2. 可重定位装入(Relocatable Loading)
可重定位装入:
定义:
程序使用相对地址
装入时根据实际位置调整地址
特点:
- 地址在装入时确定
- 可以放在任意位置
- 需要地址重定位
重定位方式:
1. 静态重定位:
- 装入时一次性转换所有地址
- 转换后地址固定
- 程序不能移动
2. 动态重定位:
- 运行时转换地址
- 使用基址寄存器
- 程序可以移动
优点:
- 灵活,可以放在任意位置
- 支持多道程序
缺点:
- 需要重定位信息
- 动态重定位需要硬件支持
可重定位装入示例:
可重定位装入示例:
可执行文件(相对地址):
0x0000: 程序起始
0x0100: 函数A
0x0200: 数据区
...
装入到内存0x5000:
静态重定位(装入时转换):
0x5000: 程序起始(原0x0000)
0x5100: 函数A(原0x0100)
0x5200: 数据区(原0x0200)
...
所有地址都加上0x5000
动态重定位(运行时转换):
基址寄存器 = 0x5000
访问0x0100时:
物理地址 = 0x0100 + 0x5000 = 0x5100
程序仍使用相对地址0x0100
硬件自动加上基址
3. 动态运行时装入(Dynamic Run-time Loading)
动态运行时装入:
定义:
程序运行时才装入需要的部分
支持程序比内存大
特点:
- 按需装入
- 支持虚拟存储
- 可以换入换出
工作原理:
1. 程序分为多个部分(页或段)
2. 初始只装入部分代码
3. 需要时装入其他部分
4. 不需要时可以换出
优点:
- 支持大程序
- 提高内存利用率
- 支持多道程序
缺点:
- 需要额外的管理
- 可能有换入换出开销
现代系统:
- 所有现代操作系统都使用
- 配合虚拟存储技术
- 对用户透明
装入过程详细步骤
装入过程详细步骤:
步骤1:检查可执行文件
- 验证文件格式
- 检查权限
- 读取文件头信息
步骤2:分配内存空间
- 根据程序大小分配内存
- 检查是否有足够空间
- 选择合适的内存区域
步骤3:装入程序代码
- 从磁盘读取代码段
- 写入分配的内存
- 设置代码段属性
步骤4:装入程序数据
- 从磁盘读取数据段
- 写入分配的内存
- 初始化数据(全局变量等)
步骤5:设置运行环境
- 设置程序计数器(PC)
- 设置栈指针(SP)
- 设置基址寄存器(如需要)
步骤6:创建进程
- 创建进程控制块(PCB)
- 设置进程状态为就绪
- 加入就绪队列
步骤7:开始执行
- 进程调度
- 开始执行程序
链接和装入的关系
链接和装入的关系:
编译阶段:
源代码 → 目标文件(相对地址)
链接阶段:
目标文件 → 可执行文件(相对地址,但已统一)
装入阶段:
可执行文件 → 内存中的程序(物理地址)
地址转换:
编译时:符号 → 相对地址
链接时:相对地址 → 统一相对地址
装入时:相对地址 → 物理地址(重定位)
现代系统:
- 链接时:生成相对地址
- 装入时:建立地址映射(页表/段表)
- 运行时:动态地址转换
3.1.3 连续分配方式
连续分配是指为进程分配连续的内存空间。这是最简单的内存分配方式,但容易产生内存碎片。
连续分配的基本概念
连续分配:
定义:
为进程分配一块连续的内存空间
进程的所有代码和数据都在连续的内存中
特点:
- 简单易实现
- 地址转换简单(基址+偏移)
- 容易产生碎片
分配方式:
1. 单一连续分配
2. 固定分区分配
3. 动态分区分配
1. 单一连续分配(Single Contiguous Allocation)
单一连续分配是最简单的分配方式,整个内存除了系统区外,全部给一个用户程序使用。
单一连续分配:
内存布局:
┌─────────────────┐
│ 操作系统 │ 系统区
├─────────────────┤
│ │
│ 用户程序 │ 用户区
│ (一个程序) │
│ │
└─────────────────┘
特点:
- 内存分为系统区和用户区
- 用户区只能运行一个程序
- 简单但内存利用率低
优点:
- 实现简单
- 管理开销小
- 无外部碎片
缺点:
- 内存利用率低
- 不支持多道程序
- 程序大小受限制
使用场景:
- 早期单道批处理系统
- 嵌入式简单系统
- 已很少使用
2. 固定分区分配(Fixed Partition Allocation)
固定分区分配将内存划分为多个固定大小的分区,每个分区可以运行一个程序。
固定分区分配:
内存布局:
┌─────────────────┐
│ 操作系统 │
├─────────────────┤
│ 分区1(固定大小)│
├─────────────────┤
│ 分区2(固定大小)│
├─────────────────┤
│ 分区3(固定大小)│
├─────────────────┤
│ 分区4(固定大小)│
└─────────────────┘
分区方式:
1. 分区大小相等:
- 所有分区大小相同
- 简单但浪费空间
2. 分区大小不等:
- 不同大小的分区
- 适应不同大小的程序
- 更灵活
分区管理:
- 分区描述表:记录每个分区的状态
- 状态:空闲/已分配
- 大小:分区大小
- 起始地址:分区起始位置
固定分区分配示例:
固定分区分配示例:
内存:64KB
分区设置:
分区1:8KB
分区2:16KB
分区3:32KB
分区4:8KB(剩余)
分区描述表:
分区 大小 起始地址 状态 进程
──────────────────────────────────────
1 8KB 0x1000 已分配 P1
2 16KB 0x3000 空闲 -
3 32KB 0x7000 已分配 P2
4 8KB 0xF000 空闲 -
分配过程:
1. 新进程P3需要12KB
2. 查找合适的分区
3. 找到分区2(16KB >= 12KB)
4. 分配给P3
5. 更新分区描述表
问题:
- 如果程序小于分区,浪费空间(内部碎片)
- 如果程序大于所有分区,无法运行
固定分区分配的特点:
固定分区分配特点:
优点:
1. 实现简单:
- 管理开销小
- 分配算法简单
2. 无外部碎片:
- 分区固定
- 不会产生外部碎片
3. 支持多道程序:
- 多个分区可以同时运行多个程序
缺点:
1. 内部碎片:
- 程序小于分区时浪费空间
- 例如:程序5KB,分区8KB,浪费3KB
2. 灵活性差:
- 分区大小固定
- 不能适应程序大小变化
3. 内存利用率低:
- 内部碎片导致浪费
- 大程序可能无法运行
使用场景:
- 早期多道批处理系统
- 现在很少使用
3. 动态分区分配(Dynamic Partition Allocation)
动态分区分配根据程序的实际大小动态分配内存,分区大小不固定。
动态分区分配:
内存布局(初始):
┌─────────────────┐
│ 操作系统 │
├─────────────────┤
│ 空闲区 │
│ (整个用户区) │
└─────────────────┘
分配后:
┌─────────────────┐
│ 操作系统 │
├─────────────────┤
│ 进程P1 │
├─────────────────┤
│ 空闲区 │
├─────────────────┤
│ 进程P2 │
├─────────────────┤
│ 空闲区 │
└─────────────────┘
特点:
- 分区大小根据程序需要动态确定
- 更灵活,内存利用率高
- 但会产生外部碎片
动态分区分配的数据结构
动态分区管理:
数据结构:
1. 空闲分区表:
- 记录所有空闲分区
- 每个表项:起始地址、大小、状态
2. 空闲分区链:
- 用链表连接空闲分区
- 每个空闲分区有指针指向下一个
空闲分区链示例:
┌──────┬──────┬──────┐
│ 大小 │起始 │ 指针 │ → 下一个空闲分区
└──────┴──────┴──────┘
空闲分区1
管理方式:
- 分配:从空闲分区中分配
- 回收:释放分区,可能合并相邻空闲区
动态分区分配算法
动态分区分配需要选择合适的空闲分区分配给进程,主要有以下几种算法:
1. 首次适应算法(First Fit)
首次适应算法:
算法:
- 从内存低地址开始查找
- 找到第一个满足大小的空闲分区
- 立即分配
优点:
- 实现简单
- 查找速度快
- 低地址分区优先使用
缺点:
- 低地址产生小碎片
- 高地址大分区可能浪费
例子:
空闲分区:10KB(0x1000), 20KB(0x5000), 15KB(0xA000)
需要:12KB
查找:
10KB < 12KB,不满足
20KB >= 12KB,满足,分配
结果:分配0x5000开始的12KB,剩余8KB
2. 最佳适应算法(Best Fit)
最佳适应算法:
算法:
- 查找所有空闲分区
- 选择满足要求且最小的分区
- 减少浪费
优点:
- 减少浪费
- 大分区保留给大程序
缺点:
- 查找速度慢(需要遍历所有)
- 容易产生小碎片
例子:
空闲分区:10KB, 20KB, 15KB
需要:12KB
查找:
10KB < 12KB,不满足
20KB >= 12KB,候选
15KB >= 12KB,候选,更小
结果:分配15KB分区,剩余3KB
3. 最坏适应算法(Worst Fit)
最坏适应算法:
算法:
- 查找所有空闲分区
- 选择满足要求且最大的分区
- 避免产生小碎片
优点:
- 避免产生小碎片
- 中等大小程序容易分配
缺点:
- 查找速度慢
- 大分区被分割,大程序可能无法运行
例子:
空闲分区:10KB, 20KB, 15KB
需要:12KB
查找:
10KB < 12KB,不满足
20KB >= 12KB,候选,最大
15KB >= 12KB,候选
结果:分配20KB分区,剩余8KB
4. 邻近适应算法(Next Fit)
邻近适应算法:
算法:
- 从上次分配的位置继续查找
- 找到第一个满足大小的空闲分区
- 循环查找
优点:
- 查找速度较快
- 分配相对均匀
缺点:
- 可能跳过更合适的分区
- 内存利用率可能不如首次适应
例子:
上次分配位置:0x5000
空闲分区:10KB(0x1000), 20KB(0x5000), 15KB(0xA000)
需要:12KB
从0x5000开始查找:
20KB >= 12KB,满足,分配
结果:分配0x5000开始的12KB
动态分区分配算法对比
动态分区分配算法对比:
算法 查找速度 内存利用率 碎片情况
─────────────────────────────────────────
首次适应 快 中等 低地址小碎片
最佳适应 慢 较高 小碎片多
最坏适应 慢 较低 大分区被分割
邻近适应 较快 中等 相对均匀
实际使用:
- 首次适应最常用
- 实现简单,性能较好
- 现代系统很少使用连续分配
分区回收
当进程结束时,需要回收其占用的分区,并可能合并相邻的空闲分区。
分区回收:
回收情况:
1. 回收区前后都空闲:
- 合并三个分区为一个
- 更新空闲分区表
2. 回收区前空闲:
- 与前一个空闲区合并
- 更新前一个空闲区大小
3. 回收区后空闲:
- 与后一个空闲区合并
- 更新后一个空闲区起始地址和大小
4. 回收区前后都不空闲:
- 创建新的空闲分区
- 加入空闲分区表
例子:
内存布局:
[P1][空闲10KB][P2][空闲15KB][P3]
P2结束,回收:
情况:前后都空闲
合并:[P1][空闲10KB+20KB+15KB=45KB][P3]
连续分配的问题:碎片
连续分配方式的主要问题是会产生内存碎片。
内存碎片:
1. 内部碎片(Internal Fragmentation):
定义:
- 分配给进程的内存大于实际需要
- 分区内未使用的空间
产生原因:
- 固定分区分配
- 程序小于分配的分区
例子:
分配8KB分区,程序只需5KB
浪费3KB(内部碎片)
2. 外部碎片(External Fragmentation):
定义:
- 内存中有很多小的空闲分区
- 每个都太小,无法满足需求
- 但总和可能足够
产生原因:
- 动态分区分配
- 进程分配和回收
- 分区大小不匹配
例子:
空闲分区:3KB, 5KB, 2KB, 4KB
需要:10KB
每个都不够,但总和14KB足够
无法分配(外部碎片)
解决碎片的方法:
1. 紧凑(Compaction):
- 移动进程,合并空闲区
- 需要动态重定位支持
- 开销大
2. 非连续分配:
- 分页、分段
- 允许程序不连续存放
- 现代系统使用
紧凑技术(Compaction)
紧凑技术:
定义:
移动内存中的进程
合并分散的空闲分区
形成大的连续空闲区
过程:
1. 暂停所有进程
2. 移动进程到内存一端
3. 更新进程的基址寄存器
4. 合并空闲分区
5. 恢复进程执行
例子(紧凑前):
[P1][空闲3KB][P2][空闲5KB][P3][空闲2KB]
紧凑后:
[P1][P2][P3][空闲10KB]
优点:
- 解决外部碎片
- 提高内存利用率
缺点:
- 开销大(需要移动数据)
- 需要动态重定位
- 影响系统性能
使用场景:
- 外部碎片严重时
- 需要大块连续内存时
- 现代系统很少使用(用非连续分配代替)
连续分配的总结
连续分配总结:
分配方式:
1. 单一连续分配:
- 最简单
- 只支持单道程序
- 已淘汰
2. 固定分区分配:
- 支持多道程序
- 有内部碎片
- 灵活性差
3. 动态分区分配:
- 灵活
- 有外部碎片
- 需要紧凑技术
共同特点:
- 程序必须连续存放
- 地址转换简单(基址+偏移)
- 容易产生碎片
现代系统:
- 很少使用连续分配
- 主要使用非连续分配(分页、分段)
- 避免碎片问题
3.2 分页存储管理
分页存储管理是现代操作系统最常用的内存管理方式。它将程序的逻辑地址空间和内存的物理地址空间都划分为固定大小的页(Page),通过页表实现地址转换,解决了连续分配产生的碎片问题。
3.2.1 分页存储管理的基本方法
分页的基本概念
分页存储管理将程序的逻辑地址空间和内存的物理地址空间都划分为固定大小的页。
分页的基本概念:
页(Page):
- 逻辑地址空间被划分为固定大小的页
- 页的大小通常是2的幂次(如4KB、8KB)
- 页是逻辑单位
页框(Page Frame)或物理页:
- 物理内存被划分为固定大小的页框
- 页框大小与页大小相同
- 页框是物理单位
页表(Page Table):
- 记录页到页框的映射关系
- 每个进程有一个页表
- 用于地址转换
地址结构:
逻辑地址 = 页号 + 页内偏移
物理地址 = 页框号 + 页内偏移
形象比喻:
如果把内存比作一个大型图书馆:
- 页 = 书中的章节(逻辑单位)
- 页框 = 书架上的位置(物理单位)
- 页表 = 目录索引(记录章节在哪个书架)
- 地址转换 = 根据目录找到实际书架位置
分页的工作原理
分页工作原理:
1. 程序逻辑地址空间:
┌─────┬─────┬─────┬─────┐
│ 页0 │ 页1 │ 页2 │ 页3 │ 逻辑页
└─────┴─────┴─────┴─────┘
2. 物理内存:
┌─────┬─────┬─────┬─────┬─────┐
│框0 │框1 │框2 │框3 │框4 │ 物理页框
└─────┴─────┴─────┴─────┴─────┘
3. 页表(映射关系):
页号 → 页框号
0 → 3
1 → 1
2 → 4
3 → 0
4. 地址转换:
逻辑地址:页1,偏移100
→ 查页表:页1 → 页框1
→ 物理地址:页框1,偏移100
分页的地址结构
在分页系统中,逻辑地址和物理地址都分为两部分:页号(页框号)和页内偏移。
地址结构:
假设页大小为4KB(2^12 = 4096字节)
逻辑地址(32位):
┌──────────────┬──────────────┐
│ 页号(20位) │ 页内偏移(12位)│
└──────────────┴──────────────┘
31 12 11 0
页号:20位,可表示2^20 = 1M页
页内偏移:12位,可表示0-4095字节
物理地址(32位):
┌──────────────┬──────────────┐
│ 页框号(20位) │ 页内偏移(12位)│
└──────────────┴──────────────┘
31 12 11 0
地址转换:
逻辑地址 = 页号 × 页大小 + 页内偏移
物理地址 = 页框号 × 页大小 + 页内偏移
页内偏移相同(不需要转换)
地址转换示例:
地址转换示例:
假设:
页大小 = 4KB = 4096字节
逻辑地址 = 0x1234(十进制4660)
计算页号和页内偏移:
页号 = 4660 / 4096 = 1
页内偏移 = 4660 % 4096 = 564
二进制表示:
逻辑地址:0x1234 = 0001 0010 0011 0100
页号(高20位):0000 0000 0000 0000 0001 = 1
页内偏移(低12位):0010 0011 0100 = 564
查页表:
页1 → 页框5
计算物理地址:
页框号 = 5
页内偏移 = 564(不变)
物理地址 = 5 × 4096 + 564 = 21044 = 0x5234
页表(Page Table)
页表是分页存储管理的核心数据结构,记录每个逻辑页对应的物理页框。
页表结构:
页表项(Page Table Entry,PTE):
每个页表项包含:
- 页框号(Frame Number)
- 有效位(Valid Bit):页是否在内存
- 访问位(Accessed Bit):是否被访问
- 修改位(Dirty Bit):是否被修改
- 保护位(Protection Bit):读写权限
页表示例:
页号 页框号 有效位 访问位 修改位 保护位
──────────────────────────────────────
0 3 1 1 0 R/W
1 1 1 0 0 R
2 4 1 1 1 R/W
3 0 0 - - -
...
说明:
页0 → 页框3,在内存,可读写
页1 → 页框1,在内存,只读
页2 → 页框4,在内存,已修改,可读写
页3 → 不在内存(有效位=0)
页表项详细说明
页表项字段详解:
1. 页框号(Frame Number):
- 物理页框的编号
- 用于地址转换
- 通常20位(32位系统,4KB页)
2. 有效位(Valid Bit):
- 1:页在内存中
- 0:页不在内存中(可能在磁盘)
- 用于虚拟存储
3. 访问位(Accessed Bit):
- 1:页最近被访问
- 0:页未被访问
- 用于页面置换算法
4. 修改位(Dirty Bit):
- 1:页被修改过(需要写回磁盘)
- 0:页未被修改(可以直接丢弃)
- 用于页面置换
5. 保护位(Protection Bit):
- R:只读
- R/W:读写
- X:执行
- 用于内存保护
分页存储管理的优点
分页存储管理的优点:
1. 解决碎片问题:
- 程序可以不连续存放
- 只有最后一页可能有内部碎片
- 内部碎片最大不超过一页
2. 支持虚拟存储:
- 页可以不在内存中
- 需要时再装入
- 支持大程序
3. 内存管理简单:
- 页大小固定
- 分配和回收简单
- 只需管理页框
4. 支持共享:
- 多个进程可以共享同一页
- 页表映射到同一页框
- 提高内存利用率
分页存储管理的缺点
分页存储管理的缺点:
1. 地址转换开销:
- 每次访问都需要查页表
- 可能多次内存访问
- 需要硬件支持(TLB)
2. 页表占用空间:
- 每个进程一个页表
- 页表可能很大
- 需要优化(多级页表)
3. 内部碎片:
- 最后一页可能不满
- 平均浪费半页
- 页越大,浪费越多
4. 实现复杂:
- 需要硬件支持
- 需要管理页表
- 需要处理缺页
页大小的选择
页大小是分页系统的重要参数,需要权衡各种因素。
页大小选择:
小页(如1KB、2KB):
优点:
- 内部碎片小
- 内存利用率高
缺点:
- 页表大
- 地址转换开销大
- 管理开销大
大页(如64KB、1MB):
优点:
- 页表小
- 地址转换快
- 管理开销小
缺点:
- 内部碎片大
- 内存利用率低
- 换入换出开销大
常见选择:
- 32位系统:通常4KB
- 64位系统:通常4KB或8KB
- 大页支持:2MB、1GB(用于特殊应用)
权衡:
- 现代系统通常选择4KB
- 平衡了各种因素
- 硬件和软件都优化了4KB页
3.2.2 地址变换机构
地址变换机构是分页存储管理的核心,负责将逻辑地址转换为物理地址。现代系统通常使用硬件实现地址变换,以提高性能。
基本地址变换机构
基本地址变换机构使用页表寄存器(Page Table Register,PTR)和页表实现地址转换。
基本地址变换机构:
硬件支持:
1. 页表寄存器(PTR):
- 存储页表在内存中的起始地址
- 存储页表长度(页数)
- 每个进程切换时更新
2. 页表:
- 存储在内存中
- 每个进程一个页表
- 页表项包含页框号
地址转换过程:
1. 从逻辑地址提取页号
2. 检查页号是否有效(< 页表长度)
3. 计算页表项地址 = 页表起始地址 + 页号 × 页表项大小
4. 从内存读取页表项
5. 检查有效位
6. 提取页框号
7. 计算物理地址 = 页框号 × 页大小 + 页内偏移
地址变换流程图
地址变换流程:
逻辑地址
│
├─→ 提取页号
│
├─→ 检查页号有效性
│ │
│ ├─→ 无效:产生越界异常
│ │
│ └─→ 有效:继续
│
├─→ 计算页表项地址
│ = 页表起始地址 + 页号 × 页表项大小
│
├─→ 访问内存读取页表项
│ │
│ ├─→ 检查有效位
│ │ │
│ │ ├─→ 无效:产生缺页异常
│ │ │
│ │ └─→ 有效:继续
│ │
│ └─→ 提取页框号
│
├─→ 计算物理地址
│ = 页框号 × 页大小 + 页内偏移
│
└─→ 访问物理内存
地址变换示例
地址变换示例:
假设:
页大小 = 4KB
页表起始地址 = 0x10000
页表项大小 = 4字节
逻辑地址 = 0x1234
步骤1:提取页号和页内偏移
逻辑地址:0x1234 = 0001 0010 0011 0100
页号 = 1
页内偏移 = 0x234 = 564
步骤2:检查页号有效性
假设页表长度 = 1000
页号1 < 1000,有效
步骤3:计算页表项地址
页表项地址 = 0x10000 + 1 × 4 = 0x10004
步骤4:读取页表项
从内存0x10004读取页表项
假设页表项内容:0x00050001
(页框号=5,有效位=1,其他位=0)
步骤5:检查有效位
有效位=1,页在内存中
步骤6:提取页框号
页框号 = 5
步骤7:计算物理地址
物理地址 = 5 × 4096 + 564 = 21044 = 0x5234
步骤8:访问物理内存
访问内存地址0x5234
地址变换的性能问题
基本地址变换机构有一个严重的性能问题:每次内存访问都需要两次内存访问(一次查页表,一次访问数据)。
性能问题:
问题:
每次内存访问需要:
1. 访问页表(内存访问)
2. 访问实际数据(内存访问)
访问速度减半!
例子:
指令:MOV AX, [0x1234]
执行过程:
1. 取指令:需要地址转换(查页表)
2. 执行指令:访问数据[0x1234],又需要地址转换(查页表)
一次指令执行可能需要多次查页表!
影响:
- 系统性能严重下降
- 无法接受
解决:
- 使用快表(TLB)
- 缓存常用的页表项
快表(Translation Lookaside Buffer,TLB)
快表是解决地址变换性能问题的关键,它是一个高速缓存,存储最近使用的页表项。
快表(TLB):
定义:
- 高速缓存,存储页表项
- 硬件实现,速度极快
- 通常有64-512个表项
工作原理:
1. 地址转换时先查快表
2. 快表命中:直接使用,无需访问内存
3. 快表未命中:访问内存页表,更新快表
快表结构:
┌──────┬──────┬──────┐
│ 页号 │页框号 │有效位 │
├──────┼──────┼──────┤
│ 1 │ 5 │ 1 │
│ 3 │ 2 │ 1 │
│ 5 │ 8 │ 1 │
└──────┴──────┴──────┘
查找方式:
- 并行查找所有表项
- 硬件实现,速度极快
- 通常1个时钟周期
使用快表的地址变换流程
使用快表的地址变换:
逻辑地址
│
├─→ 提取页号
│
├─→ 查快表(TLB)
│ │
│ ├─→ 命中:
│ │ │
│ │ ├─→ 提取页框号
│ │ │
│ │ └─→ 计算物理地址
│ │ (1次内存访问:访问数据)
│ │
│ └─→ 未命中:
│ │
│ ├─→ 访问内存页表
│ │
│ ├─→ 提取页框号
│ │
│ ├─→ 更新快表
│ │
│ └─→ 计算物理地址
│ (2次内存访问:查页表+访问数据)
性能:
快表命中率通常>95%
平均访问时间 ≈ 1.05次内存访问
接近无页表的性能
快表命中率
快表命中率是影响系统性能的关键因素。
快表命中率:
定义:
快表命中次数 / 总地址转换次数
影响因素:
1. 快表大小:
- 越大,命中率越高
- 但成本也越高
- 通常64-512项
2. 程序局部性:
- 程序有时间和空间局部性
- 最近访问的页很可能再次访问
- 命中率通常>95%
3. 快表替换算法:
- LRU(最近最少使用)
- 随机替换
- 影响命中率
典型性能:
快表命中率:95-99%
平均访问时间:
= 命中率 × 1次访问 + (1-命中率) × 2次访问
= 0.95 × 1 + 0.05 × 2
= 1.05次内存访问
性能提升:接近2倍
两级页表和多级页表
当页表很大时,页表本身也会占用大量内存。多级页表可以解决这个问题。
页表大小问题:
32位系统,4KB页:
逻辑地址空间:2^32 = 4GB
页数:4GB / 4KB = 1M页
页表项大小:4字节
页表大小:1M × 4字节 = 4MB
问题:
- 每个进程需要4MB页表
- 100个进程需要400MB
- 页表占用大量内存
- 而且很多页可能不在内存中
解决:
- 多级页表
- 只分配实际使用的页表
- 节省内存
两级页表
两级页表将页号分为两部分:页目录号和页表号。
两级页表:
地址结构:
逻辑地址 = 页目录号 + 页表号 + 页内偏移
32位地址,4KB页:
┌─────────┬─────────┬──────────┐
│页目录号 │ 页表号 │页内偏移 │
│ (10位) │ (10位) │ (12位) │
└─────────┴─────────┴──────────┘
页目录:
- 一级页表
- 每个表项指向一个二级页表
- 大小:2^10 × 4字节 = 4KB
二级页表:
- 二级页表
- 每个表项指向物理页框
- 每个二级页表:2^10 × 4字节 = 4KB
地址转换:
1. 用页目录号查页目录
2. 得到二级页表地址
3. 用页表号查二级页表
4. 得到页框号
5. 计算物理地址
两级页表地址转换流程
两级页表地址转换:
逻辑地址:页目录号=2, 页表号=3, 页内偏移=0x234
步骤1:查页目录
页目录地址 = 页目录寄存器(存储页目录起始地址)
页目录项地址 = 页目录地址 + 2 × 4
读取页目录项,得到二级页表地址(如0x20000)
步骤2:查二级页表
二级页表地址 = 0x20000
页表项地址 = 0x20000 + 3 × 4 = 0x2000C
读取页表项,得到页框号(如5)
步骤3:计算物理地址
物理地址 = 5 × 4096 + 0x234 = 0x5234
内存访问次数:
未使用快表:3次(页目录+页表+数据)
使用快表:通常1次(快表命中)
多级页表
对于64位系统,可能需要更多级页表。
多级页表:
64位系统,4KB页:
逻辑地址空间:2^64(极大)
如果只用一级页表:
页表项数:2^52(假设12位偏移)
页表大小:2^52 × 8字节 = 32PB(不可能!)
解决:多级页表
- 64位系统通常使用4级或5级页表
- 只分配实际使用的页表
- 节省大量内存
x86-64系统(4级页表):
┌──────┬──────┬──────┬──────┬──────────┐
│PGD │PUD │PMD │PTE │页内偏移 │
│(9位) │(9位) │(9位) │(9位) │(12位) │
└──────┴──────┴──────┴──────┴──────────┘
页全局目录(PGD)
↓
页上目录(PUD)
↓
页中间目录(PMD)
↓
页表(PTE)
↓
物理页框
地址转换:
需要4次内存访问(未使用快表)
使用快表:通常1次
多级页表的优缺点
多级页表优缺点:
优点:
1. 节省内存:
- 只分配实际使用的页表
- 未使用的页表不分配
- 大幅减少内存占用
2. 支持大地址空间:
- 可以支持极大的地址空间
- 64位系统必需
3. 灵活:
- 可以按需分配页表
- 支持稀疏地址空间
缺点:
1. 地址转换复杂:
- 需要多次内存访问
- 必须使用快表
2. 实现复杂:
- 需要管理多级页表
- 需要处理页表缺失
3. 快表压力大:
- 需要缓存多级页表项
- 快表设计更复杂
3.2.3 反置页表
反置页表(Inverted Page Table)是一种特殊的页表结构,它不是为每个进程维护一个页表,而是为整个系统维护一个全局页表,表项按物理页框组织。
反置页表的基本概念
反置页表:
传统页表:
- 每个进程一个页表
- 表项按逻辑页号组织
- 页号 → 页框号
反置页表:
- 整个系统一个页表
- 表项按物理页框组织
- 页框号 → 进程ID + 页号
结构:
物理页框0 → 进程1,页5
物理页框1 → 进程2,页3
物理页框2 → 进程1,页2
...
查找方式:
给定:进程ID + 页号
查找:哪个页框包含该页
方法:哈希表或全相联查找
反置页表的结构
反置页表结构:
表项内容:
┌──────────┬──────────┬──────────┐
│ 进程ID │ 页号 │ 其他信息 │
└──────────┴──────────┴──────────┘
表项数量:
- 等于物理页框数
- 与进程数无关
- 大大减少内存占用
例子:
系统有1GB内存,4KB页:
物理页框数 = 1GB / 4KB = 256K
反置页表大小 = 256K × 表项大小
传统页表(100个进程):
页表大小 = 100 × 4MB = 400MB
反置页表:
页表大小 = 256K × 8字节 ≈ 2MB
节省大量内存!
反置页表的地址转换
反置页表地址转换:
输入:逻辑地址(进程ID + 页号 + 页内偏移)
步骤1:构造查找键
键 = (进程ID, 页号)
步骤2:查找反置页表
方法1:全相联查找(慢)
- 遍历所有表项
- 比较进程ID和页号
- 找到匹配的表项
方法2:哈希表查找(快)
- 使用哈希函数计算索引
- 查找哈希表
- 处理冲突
步骤3:提取页框号
表项索引 = 页框号
步骤4:计算物理地址
物理地址 = 页框号 × 页大小 + 页内偏移
问题:
- 查找速度慢(需要搜索)
- 必须使用哈希表加速
反置页表的优缺点
反置页表优缺点:
优点:
1. 内存占用小:
- 整个系统一个页表
- 与进程数无关
- 适合多进程系统
2. 物理内存固定:
- 表项数 = 物理页框数
- 可预测的内存占用
缺点:
1. 查找速度慢:
- 需要搜索整个表
- 必须使用哈希表
- 哈希冲突影响性能
2. 实现复杂:
- 需要哈希表支持
- 需要处理冲突
- 共享页处理复杂
3. 不支持虚拟存储:
- 只记录在内存中的页
- 需要额外的数据结构支持虚拟存储
使用场景:
- IBM PowerPC系统
- 某些嵌入式系统
- 多进程、大内存系统
反置页表 vs 传统页表
反置页表 vs 传统页表:
特性 传统页表 反置页表
─────────────────────────────────────
页表数量 每个进程一个 整个系统一个
表项组织 按逻辑页号 按物理页框
查找方式 直接索引 搜索/哈希
查找速度 快(O(1)) 慢(O(n)或O(1)哈希)
内存占用 大(与进程数相关) 小(与页框数相关)
实现复杂度 简单 复杂
虚拟存储 容易支持 需要额外支持
选择:
- 大多数系统使用传统页表
- 反置页表用于特殊场景
- 现代系统主要优化传统页表(多级页表)
3.3 分段存储管理
分段存储管理将程序按照逻辑功能划分为多个段(Segment),每个段有独立的逻辑意义(如代码段、数据段、栈段等)。分段更符合程序的逻辑结构,便于实现共享和保护。
3.3.1 分段存储管理方式
分段的基本概念
分段存储管理将程序的地址空间按照逻辑功能划分为多个段,每个段有独立的名称和大小。
分段的基本概念:
段(Segment):
- 程序的逻辑单位
- 具有独立的功能和意义
- 大小不固定
常见的段:
1. 代码段(Code Segment):
- 存储程序代码
- 只读
- 可以共享
2. 数据段(Data Segment):
- 存储全局变量、静态变量
- 可读写
- 每个进程独立
3. 栈段(Stack Segment):
- 存储局部变量、函数调用
- 可读写
- 动态增长
4. 堆段(Heap Segment):
- 动态分配的内存
- 可读写
- 动态增长
5. BSS段:
- 未初始化的全局变量
- 可读写
形象比喻:
如果把程序比作一本书:
- 段 = 书的章节(逻辑单位,有独立意义)
- 分页 = 书的页码(物理单位,固定大小)
- 段表 = 目录(记录每个章节的位置和大小)
分段更符合人的思维习惯,就像书的章节一样,每个段都有明确的含义。
分段的地址结构
在分段系统中,逻辑地址由段号和段内偏移组成。
分段地址结构:
逻辑地址:
┌──────────┬──────────┐
│ 段号 │ 段内偏移 │
└──────────┴──────────┘
段号:
- 标识是哪个段
- 例如:0=代码段,1=数据段,2=栈段
段内偏移:
- 段内的相对地址
- 从0开始
- 必须小于段长度
地址转换:
逻辑地址 = (段号, 段内偏移)
物理地址 = 段基址 + 段内偏移
分段地址转换示例:
分段地址转换示例:
逻辑地址:(段号=1, 段内偏移=0x100)
步骤1:检查段号有效性
假设进程有3个段(0, 1, 2)
段号1有效
步骤2:查段表
段表起始地址 = 段表寄存器
段表项地址 = 段表起始地址 + 段号 × 段表项大小
读取段表项:
段基址 = 0x5000
段长度 = 0x2000
有效位 = 1
保护位 = R/W
步骤3:检查段内偏移
段内偏移0x100 < 段长度0x2000,有效
步骤4:检查保护位
访问类型 = 写
保护位 = R/W,允许
步骤5:计算物理地址
物理地址 = 0x5000 + 0x100 = 0x5100
步骤6:访问物理内存
访问内存地址0x5100
段表(Segment Table)
段表是分段存储管理的核心数据结构,记录每个段的信息。
段表结构:
段表项(Segment Table Entry):
每个段表项包含:
- 段基址(Segment Base):段在内存中的起始地址
- 段长度(Segment Length):段的大小
- 有效位(Valid Bit):段是否在内存中
- 访问位(Accessed Bit):是否被访问
- 修改位(Dirty Bit):是否被修改
- 保护位(Protection Bit):访问权限
- 增长方向(Growth Direction):向上或向下增长
段表示例:
段号 段基址 段长度 有效位 保护位 说明
──────────────────────────────────────
0 0x1000 0x5000 1 R/X 代码段
1 0x6000 0x2000 1 R/W 数据段
2 0x8000 0x1000 1 R/W 栈段(向下增长)
3 0x9000 0x3000 0 - 堆段(不在内存)
段表项详细说明
段表项字段详解:
1. 段基址(Segment Base):
- 段在物理内存中的起始地址
- 用于地址转换
- 32位或64位
2. 段长度(Segment Length):
- 段的大小(字节数)
- 用于越界检查
- 必须大于等于段内偏移
3. 有效位(Valid Bit):
- 1:段在内存中
- 0:段不在内存中(可能在磁盘)
- 用于虚拟存储
4. 访问位(Accessed Bit):
- 1:段最近被访问
- 0:段未被访问
- 用于段置换算法
5. 修改位(Dirty Bit):
- 1:段被修改过
- 0:段未被修改
- 用于段置换
6. 保护位(Protection Bit):
- R:读权限
- W:写权限
- X:执行权限
- 用于内存保护
7. 增长方向(Growth Direction):
- 向上增长:栈段(从低地址向高地址)
- 向下增长:栈段(从高地址向低地址)
- 用于动态增长的段
分段存储管理的地址变换机构
分段地址变换机构:
硬件支持:
1. 段表寄存器(Segment Table Register,STR):
- 存储段表在内存中的起始地址
- 存储段表长度(段数)
2. 段表:
- 存储在内存中
- 每个进程一个段表
地址转换过程:
1. 从逻辑地址提取段号和段内偏移
2. 检查段号是否有效(< 段表长度)
3. 计算段表项地址 = 段表起始地址 + 段号 × 段表项大小
4. 从内存读取段表项
5. 检查有效位
6. 检查段内偏移是否越界(< 段长度)
7. 检查访问权限
8. 计算物理地址 = 段基址 + 段内偏移
9. 访问物理内存
分段地址变换流程图
分段地址变换流程:
逻辑地址(段号, 段内偏移)
│
├─→ 提取段号和段内偏移
│
├─→ 检查段号有效性
│ │
│ ├─→ 无效:产生段越界异常
│ │
│ └─→ 有效:继续
│
├─→ 计算段表项地址
│ = 段表起始地址 + 段号 × 段表项大小
│
├─→ 访问内存读取段表项
│ │
│ ├─→ 检查有效位
│ │ │
│ │ ├─→ 无效:产生段缺失异常
│ │ │
│ │ └─→ 有效:继续
│ │
│ ├─→ 检查段内偏移
│ │ │
│ │ ├─→ 越界:产生段越界异常
│ │ │
│ │ └─→ 有效:继续
│ │
│ ├─→ 检查访问权限
│ │ │
│ │ ├─→ 不允许:产生保护异常
│ │ │
│ │ └─→ 允许:继续
│ │
│ └─→ 提取段基址
│
├─→ 计算物理地址
│ = 段基址 + 段内偏移
│
└─→ 访问物理内存
分段存储管理的优点
分段存储管理的优点:
1. 符合程序逻辑结构:
- 程序自然分为代码、数据、栈等段
- 便于理解和编程
- 符合程序员思维
2. 便于共享:
- 可以按段共享
- 例如:多个进程共享代码段
- 提高内存利用率
3. 便于保护:
- 可以按段设置保护权限
- 代码段只读,数据段可读写
- 提高系统安全性
4. 支持动态增长:
- 段可以动态增长
- 栈段和堆段可以扩展
- 灵活的内存管理
5. 便于调试:
- 段有明确的逻辑意义
- 便于定位问题
- 便于内存管理
分段存储管理的缺点
分段存储管理的缺点:
1. 外部碎片:
- 段大小不固定
- 分配和回收产生碎片
- 需要紧凑技术
2. 地址转换开销:
- 每次访问需要查段表
- 需要越界检查
- 需要权限检查
3. 段表占用空间:
- 每个进程一个段表
- 段表可能较大
- 需要管理段表
4. 实现复杂:
- 需要处理动态增长
- 需要处理段置换
- 需要管理碎片
分段 vs 分页
分段和分页是两种不同的内存管理方式,各有优缺点。
分段 vs 分页:
特性 分段 分页
─────────────────────────────────────
划分单位 逻辑单位(段) 物理单位(页)
大小 不固定 固定
地址结构 段号+段内偏移 页号+页内偏移
碎片 外部碎片 内部碎片
共享 按段共享 按页共享
保护 段级保护 页级保护
增长 支持动态增长 不支持
符合逻辑 是 否
实现复杂度 较高 较低
地址转换 段表 页表
选择:
- 分页:现代系统主要使用
- 分段:某些系统使用
- 段页式:结合两者优点
分段的共享
分段存储管理可以方便地实现段的共享。
分段共享:
共享方式:
多个进程的段表项指向同一个物理段
例子:
进程1和进程2共享代码段:
进程1段表:
段0(代码段)→ 物理地址0x1000
段1(数据段)→ 物理地址0x5000
进程2段表:
段0(代码段)→ 物理地址0x1000(共享)
段1(数据段)→ 物理地址0x6000(独立)
内存布局:
0x1000: 代码段(共享)
0x5000: 进程1数据段
0x6000: 进程2数据段
优点:
- 节省内存
- 代码段可以共享
- 只读段容易共享
实现:
- 段表项指向同一物理地址
- 设置共享标志
- 引用计数管理
分段的保护
分段存储管理可以按段设置保护权限。
分段保护:
保护方式:
1. 访问权限:
- 代码段:只读、执行
- 数据段:读写
- 栈段:读写
2. 越界检查:
- 检查段内偏移 < 段长度
- 防止越界访问
3. 权限检查:
- 检查访问类型是否允许
- 写只读段产生异常
- 执行非执行段产生异常
例子:
代码段:
保护位:R/X(只读、执行)
允许:读取、执行
禁止:写入
数据段:
保护位:R/W(读写)
允许:读取、写入
禁止:执行
栈段:
保护位:R/W(读写)
允许:读取、写入
禁止:执行
3.3.2 段页式存储管理方式
段页式存储管理结合了分段和分页的优点,既保持了分段的逻辑结构,又具有分页的物理管理优势。
段页式的基本概念
段页式存储管理先分段,再分页。程序的逻辑地址空间按段划分,每个段内部按页划分。
段页式存储管理:
地址结构:
逻辑地址 = 段号 + 段内页号 + 页内偏移
32位地址示例:
┌──────┬──────────┬──────────┐
│段号 │ 段内页号 │页内偏移 │
│(4位) │ (8位) │ (12位) │
└──────┴──────────┴──────────┘
管理方式:
1. 程序按段划分(逻辑单位)
2. 每个段内部按页划分(物理单位)
3. 段表记录段的信息
4. 每个段有自己的页表
5. 页表记录页到页框的映射
数据结构:
- 段表:每个进程一个
- 页表:每个段一个
- 页框:物理内存单位
段页式的地址结构
段页式地址结构:
逻辑地址分解:
逻辑地址 = 段号 + 段内地址
段内地址 = 段内页号 + 页内偏移
完整结构:
┌──────┬──────────┬──────────┐
│段号 │ 段内页号 │页内偏移 │
└──────┴──────────┴──────────┘
例子:
假设:
段号字段:4位(16个段)
段内页号:8位(256页/段)
页内偏移:12位(4KB页)
逻辑地址:0x12345678
分解:
段号 = 0x1 = 1
段内页号 = 0x23 = 35
页内偏移 = 0x456 = 1110
段页式的数据结构
段页式需要两种数据结构:段表和页表。
段页式数据结构:
1. 段表(Segment Table):
每个进程一个段表
段表项包含:
- 页表起始地址:该段的页表在内存中的位置
- 页表长度:该段有多少页
- 其他信息:有效位、保护位等
2. 页表(Page Table):
每个段一个页表
页表项包含:
- 页框号:物理页框编号
- 其他信息:有效位、访问位、修改位等
段表示例:
段号 页表起始地址 页表长度 有效位 保护位
──────────────────────────────────────
0 0x10000 100 1 R/X
1 0x20000 50 1 R/W
2 0x30000 200 0 -
页表示例(段0的页表):
页号 页框号 有效位 访问位 修改位
──────────────────────────────
0 5 1 1 0
1 8 1 0 0
2 3 1 1 1
...
段页式的地址转换
段页式的地址转换需要两次查表:先查段表,再查页表。
段页式地址转换:
输入:逻辑地址(段号, 段内页号, 页内偏移)
步骤1:查段表
段表项地址 = 段表起始地址 + 段号 × 段表项大小
读取段表项:
页表起始地址 = 0x10000
页表长度 = 100
有效位 = 1
保护位 = R/X
步骤2:检查段内页号
段内页号 < 页表长度,有效
步骤3:查页表
页表项地址 = 页表起始地址 + 段内页号 × 页表项大小
读取页表项:
页框号 = 5
有效位 = 1
访问位 = 1
修改位 = 0
步骤4:检查有效位和保护位
有效位 = 1,页在内存中
保护位检查通过
步骤5:计算物理地址
物理地址 = 页框号 × 页大小 + 页内偏移
物理地址 = 5 × 4096 + 0x456 = 0x5234
步骤6:访问物理内存
访问内存地址0x5234
段页式地址转换流程图
段页式地址转换流程:
逻辑地址(段号, 段内页号, 页内偏移)
│
├─→ 提取段号、段内页号、页内偏移
│
├─→ 检查段号有效性
│ │
│ └─→ 无效:段越界异常
│
├─→ 查段表
│ │
│ ├─→ 读取段表项
│ │ │
│ │ ├─→ 检查有效位
│ │ │ │
│ │ │ └─→ 无效:段缺失异常
│ │ │
│ │ └─→ 提取页表起始地址
│ │
│ └─→ 检查段内页号
│ │
│ └─→ 越界:段内页越界异常
│
├─→ 查页表
│ │
│ ├─→ 计算页表项地址
│ │ = 页表起始地址 + 段内页号 × 页表项大小
│ │
│ ├─→ 读取页表项
│ │ │
│ │ ├─→ 检查有效位
│ │ │ │
│ │ │ └─→ 无效:页缺失异常
│ │ │
│ │ ├─→ 检查保护位
│ │ │ │
│ │ │ └─→ 不允许:保护异常
│ │ │
│ │ └─→ 提取页框号
│ │
│ └─→ 计算物理地址
│ = 页框号 × 页大小 + 页内偏移
│
└─→ 访问物理内存
段页式的性能问题
段页式的地址转换需要两次内存访问(查段表和查页表),性能开销较大。
段页式性能问题:
问题:
每次内存访问需要:
1. 访问段表(内存访问)
2. 访问页表(内存访问)
3. 访问实际数据(内存访问)
总共3次内存访问!
解决:
1. 使用快表(TLB):
- 缓存(段号, 段内页号)→ 页框号的映射
- 快表命中:只需1次内存访问
- 快表未命中:需要3次内存访问
2. 快表结构:
段号 | 段内页号 | 页框号 | 有效位
──────────────────────────────
1 | 5 | 8 | 1
0 | 3 | 2 | 1
...
3. 快表查找:
输入:(段号, 段内页号)
并行查找所有表项
找到匹配项,提取页框号
段页式的优点
段页式的优点:
1. 结合分段和分页的优点:
- 保持分段的逻辑结构
- 具有分页的物理管理优势
- 既符合逻辑又高效
2. 解决碎片问题:
- 分页解决外部碎片
- 只有内部碎片(最后一页)
- 内存利用率高
3. 便于共享和保护:
- 可以按段共享
- 可以按段保护
- 灵活的内存管理
4. 支持虚拟存储:
- 可以按页换入换出
- 支持大程序
- 提高内存利用率
5. 符合程序结构:
- 程序按段组织
- 符合程序员思维
- 便于理解
段页式的缺点
段页式的缺点:
1. 地址转换复杂:
- 需要两次查表
- 必须使用快表
- 实现复杂
2. 内存占用:
- 需要段表和页表
- 每个段一个页表
- 表占用内存
3. 管理开销:
- 需要管理段表
- 需要管理多个页表
- 管理复杂
4. 实现复杂:
- 需要硬件支持
- 需要处理段缺失和页缺失
- 需要处理共享和保护
段页式 vs 纯分页 vs 纯分段
段页式 vs 纯分页 vs 纯分段:
特性 纯分页 纯分段 段页式
─────────────────────────────────────────
逻辑结构 无 有 有
碎片 内部碎片 外部碎片 内部碎片
地址转换 1次查表 1次查表 2次查表
共享 按页 按段 按段
保护 页级 段级 段级
实现复杂度 简单 中等 复杂
内存占用 中等 大 中等
性能 好 中等 好(需快表)
现代系统:
- x86系统:使用段页式
- 但段的作用被弱化
- 主要使用分页功能
- 段主要用于保护
段页式在现代系统中的应用
段页式在现代系统中的应用:
x86架构:
- 使用段页式存储管理
- 但段的作用被弱化
- 主要使用分页功能
x86-64架构:
- 进一步弱化段的作用
- 主要使用分页
- 段主要用于兼容性
Linux系统:
- 在x86上使用段页式
- 但所有进程使用相同的段设置
- 实际上主要使用分页
Windows系统:
- 在x86上使用段页式
- 段主要用于保护
- 主要使用分页功能
趋势:
- 现代系统主要使用分页
- 段的作用逐渐弱化
- 但段页式架构仍然存在
3.4 虚拟存储器
虚拟存储器是操作系统提供的一种内存管理技术,它允许程序使用比物理内存更大的地址空间。通过将外存(磁盘)作为内存的扩展,虚拟存储器实现了内存的自动管理,对用户完全透明。
3.4.1 虚拟存储器的基本概念
虚拟存储器的定义
虚拟存储器(Virtual Memory)是一种内存管理技术,它使得程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
形象比喻:
如果把内存比作一个图书馆:
- 物理内存 = 图书馆的实际书架(有限)
- 虚拟内存 = 图书馆的目录系统(可以很大)
- 磁盘 = 仓库(存储暂时不用的书)
- 虚拟存储器 = 目录系统 + 仓库,让读者感觉所有书都在图书馆
读者(程序)通过目录(页表)查找书(数据),如果书在书架(内存)上,直接取;如果不在,从仓库(磁盘)调来。
虚拟存储器的基本思想
虚拟存储器的基本思想:
1. 程序不必全部装入内存:
- 只装入当前需要的部分
- 其他部分留在磁盘上
- 需要时再装入
2. 程序可以比物理内存大:
- 逻辑地址空间可以很大
- 不受物理内存限制
- 例如:4GB程序可以在2GB内存上运行
3. 对用户透明:
- 用户感觉程序全部在内存中
- 不需要关心哪些在内存,哪些在磁盘
- 操作系统自动管理
4. 按需装入:
- 需要哪部分,装入哪部分
- 不需要的部分不装入
- 提高内存利用率
虚拟存储器的特征
虚拟存储器的特征:
1. 多次性:
- 程序可以分多次装入内存
- 不是一次性全部装入
- 需要时再装入
2. 对换性:
- 程序在内存和外存之间换入换出
- 暂时不用的部分换出
- 需要时再换入
3. 虚拟性:
- 逻辑容量远大于物理容量
- 用户感觉有无限内存
- 实际上是虚拟的
4. 离散性:
- 程序在内存中不连续存放
- 可以分散在多个页框
- 通过页表管理
虚拟存储器的实现方式
虚拟存储器主要有两种实现方式:
虚拟存储器实现方式:
1. 请求分页(Demand Paging):
- 基于分页存储管理
- 页是基本单位
- 需要时装入页
- 最常用
2. 请求分段(Demand Segmentation):
- 基于分段存储管理
- 段是基本单位
- 需要时装入段
- 较少使用
3. 段页式虚拟存储:
- 结合分段和分页
- 先分段,再分页
- 需要时装入页
- 某些系统使用
虚拟存储器的优点
虚拟存储器的优点:
1. 运行大程序:
- 程序可以比物理内存大
- 不受物理内存限制
- 支持复杂应用
2. 提高内存利用率:
- 只装入需要的部分
- 多个程序共享内存
- 充分利用内存
3. 支持多道程序:
- 可以运行更多程序
- 提高系统吞吐量
- 提高CPU利用率
4. 简化编程:
- 程序员不需要关心内存大小
- 可以使用大地址空间
- 提高开发效率
5. 提高系统灵活性:
- 程序可以动态增长
- 支持动态链接
- 支持共享库
虚拟存储器的缺点
虚拟存储器的缺点:
1. 地址转换开销:
- 需要额外的地址转换
- 可能多次访问内存
- 需要硬件支持
2. 页面置换开销:
- 需要换入换出页面
- 磁盘I/O开销大
- 可能影响性能
3. 实现复杂:
- 需要管理页表
- 需要处理缺页
- 需要页面置换算法
4. 可能产生抖动:
- 频繁的页面置换
- 系统性能下降
- 需要合理配置
虚拟存储器的容量
虚拟存储器容量:
理论容量:
- 由地址结构决定
- 32位系统:2^32 = 4GB
- 64位系统:2^64(极大)
实际容量:
- 受磁盘空间限制
- 受页表大小限制
- 受系统配置限制
物理内存:
- 通常远小于虚拟内存
- 例如:虚拟内存4GB,物理内存2GB
- 程序可以比物理内存大
交换空间(Swap Space):
- 磁盘上用于虚拟存储的区域
- 存储暂时不用的页面
- 大小通常等于或大于物理内存
3.4.2 请求分页存储管理方式
请求分页是虚拟存储器的主要实现方式,它基于分页存储管理,增加了请求调页和页面置换功能。
请求分页的基本概念
请求分页:
定义:
- 基于分页存储管理
- 程序开始时只装入部分页
- 需要时再装入其他页
- 不需要时可以换出
工作原理:
1. 程序装入时只装入部分页
2. 页表项的有效位标记页是否在内存
3. 访问不在内存的页时产生缺页中断
4. 操作系统处理缺页,装入需要的页
5. 如果内存满,需要置换页面
请求分页的页表结构
请求分页的页表需要增加一些字段来支持虚拟存储。
请求分页页表项:
页表项结构:
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ 页框号 │ 有效位 │ 访问位 │ 修改位 │ 保护位 │
└──────────┴──────────┴──────────┴──────────┴──────────┘
字段说明:
1. 页框号(Frame Number):
- 页在内存中的页框号
- 有效位=1时有效
- 有效位=0时无效
2. 有效位(Valid Bit):
- 1:页在内存中
- 0:页不在内存中(在磁盘)
- 用于判断是否缺页
3. 访问位(Accessed Bit):
- 1:页最近被访问
- 0:页未被访问
- 用于页面置换算法(LRU等)
4. 修改位(Dirty Bit):
- 1:页被修改过(需要写回磁盘)
- 0:页未被修改(可以直接丢弃)
- 用于页面置换(减少I/O)
5. 保护位(Protection Bit):
- R:只读
- R/W:读写
- X:执行
- 用于内存保护
6. 外存地址(可选):
- 页在磁盘上的位置
- 用于缺页时从磁盘读取
缺页中断(Page Fault)
缺页中断是请求分页的核心机制,当访问不在内存中的页时触发。
缺页中断:
触发条件:
- 访问页表项时,有效位=0
- 表示页不在内存中
- 需要从磁盘装入
处理过程:
1. 访问页表项,发现有效位=0
2. 产生缺页中断
3. 保存当前进程状态
4. 转入缺页中断处理程序
5. 检查页是否合法(地址检查)
6. 查找空闲页框
7. 如果无空闲页框,选择置换页面
8. 从磁盘读取页到内存
9. 更新页表项(有效位=1,设置页框号)
10. 恢复进程执行
缺页中断处理流程:
用户程序执行
│
├─→ 访问内存地址
│
├─→ 地址转换,查页表
│
├─→ 发现有效位=0(缺页)
│
├─→ 产生缺页中断
│
├─→ 进入内核态
│
├─→ 缺页中断处理程序
│ │
│ ├─→ 检查地址合法性
│ │
│ ├─→ 查找空闲页框
│ │ │
│ │ ├─→ 有:直接使用
│ │ │
│ │ └─→ 无:选择置换页面
│ │ │
│ │ ├─→ 被置换页修改过:写回磁盘
│ │ │
│ │ └─→ 被置换页未修改:直接丢弃
│ │
│ ├─→ 从磁盘读取页
│ │
│ ├─→ 更新页表项
│ │
│ └─→ 返回用户态
│
└─→ 重新执行原指令
缺页中断处理示例
缺页中断处理示例:
场景:
进程访问逻辑地址0x1234
该地址在页1,页1不在内存中
步骤1:地址转换
逻辑地址0x1234
页号 = 1
页内偏移 = 0x234
步骤2:查页表
页表项[1]:
有效位 = 0(不在内存)
外存地址 = 磁盘块100
步骤3:产生缺页中断
CPU产生中断
保存当前状态
转入中断处理程序
步骤4:缺页处理
检查页号1是否合法:是
查找空闲页框:找到页框5
从磁盘块100读取页到页框5
更新页表项[1]:
页框号 = 5
有效位 = 1
访问位 = 1
修改位 = 0
步骤5:恢复执行
返回用户态
重新执行访问指令
这次可以正常访问
内存分配策略
请求分页需要决定何时装入页面,主要有三种策略。
内存分配策略:
1. 预调页(Prepaging):
- 预先装入可能需要的页
- 基于程序局部性原理
- 减少缺页次数
- 但可能装入不需要的页
2. 请求调页(Demand Paging):
- 需要时才装入页
- 缺页时装入
- 最常用
- 只装入实际需要的页
3. 混合策略:
- 结合预调页和请求调页
- 开始时预调页
- 运行中请求调页
- 平衡性能和内存使用
页面置换
当内存中没有空闲页框时,需要选择一个页面置换出去,为新页面腾出空间。
页面置换:
触发条件:
- 发生缺页中断
- 内存中没有空闲页框
- 需要选择页面置换
置换过程:
1. 选择被置换的页面(置换算法)
2. 如果页面被修改过,写回磁盘
3. 如果页面未修改,直接丢弃
4. 更新页表项(有效位=0)
5. 将新页面装入释放的页框
6. 更新新页面的页表项
置换算法:
- 选择哪个页面置换
- 影响系统性能
- 需要好的算法
3.4.3 页面置换算法
页面置换算法是虚拟存储器的核心,它决定当内存满时选择哪个页面置换出去。好的算法可以减少缺页次数,提高系统性能。
页面置换算法的评价标准
页面置换算法评价标准:
1. 缺页率(Page Fault Rate):
- 缺页次数 / 总访问次数
- 越低越好
- 主要评价指标
2. 置换次数:
- 需要写回磁盘的次数
- 越少越好
- 影响I/O开销
3. 实现复杂度:
- 算法实现的复杂程度
- 越简单越好
- 影响系统开销
4. 开销:
- 算法执行的开销
- 越小越好
- 不能影响性能
1. 最佳置换算法(Optimal,OPT)
最佳置换算法是一种理论算法,选择未来最长时间不会被访问的页面置换。
最佳置换算法(OPT):
算法:
- 选择未来最长时间不会被访问的页面
- 需要知道未来的访问序列
- 理论最优算法
例子:
内存有3个页框,访问序列:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
时刻 访问 内存状态 置换
──────────────────────────────────
0 - [][][] -
1 1 [1][][] -
2 2 [1][2][] -
3 3 [1][2][3] -
4 4 缺页,选择3置换 [1][2][4] 3(未来最晚访问)
5 1 命中 [1][2][4]
6 2 命中 [1][2][4]
7 5 缺页,选择4置换 [1][2][5] 4(未来最晚访问)
8 1 命中 [1][2][5]
9 2 命中 [1][2][5]
10 3 缺页,选择5置换 [1][2][3] 5(未来最晚访问)
11 4 缺页,选择1置换 [4][2][3] 1(未来最晚访问)
12 5 缺页,选择2置换 [4][5][3] 2(未来最晚访问)
缺页次数:6次
特点:
优点:
- 理论最优
- 缺页率最低
缺点:
- 需要知道未来访问序列
- 无法实际实现
- 只能用于评价其他算法
用途:
- 作为理论基准
- 评价其他算法
- 算法研究
2. 先进先出算法(First-In-First-Out,FIFO)
FIFO算法选择最早进入内存的页面置换。
先进先出算法(FIFO):
算法:
- 选择最早进入内存的页面置换
- 使用队列管理页面
- 简单易实现
实现:
- 使用队列记录页面进入顺序
- 新页面进入时加入队尾
- 需要置换时选择队头页面
例子:
内存有3个页框,访问序列:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
时刻 访问 内存状态 队列 置换
──────────────────────────────────────────
0 - [][][] [] -
1 1 [1][][] [1] -
2 2 [1][2][] [1,2] -
3 3 [1][2][3] [1,2,3] -
4 4 缺页,置换1 [2,3,4] [2,3,4] 1
5 1 缺页,置换2 [3,4,1] [3,4,1] 2
6 2 缺页,置换3 [4,1,2] [4,1,2] 3
7 5 缺页,置换4 [1,2,5] [1,2,5] 4
8 1 命中 [1,2,5] [1,2,5]
9 2 命中 [1,2,5] [1,2,5]
10 3 缺页,置换1 [2,5,3] [2,5,3] 1
11 4 缺页,置换2 [5,3,4] [5,3,4] 2
12 5 命中 [5,3,4] [5,3,4]
缺页次数:9次
特点:
优点:
- 实现简单
- 开销小
缺点:
- 不考虑页面使用情况
- 可能置换常用页面
- 性能较差
- 可能出现Belady异常
Belady异常:
- 增加页框数,缺页率反而增加
- FIFO算法特有的问题
- 说明算法不够好
3. 最近最久未使用算法(Least Recently Used,LRU)
LRU算法选择最近最长时间未被访问的页面置换。
最近最久未使用算法(LRU):
算法:
- 选择最近最长时间未被访问的页面
- 基于时间局部性原理
- 性能接近OPT算法
实现方式:
1. 计数器方式:
- 每个页表项有访问时间字段
- 每次访问更新时间
- 选择时间最早的页面
2. 栈方式:
- 使用栈记录访问顺序
- 访问页面时移到栈顶
- 选择栈底页面
3. 硬件支持:
- 使用移位寄存器
- 定期右移,访问时置1
- 选择值最小的页面
例子(计数器方式):
内存有3个页框,访问序列:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
时刻 访问 内存状态 访问时间 置换
──────────────────────────────────────────
0 - [][][] - -
1 1 [1][][] [1:1] -
2 2 [1][2][] [1:1,2:2] -
3 3 [1][2][3] [1:1,2:2,3:3] -
4 4 缺页,置换1 [4:4,2:2,3:3] 1(时间最早)
5 1 缺页,置换3 [4:4,2:2,1:5] 3(时间最早)
6 2 命中 [4:4,2:6,1:5] -
7 5 缺页,置换4 [5:7,2:6,1:5] 4(时间最早)
8 1 命中 [5:7,2:6,1:8] -
9 2 命中 [5:7,2:9,1:8] -
10 3 缺页,置换5 [3:10,2:9,1:8] 5(时间最早)
11 4 缺页,置换1 [3:10,2:9,4:11] 1(时间最早)
12 5 缺页,置换2 [3:10,5:12,4:11] 2(时间最早)
缺页次数:7次
特点:
优点:
- 性能好,接近OPT
- 基于程序局部性
- 实际系统常用
缺点:
- 实现复杂
- 需要硬件支持或软件模拟
- 开销较大
4. 时钟算法(Clock)
时钟算法是LRU的近似算法,实现简单,性能较好。
时钟算法(Clock):
算法:
- 使用循环队列组织页面
- 使用访问位标记页面
- 指针循环扫描,选择访问位=0的页面
工作原理:
1. 页面组织成循环队列
2. 指针指向当前页面
3. 需要置换时:
a. 检查当前页面访问位
b. 如果=1,置0,指针前进
c. 如果=0,选择该页面置换
d. 重复直到找到访问位=0的页面
例子:
内存有4个页框,访问位初始为0
访问序列:1, 2, 3, 4, 1, 2, 5
初始:指针→[1:0][2:0][3:0][4:0]
访问1:命中,[1:1][2:0][3:0][4:0],指针→2
访问2:命中,[1:1][2:1][3:0][4:0],指针→3
访问3:命中,[1:1][2:1][3:1][4:0],指针→4
访问4:命中,[1:1][2:1][3:1][4:1],指针→1
访问1:命中,[1:1][2:1][3:1][4:1],指针→2
访问2:命中,[1:1][2:1][3:1][4:1],指针→3
访问5:缺页
检查3:访问位=1,置0,[1:1][2:1][3:0][4:1],指针→4
检查4:访问位=1,置0,[1:1][2:1][3:0][4:0],指针→1
检查1:访问位=1,置0,[1:0][2:1][3:0][4:0],指针→2
检查2:访问位=1,置0,[1:0][2:0][3:0][4:0],指针→3
检查3:访问位=0,置换3,[1:0][2:0][5:0][4:0],指针→4
特点:
优点:
- 实现简单
- 性能接近LRU
- 只需要访问位
缺点:
- 可能选择最近访问的页面
- 不如LRU精确
5. 改进的时钟算法(Enhanced Clock)
改进的时钟算法同时考虑访问位和修改位。
改进的时钟算法:
算法:
- 同时考虑访问位和修改位
- 优先选择(访问位=0, 修改位=0)的页面
- 避免写回磁盘的开销
页面分类:
1. (0, 0):未访问,未修改 - 最佳选择
2. (0, 1):未访问,已修改 - 次选(需写回)
3. (1, 0):已访问,未修改 - 再次
4. (1, 1):已访问,已修改 - 最后选择
扫描策略:
第1轮:查找(0, 0)页面
第2轮:查找(0, 1)页面,写回磁盘
第3轮:查找(1, 0)页面,清除访问位
第4轮:查找(1, 1)页面,写回磁盘
特点:
优点:
- 减少写回磁盘次数
- 性能更好
- 实际系统常用
缺点:
- 实现稍复杂
- 需要多轮扫描
页面置换算法对比
页面置换算法对比:
算法 缺页率 实现复杂度 硬件需求 实际使用
─────────────────────────────────────────
OPT 最低 无法实现 无 理论基准
FIFO 较高 简单 无 很少使用
LRU 较低 复杂 需要 常用
Clock 较低 中等 需要 常用
改进Clock 较低 中等 需要 常用
实际系统:
- Linux:使用改进的时钟算法
- Windows:使用改进的时钟算法
- 大多数系统使用时钟算法的变种
3.4.4 页面分配策略
页面分配策略决定如何为进程分配物理页框,包括分配数量、分配方式和置换范围。
页面分配策略的分类
页面分配策略:
1. 按分配数量:
- 固定分配
- 可变分配
2. 按置换范围:
- 局部置换
- 全局置换
3. 组合策略:
- 固定分配 + 局部置换
- 可变分配 + 全局置换
- 可变分配 + 局部置换
1. 固定分配 + 局部置换
固定分配 + 局部置换:
定义:
- 为每个进程分配固定数量的页框
- 进程只能置换自己的页面
- 页框数不变
特点:
优点:
- 进程间互不影响
- 系统行为可预测
- 适合实时系统
缺点:
- 页框数固定,不够灵活
- 可能浪费或不足
- 需要预先确定页框数
分配方法:
1. 平均分配:
- 所有进程平分页框
- 简单但不公平
2. 按比例分配:
- 根据进程大小分配
- 更公平
3. 按优先级分配:
- 高优先级进程分配更多
- 体现重要性
2. 可变分配 + 全局置换
可变分配 + 全局置换:
定义:
- 进程的页框数可以变化
- 可以从其他进程置换页面
- 系统统一管理
特点:
优点:
- 灵活,适应进程需求
- 内存利用率高
- 实现简单
缺点:
- 进程间相互影响
- 可能影响系统性能
- 需要全局管理
工作方式:
1. 系统维护全局空闲页框池
2. 进程缺页时从池中分配
3. 池空时从任何进程置换页面
4. 动态调整各进程页框数
3. 可变分配 + 局部置换
可变分配 + 局部置换:
定义:
- 进程的页框数可以变化
- 但只能置换自己的页面
- 结合两者优点
特点:
优点:
- 灵活且隔离
- 进程间互不影响
- 性能好
缺点:
- 实现复杂
- 需要动态调整页框数
工作方式:
1. 初始为进程分配一定页框
2. 根据缺页率调整页框数
3. 缺页率高:增加页框
4. 缺页率低:减少页框
5. 进程只能置换自己的页面
工作集(Working Set)
工作集是进程在一段时间内访问的页面集合,用于指导页面分配。
工作集:
定义:
- 进程在时间窗口[t-Δ, t]内访问的页面集合
- Δ是工作集窗口大小
- 反映进程的局部性
工作集大小:
- 工作集中页面的数量
- 反映进程的内存需求
- 用于页面分配
工作集算法:
1. 记录进程访问的页面
2. 维护工作集窗口
3. 根据工作集大小分配页框
4. 工作集外的页面可以换出
例子:
时间窗口Δ = 10
访问序列:1, 2, 3, 1, 4, 2, 5, 1, 3, 2
时刻10的工作集:
访问的页面:1, 2, 3, 4, 5
工作集大小:5
应该分配5个页框
页面分配策略的选择
页面分配策略选择:
系统类型 推荐策略
─────────────────────────
通用系统 可变分配 + 全局置换
实时系统 固定分配 + 局部置换
服务器系统 可变分配 + 局部置换
桌面系统 可变分配 + 全局置换
现代系统:
- 大多数使用可变分配 + 全局置换
- 简单且有效
- Linux、Windows都使用
3.4.5 抖动
抖动(Thrashing)是虚拟存储器中的一个严重问题,指系统频繁进行页面置换,导致系统性能急剧下降。
抖动的定义
抖动(Thrashing):
定义:
- 系统频繁进行页面置换
- CPU利用率急剧下降
- 大部分时间花在页面置换上
- 系统几乎停止工作
表现:
- 缺页率极高
- CPU利用率很低
- 磁盘I/O繁忙
- 系统响应极慢
- 进程几乎无法执行
原因:
- 进程的页框数不足
- 无法容纳工作集
- 频繁缺页和置换
- 形成恶性循环
抖动产生的原因
抖动产生的原因:
1. 进程页框数不足:
- 分配的页框少于工作集
- 无法容纳常用页面
- 频繁缺页
2. 多道程序度太高:
- 同时运行太多进程
- 每个进程页框数太少
- 所有进程都缺页
3. 页面置换算法不当:
- 算法选择不好
- 置换了常用页面
- 导致频繁缺页
4. 工作集变化:
- 进程工作集突然增大
- 原有页框数不足
- 开始频繁缺页
恶性循环:
缺页 → 页面置换 → 磁盘I/O → CPU等待 →
进程阻塞 → 调度其他进程 → 其他进程也缺页 →
更多页面置换 → 更严重的缺页
抖动的检测
抖动检测:
检测方法:
1. CPU利用率:
- CPU利用率很低(< 10%)
- 大部分时间等待I/O
- 可能发生抖动
2. 缺页率:
- 缺页率极高
- 频繁缺页中断
- 可能发生抖动
3. 页面置换频率:
- 页面置换非常频繁
- 磁盘I/O繁忙
- 可能发生抖动
4. 系统响应时间:
- 系统响应极慢
- 进程几乎无法执行
- 可能发生抖动
检测算法:
if (CPU利用率 < 阈值 && 缺页率 > 阈值) {
可能发生抖动
采取应对措施
}
抖动的解决方法
抖动解决方法:
1. 降低多道程序度:
- 减少同时运行的进程数
- 挂起部分进程
- 为剩余进程分配更多页框
2. 增加进程页框数:
- 为进程分配更多页框
- 容纳工作集
- 减少缺页
3. 改进页面置换算法:
- 使用更好的算法
- 避免置换常用页面
- 减少缺页
4. 使用工作集算法:
- 根据工作集分配页框
- 确保页框数 >= 工作集大小
- 避免抖动
5. 增加物理内存:
- 最直接的方法
- 提供更多页框
- 从根本上解决
6. 使用局部置换:
- 进程只能置换自己的页面
- 避免进程间相互影响
- 减少抖动
预防抖动
预防抖动:
1. 合理设置多道程序度:
- 不要同时运行太多进程
- 根据内存大小确定
- 为每个进程保留足够页框
2. 使用工作集算法:
- 监控进程工作集
- 根据工作集分配页框
- 确保页框数充足
3. 预留空闲页框:
- 保持一定数量的空闲页框
- 避免频繁置换
- 提高系统稳定性
4. 监控系统状态:
- 监控CPU利用率
- 监控缺页率
- 及时发现问题
5. 动态调整:
- 根据系统负载调整
- 负载高时减少进程数
- 负载低时增加进程数
抖动的实际影响
抖动实际影响:
系统表现:
- 系统几乎停止响应
- 鼠标移动都卡顿
- 键盘输入延迟
- 应用程序无法使用
用户体验:
- 极度糟糕
- 系统不可用
- 需要重启系统
解决时间:
- 可能需要几分钟
- 甚至需要重启
- 严重影响工作
预防重要性:
- 预防比解决更重要
- 合理配置系统
- 监控系统状态
3.5 请求分段存储管理方式
请求分段存储管理是虚拟存储器的另一种实现方式,它基于分段存储管理,增加了请求调段和段置换功能。虽然实际系统较少使用纯分段,但理解请求分段有助于理解段页式虚拟存储。
3.5.1 请求分段中的硬件支持
请求分段需要硬件支持来实现地址转换、缺段检测和段保护。
请求分段的基本概念
请求分段:
定义:
- 基于分段存储管理
- 程序开始时只装入部分段
- 需要时再装入其他段
- 不需要时可以换出
工作原理:
1. 程序装入时只装入部分段
2. 段表项的有效位标记段是否在内存
3. 访问不在内存的段时产生缺段中断
4. 操作系统处理缺段,装入需要的段
5. 如果内存满,需要置换段
请求分段的段表结构
请求分段的段表需要增加字段来支持虚拟存储。
请求分段段表项:
段表项结构:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ 段基址 │ 段长度 │ 有效位 │ 访问位 │ 修改位 │ 保护位 │ 外存地址 │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
字段说明:
1. 段基址(Segment Base):
- 段在内存中的起始地址
- 有效位=1时有效
- 有效位=0时无效
2. 段长度(Segment Length):
- 段的大小(字节数)
- 用于越界检查
- 必须大于等于段内偏移
3. 有效位(Valid Bit):
- 1:段在内存中
- 0:段不在内存中(在磁盘)
- 用于判断是否缺段
4. 访问位(Accessed Bit):
- 1:段最近被访问
- 0:段未被访问
- 用于段置换算法
5. 修改位(Dirty Bit):
- 1:段被修改过(需要写回磁盘)
- 0:段未被修改(可以直接丢弃)
- 用于段置换
6. 保护位(Protection Bit):
- R:只读
- W:写权限
- X:执行权限
- 用于内存保护
7. 外存地址(External Address):
- 段在磁盘上的位置
- 用于缺段时从磁盘读取
- 可以是块号或文件位置
缺段中断(Segment Fault)
缺段中断是请求分段的核心机制,当访问不在内存中的段时触发。
缺段中断:
触发条件:
- 访问段表项时,有效位=0
- 表示段不在内存中
- 需要从磁盘装入
处理过程:
1. 访问段表项,发现有效位=0
2. 产生缺段中断
3. 保存当前进程状态
4. 转入缺段中断处理程序
5. 检查段是否合法(段号检查)
6. 查找空闲内存区域
7. 如果无空闲区域,选择置换段
8. 从磁盘读取段到内存
9. 更新段表项(有效位=1,设置段基址)
10. 恢复进程执行
缺段中断处理流程:
用户程序执行
│
├─→ 访问内存地址
│
├─→ 地址转换,查段表
│
├─→ 发现有效位=0(缺段)
│
├─→ 产生缺段中断
│
├─→ 进入内核态
│
├─→ 缺段中断处理程序
│ │
│ ├─→ 检查段号合法性
│ │
│ ├─→ 查找空闲内存区域
│ │ │
│ │ ├─→ 有:直接使用
│ │ │
│ │ └─→ 无:选择置换段
│ │ │
│ │ ├─→ 被置换段修改过:写回磁盘
│ │ │
│ │ └─→ 被置换段未修改:直接丢弃
│ │
│ ├─→ 从磁盘读取段
│ │
│ ├─→ 更新段表项
│ │
│ └─→ 返回用户态
│
└─→ 重新执行原指令
硬件支持要求
请求分段需要硬件提供以下支持:
硬件支持要求:
1. 段表寄存器(STR):
- 存储段表在内存中的起始地址
- 存储段表长度(段数)
- 支持快速访问
2. 地址转换机构:
- 支持段号到段表项的转换
- 支持段基址+偏移的地址计算
- 支持越界检查
3. 中断机制:
- 支持缺段中断
- 支持保护异常
- 支持越界异常
4. 保护机制:
- 支持权限检查
- 支持访问位和修改位
- 硬件自动检查
5. TLB支持(可选):
- 缓存段表项
- 加速地址转换
- 提高性能
地址转换硬件
地址转换硬件:
基本结构:
┌──────────────┐
│ 段表寄存器 │ → 段表起始地址
└──────┬───────┘
│
↓
┌──────────────┐
│ 段表(内存) │
└──────┬───────┘
│
↓
┌──────────────┐
│ 段表项 │ → 段基址、段长度、有效位等
└──────┬───────┘
│
↓
┌──────────────┐
│ 地址计算 │ → 段基址 + 段内偏移
└──────┬───────┘
│
↓
┌──────────────┐
│ 物理地址 │
└──────────────┘
检查机制:
1. 段号检查:段号 < 段表长度
2. 有效位检查:有效位 = 1
3. 越界检查:段内偏移 < 段长度
4. 权限检查:访问类型是否允许
异常处理:
- 段号越界 → 段越界异常
- 有效位=0 → 缺段中断
- 段内偏移越界 → 段越界异常
- 权限不允许 → 保护异常
3.5.2 分段的共享与保护
分段的共享和保护是分段存储管理的重要特性,它使得多个进程可以共享代码段,同时保护各自的私有数据。
分段的共享
分段共享允许多个进程访问同一个物理段,主要用于共享代码段。
分段共享:
共享方式:
- 多个进程的段表项指向同一个物理段
- 共享段在内存中只有一份
- 多个进程可以同时访问
共享段类型:
1. 代码段:
- 程序代码通常只读
- 多个进程可以共享
- 节省大量内存
2. 数据段:
- 共享数据段
- 需要同步机制
- 用于进程通信
3. 库段:
- 共享库代码
- 动态链接库
- 提高内存利用率
共享实现:
1. 共享段表:
- 系统维护共享段表
- 记录共享段信息
- 管理共享段
2. 段表项设置:
- 共享段的段表项指向同一物理地址
- 设置共享标志
- 使用引用计数
3. 引用计数:
- 记录有多少进程共享该段
- 进程退出时减1
- 计数为0时释放段
分段共享示例
分段共享示例:
场景:
进程1和进程2都运行Word程序
内存布局:
物理内存:
0x1000: Word代码段(共享)
0x5000: 进程1数据段
0x6000: 进程2数据段
进程1段表:
段号 段基址 段长度 类型 共享
──────────────────────────────
0 0x1000 0x4000 代码段 是
1 0x5000 0x2000 数据段 否
进程2段表:
段号 段基址 段长度 类型 共享
──────────────────────────────
0 0x1000 0x4000 代码段 是
1 0x6000 0x2000 数据段 否
共享段表:
段名 物理地址 长度 引用计数
──────────────────────────────
Word代码 0x1000 0x4000 2
工作过程:
1. 进程1访问代码段:
查进程1段表,段0 → 0x1000
访问物理地址0x1000
2. 进程2访问代码段:
查进程2段表,段0 → 0x1000
访问同一物理地址0x1000
3. 两个进程共享同一代码段
节省0x4000字节内存
共享段的引用计数
共享段引用计数:
作用:
- 记录有多少进程共享该段
- 决定何时释放段
- 管理共享段生命周期
工作过程:
1. 进程共享段时:
引用计数 +1
2. 进程退出时:
引用计数 -1
3. 引用计数为0时:
释放共享段
例子:
初始:Word代码段,引用计数=0
进程1启动:引用计数=1
进程2启动:引用计数=2
进程3启动:引用计数=3
进程1退出:引用计数=2
进程2退出:引用计数=1
进程3退出:引用计数=0 → 释放段
实现:
- 共享段表中维护引用计数
- 进程共享时增加计数
- 进程退出时减少计数
- 计数为0时释放内存
分段的保护
分段保护确保进程只能按照允许的方式访问段,防止非法访问。
分段保护:
保护机制:
1. 访问权限:
- 读(R):允许读取
- 写(W):允许写入
- 执行(X):允许执行
- 组合:R/W, R/X, R/W/X
2. 越界检查:
- 检查段内偏移 < 段长度
- 防止越界访问
- 硬件自动检查
3. 段类型检查:
- 代码段:通常只读、可执行
- 数据段:可读写、不可执行
- 栈段:可读写、不可执行
保护实现:
1. 段表项中的保护位:
- 记录段的访问权限
- 硬件检查权限
2. 访问时检查:
- 每次内存访问
- 硬件检查访问类型
- 不符合权限则产生异常
3. 异常处理:
- 保护异常
- 终止进程或处理异常
分段保护示例
分段保护示例:
段表设置:
段号 段基址 段长度 保护位 说明
──────────────────────────────
0 0x1000 0x4000 R/X 代码段(只读、执行)
1 0x5000 0x2000 R/W 数据段(读写)
2 0x7000 0x1000 R/W 栈段(读写)
访问检查:
1. 读取代码段(段0,偏移0x100):
访问类型:读
保护位:R/X
允许读取 ✓
2. 写入代码段(段0,偏移0x100):
访问类型:写
保护位:R/X(无W)
不允许写入 ✗ → 保护异常
3. 执行代码段(段0,偏移0x100):
访问类型:执行
保护位:R/X
允许执行 ✓
4. 读取数据段(段1,偏移0x50):
访问类型:读
保护位:R/W
允许读取 ✓
5. 写入数据段(段1,偏移0x50):
访问类型:写
保护位:R/W
允许写入 ✓
6. 执行数据段(段1,偏移0x50):
访问类型:执行
保护位:R/W(无X)
不允许执行 ✗ → 保护异常
越界保护
越界保护:
检查机制:
- 每次访问时检查段内偏移
- 段内偏移必须 < 段长度
- 硬件自动检查
检查过程:
1. 地址转换时提取段内偏移
2. 从段表项读取段长度
3. 比较:段内偏移 < 段长度
4. 如果越界,产生段越界异常
例子:
段表项:
段基址 = 0x5000
段长度 = 0x2000
访问地址(段号=1, 段内偏移=0x1500):
0x1500 < 0x2000,有效 ✓
访问地址(段号=1, 段内偏移=0x2500):
0x2500 >= 0x2000,越界 ✗ → 段越界异常
保护作用:
- 防止访问其他段
- 防止访问系统区域
- 提高系统安全性
保护与共享的结合
保护与共享:
共享段保护:
- 共享段也需要保护
- 通常设置为只读
- 防止进程修改共享代码
例子:
Word代码段(共享):
保护位:R/X(只读、执行)
多个进程共享
任何进程都不能修改
如果进程试图写入:
产生保护异常
进程被终止或处理异常
保护共享段:
- 确保共享代码不被破坏
- 提高系统稳定性
- 保护系统安全
私有段保护:
- 每个进程的私有段
- 可以设置不同权限
- 保护进程数据
例子:
进程1数据段:
保护位:R/W
只有进程1可以访问
其他进程不能访问
分段保护的优势
分段保护的优势:
1. 细粒度保护:
- 可以按段设置权限
- 代码段、数据段、栈段不同权限
- 灵活的保护机制
2. 符合逻辑结构:
- 段有明确的逻辑意义
- 保护设置直观
- 便于理解和配置
3. 硬件支持:
- 硬件自动检查
- 检查速度快
- 不影响性能
4. 安全性高:
- 防止非法访问
- 防止代码注入
- 提高系统安全
5. 便于调试:
- 保护异常容易定位
- 知道是哪个段的问题
- 便于排查问题
分段共享与保护的总结
分段共享与保护总结:
共享:
- 多个进程共享同一段
- 主要用于代码段
- 节省内存空间
- 使用引用计数管理
保护:
- 按段设置访问权限
- 硬件自动检查
- 防止非法访问
- 提高系统安全性
结合:
- 共享段也需要保护
- 通常设置为只读
- 保护私有段
- 灵活的内存管理
实际应用:
- 现代系统主要使用分页
- 但分段的概念仍然重要
- 段页式结合两者优点
- 提供灵活的内存管理