408 · 操作系统 · 第 3 章(上)

内存管理导图

这一章只回答一个问题:一块内存怎么分给多个进程。先是连续分配(进程占一整段),碎片越来越麻烦,于是改成把进程切成页随便放,再用页表记住每页放在哪儿。

按需切一段给你分区大小跟着进程走 → 剩下的空隙会太小,产生外部碎片
按固定大小整块给你分区或页框大小固定 → 装不满的部分浪费,产生内部碎片
判断碎片类型就看这一条浪费在分出去的区域里面是内部碎片,在区域之间是外部碎片
一 · 演进导图

每一步都在解决上一步留下的问题

内存管理把有限的内存分给多个进程,还要互不干扰(地址变换 + 存储保护)
连续分配一个进程必须占内存里连续的一整段

单一连续分配

内部碎片
做法
内存分系统区和用户区,用户区只放一道程序
新问题
只能单道,CPU 和内存利用率都低

固定分区分配

内部碎片
做法
用户区事先切成若干固定分区(大小可相等或不等),每区放一道程序;用分区说明表管理
新问题
程序比分区小就浪费,比最大分区还大就装不下

动态分区分配

外部碎片
做法
进程来了按它的实际大小现切一块,用空闲分区表或空闲分区链登记
新问题
来来去去之后,空闲区被切成很多小块,单块都放不下新进程
选哪块
这就是四种分配算法要解决的问题

紧凑(拼接)

事后消除外部碎片
做法
把已分配的进程搬到一起,空闲区拼成一大块
代价
搬家费时;必须用动态重定位(重定位寄存器),否则搬完地址全错
非连续分配进程可以拆开,分散放进内存的不同位置

基本分页

内部碎片(仅最后一页)无外部碎片
做法
内存切成等大的页框,进程切成同样大的页,任意一页可以放进任意一个空闲页框
为什么没外部碎片
所有页框一样大,只要还有空闲页框就一定放得下
新问题
页分散了,得记住“第几页放在哪个块”

页表

做法
每个进程一张页表:页号 → 物理块号;PCB 里存页表起始地址和长度,运行时装进页表寄存器 PTR
新问题
每次取数据要先访存查页表,再访存取数据,一共 2 次

快表 TLB

做法
把最近用过的页表项缓存在高速硬件里(局部性原理,和 Cache 同一个道理)
效果
命中时只需 1 次访存

多级页表

解决
单级页表必须连续存放,逻辑地址空间一大,页表就要占一大段连续内存。把页表本身也分页,再用页目录管理
代价
多一级就多一次访存(二级页表无 TLB 时 3 次)

基本分段

外部碎片
做法
按逻辑模块(代码段、数据段、栈段)划分,段长不等,段内连续;用段表记录段基址和段长
好处
便于共享和保护;但段长可变,又回到了动态分区的外部碎片问题

段页式

内部碎片无外部碎片
做法
先分段(给程序员看的逻辑结构),段内再分页(给内存分配用)
代价
段表 → 页表 → 数据,无 TLB 时 3 次访存

下一步 → 虚拟内存:上面这些方案都要求进程整个装进内存。请求分页只装一部分,缺页时再调入,内存满了就用置换算法(OPT / FIFO / LRU / CLOCK)换出。

二 · 动态分区的四种分配算法

区别只在空闲分区怎么排序

记住每种算法的排序方式,特点和缺点都能推出来。选择题最常考:哪种算法综合最好、哪种产生最多小碎片。

算法空闲分区排序怎么找优点缺点
首次适应 FF地址递增从头找,第一个够大的就用综合性能最好,算法开销小;高地址保留了大块低地址端留下很多小碎片,每次查找都要从它们身上跳过
邻近适应 NF地址递增(循环链表)从上次找到的位置往后接着找空闲区分布更均匀,查找开销小高地址的大块也会被切碎,通常比首次适应差
最佳适应 BF容量递增找能放下的最小块大块被保留下来,给大进程用切剩的边角最小,产生最多难以利用的小碎片;回收后要重新排序
最坏适应 WF容量递减永远切最大的那块切剩的部分还比较大,还能用大块很快被用完,大进程来了没地方放;回收后要重新排序
回收 · 只和前一块相邻与前一空闲区合并,改它的大小,表项数不变
回收 · 只和后一块相邻与后一空闲区合并,改起址和大小,表项数不变
回收 · 前后都相邻三块合成一块,表项数减 1
回收 · 前后都不相邻新增一个表项,表项数加 1
三 · 动手看 · 同一串请求,五种分配方式

