引言
内存的分配和回收分为两种方式:连续分配管理方式 和非连续分配管理方式。
非连续分配:为用户进程分配的可以是一些分散的内存空间。
非连续分配管理方式分为:
- 基本分页存储管理方式
- 基本分段存储管理方式
- 段页式存储管理方式
1. 基本分页存储管理
1.1 基本分页存储管理的基本概念
基本分页存储管理:操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的页框有一一对应的关系。

页框 :将内存空间分为一个个大小相等的分区(比如:每个分区4KB),每个分区就是一个页框。
页框号:每个页框有一个编号,即"页框号",页框号从0开始.
页框 = 页帧 = 内存块 = 物理块 = 物理页面
页框号 = 页帧号 = 内存块号 = 物理块号 = 物理页号
页 /页面 :将进程的逻辑地址空间也分为与页框大小相等的一个个部分,每个部分称为一个"页"或"页面" 。
页号:每个页面也有一个编号,即"页号",页号也是从0开始。
各个页面不必连续存放,可以放到不相邻的各个页框中。
注:进程的最后一个页面可能没有一个页框那么大。也就是说,分页存储有可能产生内部碎片,因此页框不能太大,否则可能产生过大的内部碎片造成浪费。
逻辑地址和物理地址
-
物理地址:物理地址是数据最终存储在物理内存中的实际地址。
物理地址代表了物理内存条上的一个确切位置。当操作系统或硬件需要读写内存中实际的数据时,它会使用物理地址来定位这些数据。
-
逻辑地址 :又称虚拟地址,是程序中使用的地址。
当编程时,我们创建变量、数组、对象等,这些都是使用的逻辑地址。这个地址是相对地址,相对于程序的起始位置而言的。逻辑地址使得每个程序都可以认为自己是在使用连续的、独立的地址空间。
逻辑地址主要在程序的编译和执行时使用,它通过操作系统的内存管理单元转换为物理地址才能最终访问实际的内存单元。
1.2 重要的数据结构 ------ 页表
为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。页表通常存在PCB中。
页面和页框一一对应关系。

- 一个进程对应一张页表
- 进程的每个页面对应一个页表项
- 每个页表项由"页号"和"块号"组成
- 页表记录进程页面和实际存放的内存块之间的映射关系
- 每个页表项的长度是相同的
- 页表项连续存放,因此页号可以是隐含的,不占存储空间。
1.3 如何实现地址的转换?
-
进程在内存中连续存放时,操作系统是如何实现逻辑地址到物理地址的转换的?
主要依赖于地址重定位技术,重定位寄存器指明了进程在内存中的起始位置,加上目标逻辑地址就是在内存中的物理地址。目标逻辑地址即为相对于起始位置的偏移量。
-
将进程地址空间分页之后,操作系统该如何实现逻辑地址到物理地址的转换?
分页存储管理的逻辑地址结构如下所示:

