操作系统之内存管理
物理内存 VS 虚拟内存
物理内存
优点:
- 速度快:RAM是CPU可直接访问的高速存储器,访问延迟远低于磁盘
- 支持多任务运行:多进程在内存中切换迅速
- 可被CPU直接寻址:CPU指令直接对物理地址操作(现代OS都是经过虚拟地址转换的)
缺点:
- 容量有限成本高:RAM价格昂贵,不能无限扩展
- 数据掉电丢失:断电后内容消失
- 程序必须装入内存才能运行:无法直接运行其他存储介质上的程序
虚拟内存
操作系统为每个进程提供一个独立的、统一的、连续的地址空间,通过页表将虚拟地址映射到物理内存或磁盘。
优点:
- 扩充有效内存容量:通过磁盘分页,程序不必全部装入内存
- 每个进程拥有独立空间:安全隔离,避免相互访问内存
- 简化编程模型:程序认为自己拥有完整的连续内存空间
缺点:
- 速度比物理内存慢:页缺失需要从磁盘换页,速度远慢于RAM
- 内核复杂性提高:需要管理页表、交换、高速缓存一致等
- 可能引发抖动:频繁换页导致系统几乎停顿
引入虚拟内存就完全解决物理内存的限制了吗?
不完全解决,而是缓解和抽象
本质原因
虚拟内存通过按需加载和页面置换机制,使得:
- 程序不需要一次性放入内存
- 只有正在使用的部分放入内存
- 不活跃部分换到磁盘中
程序可用的地址空间 ≠ 物理内存大小
解决的问题
- 内存不够放程序,通过分页 + 磁盘换出
- 进程互相干扰,通过每个进程独立地址空间
- 内存碎片,通过页式管理提供连续虚拟空间
- 编程困难,屏蔽了物理内存细节
没有完全解决的问题
- 磁盘比内存慢,缺页会严重影响性能
- 物理内存依旧有限,虚拟内存越大,缺页越频繁
- 并不是无限内存,一旦换页密集,性能崩溃
虚拟地址要映射物理地址,需要操作系统提供 内存管理机制 。
常见的内存管理机制
早期单一连续分配
- 只有一个进程运行
- 程序整个装入内存
- 无虚拟地址概念,直接映射
固定分区分配
物理内存被分成固定大小的分区,每个分区只能装一个进程
- 简单实现
- 内部碎片较多
- 多任务,但不灵活
动态分区分配
内存按需分配不同大小连续空间
典型算法:
- First Fit(首次适配):找第一个足够大的空间
- Best Fit(最优适配):找最小的足够大的空间
- Worst Fit(最差适配):找最大的空间
问题:外部碎片严重,需要紧缩
分页管理(Paging)【现代主流】
虚拟地址和物理地址都划成固定大小的页(Page)/页框(Frame)
- 解决外部碎片
- 页表实现地址映射
- 内存不连续
优点:
- 内存利用率高
- 支持虚拟内存,按需调页
- 支持进程隔离
缺点:
- 页表占空间
- 多级页表复杂度高
- TLB缓存命中影响性能
分段管理(Segmentation)
把内存按逻辑段划分(代码段/数据段/栈段)
- 地址 = 段号 + 段内偏移
- 每段大小可变
优点:
- 符合程序逻辑
- 保护性好
缺点:
- 会产生碎片
段页式管理(Segmentation + Paging)
先按段划分,再对段内分页。结合了逻辑结构和无碎片的优点
按需分页(Demand Paging)
配合虚拟内存使用
- 程序不用全部装入
- 只装用到的页
- 缺页中断触发换页机制
核心算法
- FIFO(先进先出)
- LRU(最近最少使用)
- CLOCK / Second-Chance(实用近似LRU)
内存映射文件(Memory-mapped I/O / mmap)
把文件映射到虚拟内存空间
- 区别于 read/write
- 虚拟内存管理文件
- 常见于数据库、共享内存
什么是分页?
把虚拟地址和物理地址都切成固定大小的小块(页 Page / 页框 Frame),让进程不需要连续的物理内存,也不需要一次性全部装入内存。
分页的三条基本规定
- 虚拟地址 = 页号 + 页内偏移:CPU访问内存先被拆分
- 建立页表(Page Table):记录每页虚拟到物理的映射
- 硬件MMU + TLB协助:加速地址转换
虚拟地址是怎么变成物理地址的?
假设:
虚拟地址:32bit
页大小:4KB(2^12)
那么:
高20位:页号
低12位:页内偏移
虚拟地址 = 20bit + 12bit
然后:
CPU通过页号查找页表,找到对应的物理页框号
最后计算物理地址:
物理地址 = 物理页框号 + 偏移
举例:
页大小 = 4KB
虚拟地址 = 0x00105ABC
分解地址
4KB = 2^12 → 偏移 12 bit
页号 = 0x00105
偏移 = 0xABC
查页表
假设:虚拟页号 0x00105 → 映射到物理页框 0x00300
拼物理地址
物理地址 = 0x00300ABC = 0x00300 + 0xABC
出现的问题
如果每个进程都有一个页表,而虚拟空间4G,每页4KB就等于1048576个页。每项4B 转换成页表大小 就是 4MB。每个进程都有一份,太大了!
所以就出现了
- 多级页表:减少页表占用内存
- TLB:缓存页表加速
- 反置页表:全局页表公用
引入的硬件:
- MMU内存管理单元:负责虚拟地址到物理地址的转换,页表访问和触发缺页异常
- TLB(快表):页表缓存,加速映射,TLB未命中就查页表,页表未命中就缺页中断,从磁盘加载
缺页中断的过程:
- CPU访问虚拟内存
- MMU找不到页映射(先查TLB再查内存页表)
- OS捕获异常
- 从磁盘加载该页到RAM
- 更新页表
- 重新执行指令
什么是分段?
分段(Segmentation)是一种把程序的地址空间按逻辑功能划分的内存管理方式
程序运行时,会被划分成多个逻辑段(Segment),每个段长度不一样
| 段 | 含义 |
|---|---|
| 代码段(Text Segment) | 存放程序指令(只读) |
| 数据段(Data Segment) | 已初始化的全局变量、静态变量 |
| BSS 段 | 未初始化的全局/静态变量 |
| 堆段(Heap) | 动态分配内存(malloc/new) |
| 栈段(Stack) | 函数调用、局部变量 |
这些段都是独立管理,独立保护的。分页是按照固定大小分,分段是按照逻辑意义分。
为什么需要分段?
分页解决了物理内存碎片与虚拟内存不足问题,但分页没有“逻辑意义”,只是切块。而程序本身有逻辑结构的。分段让内存管理更符合程序逻辑,并可以对每段设置不同权限。
优点:
- 按逻辑划分:对应程序结构,方便保护与共享
- 每段可以不同大小:没有内部碎片
- 可权限控制:代码段不可写,栈自动增长
- 支持共享:多个进程共享代码段
缺点:
- 外部碎片:段大小不固定,使内存出现空洞
- 分配复杂:需要找足够连续的块
- 不利于虚拟内存扩展:分段不容易换入换出小块
因此操作系统逐渐以分页为主,分段为辅。
分段思想
虚拟地址 = 段号 + 段内偏移
CPU 访问内存时:
-
取段号(Segment Selector)
-
找该段的段表(Segment Table)
-
获取该段的基址 + 限长(base + limit)
-
物理地址 = 基址 + 偏移量
段表和页表的关系类比
| 分页 | 分段 |
|---|---|
| 页表(Page Table) | 段表(Segment Table) |
| 虚拟页号 + 偏移 | 段号 + 段内偏移 |
| 固定大小 | 不固定大小 |
| 物理页框号 | 段基址 Base |
| 页大小固定 | 段长度 Limit |
段式地址的计算公式
物理地址 = 段基址(Base) + 段内偏移(Offset)
分页:
物理地址 = 页框号(Frame) + 页内偏移
页表告诉:虚拟页 → 物理页框
段表告诉你:段号 → 段基址 Base
多级页表
为了解决页表过大,因此对页表进行了分页,又叫多级页表
核心思想:延迟分配页表,只在需要的虚拟页才建对应页表项
例子
假设 32 位虚拟地址,4KB 页:
虚拟地址:32 bit = [10 bit 一级页表 | 10 bit 二级页表 | 12 bit 页内偏移]
访问过程:
- 取虚拟地址高 10 位 → 一级页表索引,一级页表存放二级页表的物理地址
- 取中间 10 位 → 二级页表索引,二级页表存放实际物理页框号
- 取低 12 位 → 页内偏移
- 物理地址 = 页框号 + 页内偏移
TLB硬件加速
每次访问内存都需要多次查询页表(四级页表需要4次内存访问),性能开销巨大。
TLB是什么?
- 位于CPU内部的高速缓存
- 存储最近使用的虚拟地址到物理地址的映射
- 典型容量:64-1024 项
工作流程
1. CPU生成虚拟地址
2. 首先查询TLB
├─ TLB命中:直接获取物理地址(几个时钟周期)
└─ TLB未命中:通过页表查询(几十到几百个时钟周期)
3. 将新的映射加入TLB
TLB管理
- 进程切换时:需要刷新TLB(或使用ASID/PCID标识进程)
- 页表修改时:需要使TLB失效(通过 invlpg 指令)
- 命中率:通常可达 95-99%
页面置换算法
最优算法(OPT - Optimal)
原理:选择未来最长时间不会被访问的页面换出
- 优点:理论上缺页率最低,作为其他算法的性能基准
- 缺点:无法实现(需要预知未来的访问序列)
- 用途:仅用于理论研究和算法性能评估
先进先出(FIFO)
原理:选择最早进入内存的页面换出,维护一个队列
-
优点:
-
- 实现简单,只需维护一个队列
- 开销小
-
缺点:
-
- 性能差,没有考虑页面使用频率
- 可能换出经常使用的页面
- 存在Belady异常
-
适用:很少单独使用,主要用于教学
最近最少使用(LRU - Least Recently Used)
原理:选择最长时间未被访问的页面换出(基于局部性原理)
-
优点:
-
- 性能接近最优算法
- 符合局部性原理,缺页率低
- 无Belady异常
-
缺点:
-
- 开销大:每次访问都需要更新时间戳或调整链表
- 硬件支持复杂
- 完整实现成本高
-
适用:理论性能好,但实际很少严格实现
时钟算法(Clock / 二次机会算法)
原理:LRU的近似实现,使用访问位(Access Bit),页面组织成环形链表
-
优点:
-
- 比LRU开销小很多
- 性能接近LRU
- 实现简单,只需一个访问位
- 无Belady异常
-
缺点:
-
- 不如LRU精确
- 最坏情况下需要扫描两遍
-
适用:实际操作系统最常用的算法
改进的时钟算法(Enhanced Clock)
原理:使用访问位(A)和修改位(D)两个标志位
-
优点:
-
- 优先换出干净页(无需写回磁盘,更快)
- 减少磁盘I/O
- 性能优于基本时钟算法
-
缺点:
-
- 比基本时钟算法稍复杂
- 最坏情况需要4轮扫描
-
适用:实际系统中广泛使用
最不常用(LFU)
原理:选择访问次数最少的页面换出
-
优点:
-
- 考虑了访问频率
- 对某些访问模式效果好
-
缺点:
-
- 早期访问的页面计数器高,即使后来不用也难以换出
- 需要维护计数器,开销大
- 性能通常不如LRU
-
适用:很少使用,缓存系统中偶尔使用
最近未使用(NRU)
原理:使用访问位(R)和修改位(M),将页面分为4类
类0: (R=0, M=0) - 最近未访问,未修改
类1: (R=0, M=1) - 最近未访问,已修改
类2: (R=1, M=0) - 最近访问,未修改
类3: (R=1, M=1) - 最近访问,已修改
策略:
- 从编号最小的非空类中随机选择一个页面换出
- 定期清除访问位(如每次时钟中断)
优缺点:
-
优点:
-
- 实现简单,开销小
- 性能接近时钟算法
-
缺点:
-
- 精度不如LRU
- 同类中随机选择,可能不够优化
-
适用:一些简单的操作系统
性能对比:
缺页率
OPT < LRU ≈ Clock ≈ Enhanced Clock < NRU < LFU < FIFO
实现复杂度
FIFO < NRU < Clock < Enhanced Clock < LFU < LRU < OPT(不可实现)
分页和分段管理中内存碎片问题
内存碎片种类
内部碎片:已经分配给进程但无法使用的内存空间。
外部碎片:系统中未分配的内存,但由于不连续而无法满足分配请求。
分页管理中的碎片问题
只有内部碎片,产生原因:
- 页的大小固定(如4KB)
- 进程最后一页通常不会刚好用满
- 虚拟地址连续,但物理地址可以不连续
为什么没有外部碎片:任何空闲页框都可以分配给任何进程,不需要连续的物理空间。
分段管理中的碎片问题
主要是外部碎片,产生的原因:
- 段的大小不固定(根据逻辑单元决定)
- 需要连续的物理空间
- 频繁分配和释放导致内存"碎片化"
解决方案
分页中解决内部碎片:
- 减少页面大小
- 可变页大小(大页支持)
- 接受内部碎片
分段中解决外部碎片:
- 内存紧凑
- 分配策略优化
- 分段+分页(段页式管理)
进程复制内存优化:写时复制(Copy - on Write)
传统直接复制方式
过程:
- 分配与父进程相同大小的物理内存
- 复制父进程的所有内存内容到子进程
- 复制页表
- 更新子进程的页表指向新的物理页
问题:
- 时间开销大
- 内存开销翻倍
写时复制(COW)
核心思想:不立即复制,而是共享内存,只有在写入时才真正复制。
3126

被折叠的 条评论
为什么被折叠?