外部碎片是怎么来的,分页又是怎么绕开的

内存 100 KB。依次执行:A 30、B 20、C 25、D 15,释放 A、C,再申请 E 8、F 12、G 30(单位 KB)。切换算法,一步步往下点,看最后一步 G 能不能放下。分页模式下页框大小为 5 KB,共 20 个页框。

    四 · 分页的地址变换

    页表就是一张“页号 → 块号”的对照表

    逻辑地址拆成页号和页内偏移,用页号查页表得到块号,偏移原样不动,拼起来就是物理地址。

    页面大小 L = 1 KB = 1024 B
    页号 P = ⌊A / L⌋2
    页内偏移 W = A mod L452

      该进程的页表(页表长度 M = 5)

      页表项里只存块号,不存页号:页号就是下标,由“页表起始地址 + P × 页表项长度”直接算出位置。

      2 次基本分页,无快表
      1 次快表命中
      3 次二级页表,无快表
      3 次段页式,无快表
      五 · 两者的关系

      分页和首次适应这些算法,能不能共存

      一句话

      首次适应等算法是为了“大小不一的连续分配”而设计的:块大小不一样,才需要挑选放哪块。分页让每一块都一样大,挑选这件事就不存在了。所以对同一块内存,两者是替代关系;但只要系统里还有“大小可变、又必须连续”的东西,这些算法就还会用上。

      分页是怎么消除外部碎片的?

      外部碎片的根源是:剩下的空隙太小,装不下一个必须连续的进程。分页同时拿掉了这两个条件:

      • 进程不用连续了,拆成页分散放;
      • 页框一样大,任何一个空闲页框都放得下任何一页。

      代价是换成了内部碎片(每个进程最后一页装不满,平均半页),外加页表占用的空间和查表的时间。

      分页以后还用首次适应吗?

      给进程分页框时不用。哪个空闲页框都一样,从空闲页框链表或位示图里拿一个就行,没有“挑哪一块最合适”的问题。

      所以首次适应、最佳适应这类算法属于动态分区(和分段),不属于分页。

      哪些地方它们会共存?

      • 分段系统:段长可变,段在内存里必须连续,所以给段找位置时仍然用首次适应、最佳适应这些算法,也仍然有外部碎片,可能要紧凑。
      • 页表本身:单级页表要占一段连续内存。逻辑地址空间一大,页表就大,连续分配的麻烦又回来了。多级页表把页表也分页,就是为了消除这个问题。
      • 段页式:分段只负责逻辑结构(共享、保护),内存按页框分配,所以段页式里又用不上这些算法,也没有外部碎片。
      • 紧凑和分页:都是对付外部碎片的办法。紧凑是碎片出现后再搬家合并,开销大;分页是从源头上不让外部碎片出现。
      六 · 碎片速查 & 易错点

      选择题直接对表

      方案内部碎片外部碎片一句话原因
      单一连续分配有无整个用户区给一道程序,装不满就浪费
      固定分区分配有无分区大小固定,程序装不满分区
      动态分区分配无有按需切,切剩的小空隙放不下别人;可用紧凑解决
      基本分页有(仅最后一页)无页框固定大小,最后一页装不满
      基本分段无有段长可变且段内连续,本质是动态分区
      段页式有(每段最后一页)无内存按页分配
      “最佳”并不最好最佳适应产生的小碎片最多;综合性能最好的是首次适应。
      紧凑的前提必须支持动态重定位,搬完只改重定位寄存器;静态重定位做不了紧凑。
      页号不存在页表项里页号是下标。页表项长度按块号所需位数算,再按字节对齐。
      页内偏移的位数由页面大小决定:1 KB 页 → 10 位。页号位数 = 逻辑地址位数 − 偏移位数。
      越界检查P ≥ 页表长度 M 时产生越界中断,注意是“≥”:页号从 0 开始。
      分页对用户透明页是系统切的,一维地址;段是程序员划分的,二维地址(段号 + 段内偏移)。