地址结构包含两个部分:前一部分为页号,后一部分为页内偏移量 W。
特点:虽然进程的各个页面是离散存放的,但是页面内部是连续存放的
如果要访问逻辑地址 A,则
①确定逻辑地址 A 对应的"页号" P
②找到 P 号页面在内存中的起始地址(需要查页表)
③确定逻辑地址 A 的"页内偏移量" W
逻辑地址 A 对应的物理地址 = P 号页面在内存中的起始地址 + 页内偏移量 W
子问题:如何确定一个逻辑地址对应的页号、页内偏移量?
erlang
页号 = 逻辑地址 / 页面长度 (取除法的整数部分)
页内偏移量 = 逻辑地址 % 页面长度(取除法的余数部分)
在计算机内部,地址是用二进制表示的,如果页面大小 刚好是 2 的整数幂,则计算机硬件可以很快速的把逻辑地址拆分成(页号,页内偏移量)
结论:如果每个页面大小为 2KB,用二进制数表示逻辑地址,则末尾 K 位即为页内偏移量,其余部分就是页号。
假设某计算机用32个二进制位表示逻辑地址,页面大小为4kB = 2^12B = 4096:
yaml
0号页的逻辑地址范围应该是 0~4095,用二进制表示应该是:
00000000000000000000000000000000 ~ 00000000000000000000111111111111
1号页的逻辑地址范围应该是 4096~8191,用二进制表示应该是:
00000000000000000001000000000000 ~ 00000000000000000001111111111111
2号页的逻辑地址范围应该是 8192~12287,用二进制表示应该是:
00000000000000000010000000000000 ~ 00000000000000000010111111111111
Eg:逻辑地址 2,用二进制表示应该是 00000000000000000000000000000010
ini
页号 = 2/4096 = 0 = 00000000000000000000
页内偏移量 = 2%4096 = 2 = 000000000010
Eg:逻辑地址 4097,用二进制表示应该是 00000000000000000001000000000001
ini
页号 = 4097/4096 = 1 = 00000000000000000001
页内偏移量 = 4097%4096 = 1 = 000000000001
根据页号可以查询页表,而页表中记录的只是内存块号,而不是内存块的起始地址!
J 号内存块的起始地址 = J * 内存块大小
0号内存块的起始物理地址是 00000000000000000000000000000000
1号内存块的起始物理地址是 00000000000000000001000000000000
2号内存块的起始物理地址是 00000000000000000010000000000000
3号内存块的起始物理地址是 00000000000000000011000000000000
Eg:假设通过查询页表得知1号页面存放的内存块号是9(1001),则9号内存块的起始地址 = 9*4096 = 00000000000000001001000000000000
markdown
则逻辑地址4097对应的物理地址 = 页面在内存中存放的起始地址 + 页内偏移量
=(00000000000000001001000000000001)
结论:如果页面大小刚好是2的整数幂,则只需把页表中记录的物理块号拼接上页内偏移量就能得到对应的物理地址
总结:页面大小 刚好是 2 的整数幂有什么好处?
- 逻辑地址的拆分更加迅速------如果每个页面大小为 2KB,用二进制数表示逻辑地址,则末尾 K 位 即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为 2 的整数幂,计算机硬件就 可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法运算,从而提升了运行 速度。
- 物理地址的计算更加迅速------根据逻辑地址得到页号,根据页号查询页表从而找到页面存放的内 存块号,将二进制表示的内存块号和页内偏移量拼接起来,就可以得到最终的物理地址。
1.4 基本地址变换机构
基本地址变换结构:借助进程的页表将逻辑地址转换为物理地址。
基本地址变换机构可以借助进程的页表将逻辑地址转换为物理地址。
通常会在系统中设置一个页表寄存器(PTR),存放页表在内存中的起始地址 F 和页表长度 M。
进程未执行时,页表的始址 和 页表长度 放在进程控制块(PCB)中,当进程被调度时,操作系统内核会把它们放到页表寄存器中。
注意:页面大小是 2 的整数幂
Eg:设页面大小为L,逻辑地址A到物理地址E的变换过程如下:

1.5 具有快表的地址变换机构
快表:快表,又称联想寄存器(TLB, translation lookaside buffer ),是一种访问速度比内存快很多的高速缓存(TLB不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。
速度越快的存储设备,单位成本越高。因此采用多级存储结构来平衡性能与成本。

有的系统支持快表和慢表同时查找。
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。
因为局部性原理,一般来说快表的命中率可以达到 90% 以上。
局部性原理:
-
时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)
-
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)
1.6 两级页表
若某计算机系统按字节寻址,支持 32 位的逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为 4B。4KB = 212B,因此页内地址要用12位表示,剩余 20 位表示页号。
因此,该系统中用户进程最多有 220 页。相应的,一个进程的页表中,最多会有 220 = 1M = 1,048,576 个页表项,所以一个页表最大需要 220 * 4B = 222 B,共需要 222/ 212 = 210 个页框存储该页表。
根据页号查询页表的方法:K 号页对应的页表项存放位置 = 页表始址 + K * 4。
要在所有的页表项都连续存放的基础上才能用这种方法找到页表项。根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页表都常驻内存。
单级页表存在的问题:
- 问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
对策:把页表再分页并离散存储,然后再建立一张页表记录页表各个部分的存放位置,称为页目录表,或称外层页表,或称顶层页表
-
问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。
对策:可以在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存。
若想访问的页面不在内存中,则产生缺页中断(内中断/异常),然后将目标页面从外存调入内存。
细节:
-
若分为两级页表后,页表依然很长,则可以采用更多级页表,一般来说各级页表的大小不能超过一个页面
-
两级页表的访存次数分析(假设没有快表机构)
- 第一次访存:访问内存中的页目录表
- 第二次访存:访问内存中的二级页表
- 第三次访存:访问目标内存单元
2. 基本分段存储管理方式
2.1 分段
分段 :进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址。
内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。
由于是按逻辑功能模块划分,用户编程更方便,程序的可读性更高。
LOAD 1, D | <A>; //将分段D中A单元内的值读入寄存器1
STORE 1, X | <B>; //将寄存器1的内容存入X 分段的B单元中
分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。如:

段号的位数决定了每个进程最多可以分几个段
段内地址位数决定了每个段的最大长度是多少
分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。
分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。
程序员写代码时,必须同时给出:
- 段号(你要访问哪个模块)
- 段内偏移(你要访问模块里的哪个位置)
与分页对比:
- 分页:逻辑地址 = 页号 + 页内偏移,但程序员不需要知道页号,系统自动算
- 分段:逻辑地址 = 段号 + 段内偏移,程序员必须显式指定段号
2.2 段表
问题:程序分多个段,各段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段的存放位置。为此,需为每个进程建立一张段映射表,简称"段表"。
-
每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称"基址")和段的长度。
-
各个段表项的长度是相同的。例如:某系统按字节寻址,采用分段存储管理,逻辑地址结构为(段号16位, 段内地址16位),因此用16位即可表示最大段长。物理内存大小为4GB(可用32位表示整个物理内地址空间)。因此,可以让每个段表项占 16+32 = 48位,即6B。由于段表项长度相同,因此段号可以是隐含的,不占存储空间。若段表存放的起始地址为 M,则 K号段对应的段表项存放的地址为 M + K*6。
2.3 分段、分页对比
页是信息的物理单位 。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。
段是信息的逻辑单位 。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。
页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。
分段比分页更容易实现信息的共享和保护。
不能被修改的代码称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的。可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)
访问一个逻辑地址需要几次访存?
分页(单级页表):第一次访存------查内存中的页表,第二次访存------访问目标内存单元。总共两次访存
分段:第一次访存------查内存中的段表,第二次访存------访问目标内存单元。总共两次访存
与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以少一次访问,加快地址变换速度。
分段的几个实际使用场景:
| 实际场景 | 分段如何解决 |
|---|---|
| 多个程序共享printf | 让它们的"printf段"指向同一块物理内存 |
| 防止程序修改自己的代码 | 将代码段设为"只读+可执行" |
| 栈动态增长 | 栈段可以按需扩展,不受固定页大小限制 |
| 模块化编程 | 每个模块独立成段,便于管理和调试 |
3. 段页式管理方式
3.1 为什么需要段页式管理?
要理解段页式管理,先回顾分页和分段各自的优缺点:
| 管理方式 | 优点 | 缺点 |
|---|---|---|
| 分页管理 | 内存利用率高,无外部碎片,只产生少量内部碎片 | 按物理结构划分,不方便按逻辑模块实现信息的共享和保护 |
| 分段管理 | 按逻辑结构划分,方便实现信息的共享和保护 | 段很长时需要分配很大的连续空间;会产生外部碎片 |
分段产生的外部碎片可以使用"紧凑"来解决,但时间代价很大。
因此,人们提出了分段与分页的结合------段页式管理。
3.2 段页式管理的基本思想
段页式管理 = 先分段,再对每个段分页
示例:
makefile
进程逻辑结构:
┌─────────┐
│ 0号段 │ 7KB → 分为 2个页面:4KB + 3KB
├─────────┤
│ 1号段 │ 10KB → 分为 3个页面:4KB + 4KB + 2KB
├─────────┤
│ 2号段 │ 4KB → 分为 1个页面:4KB
└─────────┘
内存空间: 划分为大小相等的内存块(页框/物理块),每块4KB
内存块0: [进程0号段页面0]
内存块1: [进程1号段页面0]
内存块2: [进程0号段页面1]
内存块3: [进程2号段页面0]
内存块4: [进程1号段页面1]
内存块5: [进程1号段页面2]
这样既保留了分段的逻辑清晰性,又通过分页消除了外部碎片。
3.3 逻辑地址结构
分页管理 的逻辑地址:页号 + 页内偏移量(一维)
分段管理 的逻辑地址:段号 + 段内地址(二维)
段页式管理 的逻辑地址:段号 + 页号 + 页内偏移量(二维)
scss
地址结构示例(按字节寻址):
┌──────────────┬───────────┬────────────────┐
│ 段号(16位) │ 页号(4位) │ 页内偏移量(12位) │
└──────────────┴───────────┴────────────────┘
由此可得:
- 段号16位 → 每个进程最多有 2^16 = 64K 个段
- 页号4位 → 每个段最多有 2^4 = 16 个页
- 页内偏移量12位 → 页面大小 = 2^12 = 4KB
关键点: 用户编程时只需要给出段号和段内地址,系统自动将段内地址拆分为页号和页内偏移量。分段对用户可见,分页对用户不可见。
3.4 段表与页表结构
段表结构对比
| 分段管理的段表 | 段页式管理的段表 | |
|---|---|---|
| 组成 | 段号、段长、起始地址 | 段号、页表长度 、页表存放块号 |
| 作用 | 直接定位段在内存中的位置 | 定位该段对应的页表存放位置 |
页表结构
段页式管理中的页表与分页管理中的页表结构相同。
- 一个进程只有一个段表
- 每个段对应一个页表 ,因此一个进程可能有多个页表
3.5 地址转换过程
地址转换需要三次访存(无快表时):
makefile
逻辑地址: [段号s | 页号p | 页内偏移量d]
步骤1: 取段号s,与段表寄存器中的段表长度比较
若 s ≥ 段表长度 → 越界中断,结束
若合法 → 继续
步骤2: 计算段表项地址 = 段表始址 + s × 段表项大小
从内存中读出段表项 → 获得页表存放块号f₁
步骤3: 访问内存块f₁,读出该段的页表
取页号p,与页表长度比较
若 p ≥ 页表长度 → 越界中断,结束
若合法 → 继续
步骤4: 计算页表项地址 = f₁对应的页表始址 + p × 页表项大小
从内存中读出页表项 → 获得物理块号f₂
步骤5: 物理地址 = f₂ × 块大小 + 页内偏移量d
访问目标内存单元
访存次数分析:
- 第1次:访问段表
- 第2次:访问页表
- 第3次:访问目标内存单元
引入快表后: 以段号+页号作为查找关键字。若快表命中,直接获得物理块号,仅需1次访存(访问目标内存单元)。
3.6 总结
| 对比项 | 分页管理 | 分段管理 | 段页式管理 |
|---|---|---|---|
| 划分单位 | 物理块(固定大小) | 逻辑段(可变大小) | 段内再分页 |
| 地址结构 | 一维 | 二维 | 二维 |
| 外部碎片 | 无 | 有 | 无 |
| 内部碎片 | 有(少量) | 无 | 有(少量) |
| 信息共享保护 | 不方便 | 方便 | 方便 |
| 用户可见 | 分页不可见 | 分段可见 | 分段可见,分页不可见 |
| 段表内容 | 无段表 | 段号、段长、起始地址 | 段号、页表长度、页表块号 |
| 页表内容 | 页号→块号 | 无页表 | 页号→块号(与分页相同) |
| 无快表访存次数 | 2次 | 1次 | 3次 |
| 有快表访存次数 | 1次 | 1次 | 1次 |