3. 内存管理

一、整条主线:从写代码到 CPU 执行指令

完整流程:编辑 → 编译 → 链接 → 装入内存 → 运行

  1. 编辑:写 C 源码(int x=10; x=x+1;)
  2. 编译:高级语言 → 目标模块(.o),每个目标模块内部地址从 0 开始(局部逻辑地址)
  3. 链接:把多个目标模块 + 库合并,生成装入模块(.exe可执行文件)。
    装入模块里所有地址是相对于模块起始位置 0 的逻辑地址(相对地址)。
  4. 装入(装载):操作系统把 exe 放到物理内存,把逻辑地址翻译成物理地址,这个翻译动作就叫重定位。

核心问题:exe 指令里写的是地址79(逻辑地址,假设模块从 0 开始),如果操作系统把这个程序放到内存100 号位置开始,那变量 x 真实物理地址应该是100+79=179。
如果不做地址转换,CPU 去访问物理地址 79,那是别的进程的内存空间,直接出错!
→ 三种装入方式,本质就是什么时候做这个地址加法。

三种装入方式对比

1. 绝对装入

编译的时候提前知道程序将来会放在内存哪一块物理地址,编译直接生成物理地址。

装入内存的时候一次性完成地址转换(静态:运行前完成)

缺陷:内存不够就装不进去;运行时不能挪位置。

3. 动态运行时装入(动态重定位)【现代 OS 主流】

装入的时候不做地址转换! 指令里仍然保留逻辑地址;等到 CPU 真正执行这条指令那一刻,才做地址加法。

⚠️考点区分:
静态重定位:装入时一次性改指令内部的地址,指令里面存的是物理地址;
动态重定位:指令永远存逻辑地址,硬件实时计算,指令本身不会被修改。

简答考点

问:静态重定位和动态重定位的区别?

  1. 重定位时机:静态在装入时;动态在指令执行时
  2. 是否修改指令:静态修改指令内地址;动态指令始终存逻辑地址,不修改指令
  3. 运行能否移动:静态不能移动;动态可以移动
  4. 硬件支持:静态不需要专门寄存器;动态必须重定位寄存器
  5. 内存分配:静态需要连续、一次性全部装入;动态支持不连续、按需装入

扩展:和后面章节的关联

从写程序到程序运行
编译:由编译程序将用户源代码编译成若干个目标模块(编译就是把高级语言翻译为机器语言)
链接:由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块
装入(装载):由装入程序将装入模块装入内存运行1

对于链接做解释
源代码.c → 编译 → 多个独立目标模块.o(每个模块内部地址都是从 0 开始的局部逻辑地址,模块之间的外部调用符号是未解析的)
然后进入链接阶段,链接的本质:解析模块之间的外部符号引用,合并地址空间,把零散目标模块拼成一个统一地址空间的程序;链接分 3 类,区别只在于链接发生在哪个时间点。
链接完成后得到装入模块,再执行装入,放入内存,把逻辑地址转为物理地址。

一句话区分:编译是高级语言→机器码;链接是把多个.o拼起来;装入是把程序放到内存。

链接的三种方式

  1. 静态链接(运行前链接)

时机:程序运行之前(编译之后、装入之前)一次性完成链接

  1. 操作:把所有目标模块 + 需要用到的库函数,全部合并、重定位,拼成一个完整、独立的可执行装入模块。所有外部符号全部解析完毕,形成一套连续完整的逻辑地址。
  2. 特点:
  1. 408 考点:静态链接生成完整的装入模块,链接发生在装入内存之前。

  2. 装入时动态链接(边装入、边链接)
    时机:把目标模块装入内存的过程中,同步完成链接

  3. 操作:不提前把所有模块拼成一个大文件。把各个独立.o逐个加载进内存,装入一个模块,就立刻解析这个模块的外部符号,完成链接。

  4. 特点:

  1. 考点:链接动作发生在装入阶段,运行前所有模块都已经载入内存并完成链接。

  2. 运行时动态链接(最灵活,就是动态库 dll/so)

时机:程序已经跑起来,执行过程中,需要用到某个模块的时候,才去链接 + 载入

  1. 操作:程序启动时,只装入主模块。其余目标模块暂时不加载;只有运行过程中调用到这个模块里的函数,操作系统才去磁盘找到这个模块,载入内存,完成符号链接。
  2. 特点(高频简答):
    • ✅ 用不到的模块,完全不装入内存,节省内存;
    • ✅ 模块可以共享:多个进程可以共用内存里同一份动态库;
    • ✅ 库升级只需要替换库文件,程序不需要重新编译链接;
  3. 缺点:运行时才加载,第一次调用模块会有一点点加载开销。
  4. 408 例子:Windows 的.dll、Linux 的.so共享库就是这个。
链接方式 链接发生时间 模块是否提前合并 核心特点
静态链接 运行前,装入内存之前 全部合并成单个装入模块 exe 包含全部代码;库更新必须重链接
装入时动态链接 装入内存的过程中 不提前合并,装入时逐个链接 全部模块最终都会载入内存
运行时动态链接 程序运行过程中,按需加载 不合并,需要才载入链接 按需装入,支持模块共享,节省内存

链接和装入的过程 一定不要弄混

如果采用的装入方式是动态重定位 采用的链接方式是运行时动态链接 完整的流程是

  1. 编译阶段
    主模块、模块 A、模块 B 各自编译,各自生成.o目标文件,每个模块内部都有从 0 开始的局部逻辑地址。此时各个模块互相不知道对方的地址。

  2. 程序启动
    只把主模块加载进内存,主模块的局部逻辑地址,在链接时映射到进程虚拟地址空间一段区域。
    此时A、B 模块还在磁盘,完全不进内存。

  3. 运行到调用 A 模块函数的语句

  1. 执行 A 模块里面的指令(动态重定位)
    CPU 取 A 里面的指令,指令里存的是逻辑地址;硬件用基址寄存器(重定位寄存器),在指令执行的那一刻,实时把逻辑地址换算成物理地址。

重点:动态重定位是取指令执行的时候硬件现场转换,不是装入模块的时候一次性转换。

二者独立,可以自由搭配:
运行时动态链接(软件按需加载模块) + 动态重定位(硬件运行时地址转换),就是现代操作系统加载动态库的标准组合。

内存管理

包括一下几个方面

内存是有限资源,多个进程并发运行,OS 要解决两件事:1.运行的时候 给进程分配空间 记录哪些是空闲 哪些已经分配 2. 回收 把进程占用的内存回收

物理内存容量有限,但是可以利用外存(磁盘),让程序的总地址空间大于物理内存大小。也就是虚拟内存的核心思想。程序的一部分放在磁盘,只有需要的时候才调入内存,看起来内存 “变大了”。

例子图:进程 1 物理地址 100~279;访问不在这个区间就报错。

方法 2:重定位寄存器 + 界地址寄存器(最常考!)

进程的内存映像
‼️‼️薄弱地方 容易忘!!

虚拟地址空间
CPU 给进程用的虚拟地址是 n 位,那么虚拟地址编号总数就是 2 n 2^n ,也就是虚拟地址空间大小。
例:32 位虚拟地址 → 2 32 2^{32} = 4GB 虚拟地址空间
64 位虚拟地址,一般只用 48 位做地址,那虚拟空间就是 2 48 2^{48} 。

32 位系统进程虚拟地址空间共 4GB,操作系统将其划分为低 3GB 用户区与高 1GB 内核区。
用户区属于进程私有;内核区在每个进程虚拟地址空间中都存在,所有进程的内核虚拟地址映射到同一内核物理内存。
这样设计,进程切换时内核部分页表无需更换,降低上下文切换开销;用户态程序无法访问内核区,实现权限隔离。

同一进程用户态与内核态切换,不更换页表;进程之间切换,才更换页表。内核区在每个进程的页表预先映射,所以内核态可以直接用当前进程的虚拟地址访问内核代码。

举个例子

进程 A 正在运行,调用 read:

分区从上到下(地址从高→低)

1. 操作系统内核区(最高 1GB,0xC0000000 ~ 0xFFFFFFFF)

存放:操作系统内核代码、内核数据、PCB 进程控制块等。
✅权限:用户态进程不能直接访问,只有内核态才能读写。

所有进程共享同一份内核,但是在每个进程的虚拟地址空间里都映射到这片高位区域

  1. 用户栈 Stack(用户区最高位置,向下增长)
    对应 C 例子:int d=4; main 函数里面的局部变量,就放在栈。

  2. 共享库存储映射区
    存放动态链接库(共享库),比如printf的代码。
    对应前面学的运行时动态链接:调用库函数时,才把共享库映射到这个区域。多个进程可以共享同一份库代码,节省物理内存。

  3. 堆 Heap(向上增长)

C 例子:int *p = (int *)malloc(...),指针变量p本身在栈,p 指向的数组空间,在堆。

  1. 读 / 写数据段(.data/.bss)

C 例子:int a=1;(全局变量)、static int c=3;(静态变量),放在这里。
✅特点:进程启动时就分配好,进程整个生命周期都存在。

  1. 只读代码 / 只读数据段(.text + .rodata)
  1. 未使用区
    0 地址附近,空的。禁止访问,访问触发段错误。

覆盖与交换
(这个知识点了解即可)

关于一些容易混淆概念的辨析
1)连续、非连续分配
解决的问题:进程放到物理内存时,内存地址是一整块连续,还是分散多个块。

这两个是分配的规则。覆盖、交换可以搭配连续分配一起用(早期系统都是连续分配 + 覆盖 / 交换)。

2)覆盖技术
属于内存扩充,目标:一个程序太大,放不下内存,同一时刻只加载不会并发执行的模块。
底层一般依托连续分配实现,但是它本身不是分配方式,是扩充手段。
3)交换(对换)技术
也属于内存扩充,目标:多道程序环境,内存进程太多,把整个进程暂时挪到磁盘,腾出内存。
同样,交换本身不是分配方式,只是内存紧张时,在内外存搬运整个进程的机制

和虚拟内存的关系(考试高频坑)

覆盖、交换 ≠ 虚拟内存。

简单一句话区分:
虚拟内存是OS 自动按需加载一小部分(页);
交换是OS 搬整个进程;
覆盖是程序员提前规划好,同一程序内部模块互相覆盖。

  1. 覆盖技术
    让程序中不会同时执行的代码段,共享同一块内存空间,以此解决内存不足的问题。不需要把整个程序一次性全部载入内存,只加载当前需要用到的部分。

关键特点

  1. 程序员手动划分覆盖结构,操作系统只负责自动完成覆盖的加载;
  2. 对用户不透明,写代码时就要规划好哪些模块互斥、可以覆盖,编程负担很重;
  3. 属于早期操作系统技术,现在已经淘汰;
  4. 局限:只能解决单个程序大小超过内存的问题,不能解决多进程内存紧张。

一句话:覆盖是同一个程序内部,代码段互相覆盖,程序员手动安排。

  1. 交换技术(对换技术)
    内存空间不够时,把整个进程从内存换出到外存磁盘;等内存有空、进程具备运行条件,再把整个进程换回内存。是进程级别的内外存调度。
    中级调度(内存调度)本质就是交换:决定把哪个挂起在外存的进程重新调入内存。

  2. 就绪挂起:进程在磁盘,已经就绪,等待调入内存;

  3. 阻塞挂起:进程在磁盘,还在等待事件。
    磁盘分为两块区域:

  4. 文件区:存放普通文件,追求空间利用率,用离散分配,读写慢;

  5. 对换区:专门存放被换出的进程,追求读写速度,用连续分配,I/O 速度更快。

交换的细节要点

  1. 什么时候换?内存紧张(比如缺页频繁)的时候触发交换;
  2. 换谁出去?优先换阻塞进程、低优先级进程;
  3. ⚠️ PCB(进程控制块)常驻内存,不会被换出外存;
  4. 作用:解决多道程序环境下内存不足的问题,多个进程竞争内存。

一句话:交换是进程整体在内存和外存之间换入换出,操作系统自动完成,对程序员透明。

  1. 覆盖:同一程序,互斥模块共享内存,程序员负责划分,不透明;
  2. 交换:进程整体内外存互换,中级调度就是交换,OS 自动完成;
  3. 交换带来挂起态:就绪挂起、阻塞挂起;PCB 永远留在内存;
  4. 磁盘对换区速度 > 文件区;
  5. 区分:覆盖是段级、程序内;交换是进程级、多进程之间;二者都不是虚拟内存

内存分配

单一连续分配
在单一连续分配方式中,内存被分为系统区和用户区。
系统区通常位于内存的低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程相关数据。
✅ 优点

  1. 最简单,实现成本低
  2. 无外部碎片:用户区本身就是一整块连续空闲空间
  3. 可以搭配覆盖技术,解决程序大于用户区内存的问题
  4. 早期简单系统(MS-DOS)甚至可以不做内存保护

❌ 缺点

  1. 单任务,同一时间只能跑一个用户程序,无法多道程序
  2. 有内部碎片:进程只需要一部分用户区,剩下的空间分给了进程、不能被别的进程使用
  3. 内存利用率极低
  1. 分区大小相等:所有分区一样大。适合多个相同任务的场景;灵活性差。
  2. 分区大小不等:有大有小,适配不同大小进程,更常用。

OS 维护分区说明表,记录每个分区:分区号、大小、起始地址、分配状态(已分配 / 未分配)。当进程要装入内存,查表,找一个能装下它、且未分配的分区分配。

✅ 优点

  1. 实现简单;无外部碎片(分区固定,空闲分区都是完整分区)
  2. 支持多道程序并发,是最早的多道内存管理方式

❌ 缺点

  1. 一定会产生内部碎片:分区大小固定,进程占不满整个分区,剩余空间浪费;分区越大,浪费越严重
  2. 灵活性差:如果进程太大,所有分区都装不下,此时只能用覆盖技术
  3. 分区数量固定,系统能同时运行的进程上限固定

举例子:分区大小 6MB,进程只需要 4MB,剩下 2MB 就是内部碎片,无法给别的进程

- 动态分区分配

进程装入内存的时候,才根据进程实际需要的大小,从空闲内存里切一块刚好合适的连续空间分配给它。进程退出,回收这块分区。分区大小、分区数量都是动态变化的。

要解决 3 个核心问题:

  1. 用什么数据结构记录空闲内存?(空闲分区表 / 空闲分区链)
  2. 多个空闲分区都满足需求时,选哪一块?→ 四种分配算法:首次适应、循环首次适应、最佳适应、最坏适应
  3. 分配、回收分区怎么操作?回收时要判断相邻分区是否空闲,进行分区合并。

✅ 优点

  1. 没有内部碎片!分配多大就切多大,分配给进程的空间没有浪费
  2. 分区按需创建,灵活性强,内存利用率相比固定分区更高

❌ 缺点

  1. 产生外部碎片。进程不断申请、释放内存,空闲内存被切割成很多零散小空闲块;总空闲空间够,但没有一块连续空间装下新进程。
  2. 分配回收算法有开销;外部碎片严重时需要紧凑(内存紧缩),把碎片拼在一起,但是紧凑需要移动进程,开销很大。

- 回收内存的 4 种情况

当进程结束,回收它占用的分区,检查相邻上下两块是不是空闲,分 4 类:

  1. 情况 1:上下邻块都不是空闲
    直接在空闲分区表 / 链新增一条记录,把回收的分区作为独立空闲分区。
  2. 情况 2:只有上邻(低地址方向)是空闲
    把回收分区合并到上方空闲分区,修改上方那条记录的分区大小。
  3. 情况 3:只有下邻(高地址方向)是空闲
    把回收分区和下方空闲分区合并,修改下邻空闲分区的起始地址 + 增大分区大小。
  4. 情况 4:上、下邻两块全都是空闲
    三块合并成一大块。删掉下邻那条空闲记录,修改上邻空闲分区的大小。

一句话记忆:看左右邻居,能合并就合并,减少碎片。

动态分区分配算法

核心目标:当多个空闲分区都满足进程大小需求时,按规则选哪一块分配。

  1. 首次适应 First Fit(FF)
    ✅ 思想:从低地址往高地址从头查找,找到第一个能满足大小的空闲分区就分配。✅ 空闲分区排序:按起始地址从小到大(地址递增)。✅ 分配:从链头 / 表开头遍历,找到第一个够大的块。✅ 优点:

  2. 综合性能最好;

  3. 优先利用低地址空闲块,高地址的大空闲块尽量保留,给后面大进程;

  4. 回收分区时,大多情况不需要重新排序,开销小。✅ 缺点:

一句话:从头找,找到第一个够用的就分。

  1. 最佳适应 Best Fit(BF)
    算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区
    域。因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区

✅ 空闲分区排序:按分区容量从小到大(容量递增)。✅ 分配:顺序查找,找到第一个能满足大小的分区(也就是最小够用块)。✅ 优点:大空闲块不容易被拆分,更容易满足后续大进程。✅ 缺点【高频考点】:

  1. 每次切最小块,剩下的极小碎片非常多,很难再利用(外部碎片严重);
  2. 回收分区时,要按大小重新插入排序,开销大。

一句话:挑刚好够的最小块,“物尽其用”,但碎渣多。

  1. 最坏适应 Worst Fit(WF,最大适应 Largest Fit)

✅ 思想:每次选当前最大的空闲分区分配。

思路:把大块切掉,剩下的剩余空间不至于太小,减少微小碎片。✅ 空闲分区排序:按分区容量从大到小(容量递减)。✅ 分配:从头就是最大块,只要够就分配。✅ 优点:减少微小碎片的产生。✅ 缺点【高频考点】:

  1. 大空闲块很容易被切没;如果后面来了一个很大的进程,没有足够大连续内存,直接分配失败;
  2. 回收同样需要按大小重排,开销大。

一句话:每次拿最大的块去切,防止小碎片,但大块容易被用光。

  1. 最坏适应 Worst Fit(WF,最大适应 Largest Fit)
    思想:每次选当前最大的空闲分区分配。

思路:把大块切掉,剩下的剩余空间不至于太小,减少微小碎片。✅ 空闲分区排序:按分区容量从大到小(容量递减)。✅ 分配:从头就是最大块,只要够就分配。✅ 优点:减少微小碎片的产生。✅ 缺点【高频考点】:

  1. 大空闲块很容易被切没;如果后面来了一个很大的进程,没有足够大连续内存,直接分配失败;
  2. 回收同样需要按大小重排,开销大。

一句话:每次拿最大的块去切,防止小碎片,但大块容易被用光。

  1. 邻近适应 Next Fit(NF,循环首次适应)
    思想:由首次适应改进而来。不再每次都从头开始,从上次查找结束的位置继续往后找,链表可做成循环链表。✅ 空闲分区排序:按起始地址从小到大(和 FF 一样,地址递增)。✅ 优点:不用每次都从低地址从头遍历,减少低地址碎片的重复扫描开销;高低地址分区被使用的概率更均衡。✅ 缺点:

  2. 会把高地址的大空闲分区也拆分消耗掉;

  3. 不像 FF 那样保留高地址大分区,后续大进程更容易分配失败。

一句话:上次查到哪,这次就从哪接着往后找,循环查找。

连续内存分配:4 种动态分区分配算法(文字整理)

算法 算法思想 分区排列顺序 优点 缺点
首次适应 从头到尾找适合的分区 空闲分区以地址递增次序排列 综合看性能最好。算法开销小,回收分区后一般不需要对空闲分区队列重新排序
最佳适应 优先使用更小的分区,以保留更多大分区 空闲分区以容量递增次序排列 会有更多的大分区被保留下来,更能满足大进程需求 会产生很多太小的、难以利用的碎片;算法开销大,回收分区后可能需要对空闲分区队列重新排序
最坏适应 优先使用更大的分区,以防止产生太小的不可用的碎片 空闲分区以容量递减次序排列 可以减少难以利用的小碎片 大分区容易被用完,不利于大进程;算法开销大(原因同上)
邻近适应(循环首次适应) 由首次适应演变而来,每次从上次查找结束位置开始查找 空闲分区以地址递增次序排列(可排列成循环链表) 不用每次都从低地址的小分区开始检索。算法开销小(原因同首次适应算法) 会使高地址的大分区也被用完

本质:这 4 种都属于动态连续分配,分配的是物理内存,程序加载到连续的物理内存块;需要动态重定位。
注意:静态重定位是【装入的时候一次性把所有逻辑地址改成物理地址】,它要求:程序装入内存之后,这个分区不能移动**。
而动态分区分配(首次 / 最佳 / 最坏 / 邻近适应),内存里的分区是可以移动(紧凑)的,所以静态重定位就失效了。**

选择题结论:综合性能首次适应 > 邻近适应 > 最佳 / 最坏适应。

非连续分配管理方式

基本分页存储管理的基本概念
将内存空间分为一个个大小相等的分区
页框=页帧=内存块=物理块=物理页面 是一个概念 他们都有一个编号叫做 页框号=页帧号=内存块号=物理块号=物理页号

将进程的逻辑地址空间也分为与页框大小相等的一个个部分
每个部分称为一个“页”或“页面” 。每个页面也有一个编号,即“页号”,页号也是从0开始
各个页面不必连续存放,可以放到不相邻的各个页框中。
用页表来指示

考点:页越大,内部碎片平均浪费越大;页太小,页表会变得很长。

  1. 页表存放位置:PCB(进程控制块)保存页表起始地址,页表本身存在内存。
  2. 页表项:一行就是一个页表项,记录【页号,页框号】
    • 关键结论:页号是隐含的!不存放在页表项里面
      页表是连续数组,第 P 个表项,天然就对应页号 P,所以页号不用占存储空间,只存页框号。
      例:页表第 0 项,对应 0 号页;内容是 3,代表 0 号页面放在3 号页框。

✅ 重要考点:页表项存的是页框号,不是物理起始地址!
页框 J 的物理起始地址 = 页框号 J × 页面大小。

分页存储的地址转换

分页下,进程页面离散放在内存,不能只用一个基址寄存器,需要查表。
逻辑地址 A,拆成两部分:页号 P + 页内偏移 W

  1. 拆分逻辑地址:

页内偏移的位数,由页面大小决定,页面内部地址连续,页内偏移在逻辑地址、物理地址里完全不变!

  1. 查页表:用页号 P,找到对应的页框号 F
  2. 计算物理地址:
    物理地址 = 页框 F 的起始地址 + 页内偏移 W
    等价公式:物理地址 = F × 页面大小 + W F\times页面大小 + W

基本地址变换机构

基本地址变换机构,也叫页表地址变换机构,是分页存储管理中负责把逻辑地址转换成物理地址的硬件机制。
程序运行时,并不要求整个作业全部连续存放在内存中。一个作业的多个页,可以离散地存放在不同的物理块里。因此,用户程序给出的逻辑地址,不能直接当作内存地址使用,必须经过地址变换。

地址变换的基本过程
第 1 步:根据逻辑地址计算页号和页内偏移量
第 2 步:判断页号是否越界
第 3 步:访问页表,得到物理块号
第 4 步:形成物理地址

基本地址变换机构的组成

  1. 页表寄存器
  2. 页表
  3. 地址加法器
  4. 比较器

第一次访问内存:查页表
第二次访问内存:访问目标内存单元

具有快表的地址变换机构
快表,又称联想寄存器(TLB, translation lookaside buffer ),是一种访问速度比内存快很多的
高速缓存(TLB不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。

一、核心背景

基本分页的原生地址变换:每次访问逻辑地址,必须 2 次访问内存。
第 1 次:访问内存里的页表,拿到内存块号;
第 2 次:用物理地址,访问真正要读写的数据。
两次访存速度很慢。
而程序有局部性原理,很多页表项会被反复查,于是引入TLB 快表,TLB 是高速硬件缓存,只存页表项的副本,查询速度远快于内存。

一句话:TLB = 页表的 Cache;普通 Cache 是内存数据的 Cache,这是考点区分。

带 TLB 的地址变换完整流程(硬件自动完成)

  1. CPU 给出逻辑地址,硬件拆分:页号 + 页内偏移
  2. 硬件同时做两件事:页号合法性越界检查;同时拿页号去查 TLB(快表)
    • ✅ TLB 命中:TLB 里找到对应页号,直接取出内存块号。块号拼接页内偏移得到物理地址。只需要 1 次内存访问(访问目标数据)。

    • ❌ TLB 未命中:去内存查慢表(内存中的页表),拿到内存块号;同时把这个页表项拷贝进 TLB(TLB 满了就用替换算法淘汰旧项);块号拼接偏移得到物理地址,访问目标数据。一共 2 次内存访问(查页表 1 次 + 访问数据 1 次)。

    • TLB 是硬件,访问 TLB 不算 “访存”,访存特指访问主存。

一个串联逻辑的例子
先明确:
TLB:页表项的高速缓存,解决「逻辑地址→物理地址」的地址翻译;
Cache:内存数据块的高速缓存,解决「物理地址拿到之后,快速拿数据」。

执行顺序:CPU 给逻辑地址 →【TLB 做地址转换得到物理地址】→【拿着物理地址去查 Cache 拿数据】

重要区分:
✅ TLB 管地址翻译;Cache 管数据读取。二者是两套独立缓存,可以自由组合 4 种情况。

  1. CPU 给出逻辑地址,硬件拆分页号、页内偏移
  2. 查 TLB,命中,直接拿到内存块号,拼接偏移得到物理地址(耗时 1μs)
  3. 拿着物理地址去查 Cache,Cache 命中,直接从 Cache 读出数据
    ✅ 整个过程不需要访问主存

总耗时:1μs(TLB)

通俗理解:地址翻译在 TLB 搞定;目标数据正好在 Cache 里,全程不碰内存。

  1. TLB 命中 → 算出物理地址(1μs)
  2. Cache 没找到数据,访问主存拿目标数据(100μs)
    ✅ 访存次数:1 次主存(读数据)
    总耗时:1 + 100 = 101 μs

通俗理解:地址翻译很快,但是数据不在 Cache,要去内存读数据。

  1. 查 TLB,未命中。需要访问内存中的页表,拿到内存块号,算出物理地址(1+100 μs)
  2. 拿到物理地址,查询 Cache,Cache 命中,直接读出数据,不用再访问内存
    ✅ 访存次数:1 次主存(读页表)
    总耗时:1 + 100 = 101 μs

通俗理解:TLB 没有页表项,要去内存翻页表做地址转换;但是转换完之后,目标数据刚好在 Cache 里。

  1. TLB 缺失 → 访问内存页表,得到物理地址(1+100 μs)
  2. 拿到物理地址,Cache 缺失 → 再次访问内存读取目标数据(100μs)
    ✅ 访存次数:2 次主存(页表 1 次 + 目标数据 1 次)
    总耗时:1 + 100 + 100 = 201 μs

两级页表

首先要明白为什么要引出两级页表 因为单级页表会有问题
若分为两级页表后,页表依然很长,则可以采用更多级页表,一般来说各级页表的大小不能超过一个页面

问题 1:页表需要占用一大片连续内存

当逻辑地址空间很大(比如 64 位),单级页表会巨大,很难找到一大块连续内存存放整张页表。

最高层:页目录表(一级页表)
它的起始地址存放在进程 PCB + 页表基址寄存器里。CPU 做地址转换第一步,就要读这个表。

两级页表地址变换步骤(无 TLB)

  1. 逻辑地址硬件拆分为:一级页号、二级页号、页内偏移
  2. 页表寄存器存放页目录表起始地址;用一级页号,访问内存的页目录表,取出二级页表的页框号。👉 第 1 次访存
  3. 用二级页号 + 二级页表起始地址,访问内存的二级页表,取出目标页面的页框号。👉 第 2 次访存
  4. 页框号拼接页内偏移,得到物理地址,访问目标数据。👉 第 3 次访存

👉 结论:两级页表,无 TLB,访问 1 个逻辑地址,一共需要 3 次访存!

对比记忆:单级页表无 TLB:2 次访存。级数越多,访存次数越多,这是多级页表的代价。

TLB 仍然是存放页号→物理页框号,直接缓存最终页表映射,不管你是几级页表。

TLB 的好处:抵消多级页表带来的多次访存开销。这也是多级页表能在实际系统使用的关键。

疑问1:关于页表寄存器的疑问
页表寄存器存放:当前进程整个页表在内存中的起始物理地址(页表基址)
页表寄存器存放:页目录表(最高层一级页表)的起始物理地址
TLB 和这个寄存器互不冲突。TLB 命中的时候,直接拿到物理页框号,完全不需要读取页表寄存器、不需要访问各级页表。

页表长度存放在进程 PCB(进程控制块)中。
进程切换时,OS 把 PCB 里的「页表长度」送入 MMU 的
页表长度寄存器
(这个寄存器≠页表基址寄存器 CR3!两个独立寄存器)。

疑问2:多级页表下,越界检查怎么变?
????

基本分段存储管理方式

进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言
中,程序员使用段名来编程),每段从0开始编址内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻
1、段表项里面存什么
每个段表项 = 段长(界限) + 基址(本段在内存的起始物理地址)
每个段表项大小一致的话 段号就是隐含的
分段地址变换完整流程

  1. 逻辑地址拆分为:段号(16bit) + 段内地址(16bit)
  2. 段表寄存器:存段表在内存的起始地址 M(类比分页里的页表基址寄存器)
  3. 硬件计算:段表项地址 = M + 段号 × 段表项长度(6B)
  4. 取出段表项:拿到【基址】和【段长】
  5. 越界检查:段内地址 ≥ 段长 → 地址越界中断!

✅ 分页是:页号 ≥页表长度才越界;分段是:段内地址 ≥段长越界!这个是选择题高频坑!

  1. 合法:物理地址 = 基址 + 段内地址,访问内存

一句话:分页是 “物理切块”,分段是 “逻辑分块”。

分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。如

段号的位数决定了每个进程最多可以分几个 段内地址位数决定了每个段的最大长度是多

分段地址转换
① 逻辑地址进入 MMU 得到段号和段内地址
② 查段表 段表寄存器提供段表起始地址: 段表项地址 = 段表起始地址 + 段号 × 段表项长度
③ 找到那一项段表项 可以看到基址 和段长 检查段内地址是否越界 段内地址 ≥ 段长 → 越界中断
④ 计算物理地址 物理地址 = 基址 + 段内地址

分段、分页管理的对比

页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管
理上的需要,完全是系统行为,对用户是不可见的。
段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻
辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。
页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。
分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。

分段比分页更容易实现信息的共享和保护

不能被修改的代码称为纯代码或可重入代码(不属于临界资源) 试想一下 假设代码段有一部分功能是所有进程都可以访问的
但是因为分页这个操作是系统来完成的 他可能会把共享区和非共享区的数据混在一起放在一个页框里 所以是不方便共享的
但是段的话 这部分逻辑就单独抽成一个段

可以把共享数据单独作为一个段:多个进程的段表都指向这个段,就能实现共享。

段页式管理

为什么要引入段页式
分页管理✅优点:内存利用率高,只有页内碎片,没有外部碎片❌缺点:分页对用户透明,不方便按照逻辑模块做共享、保护

分段管理:✅优点:段是逻辑模块,方便实现段的共享、访问保护❌缺点:会产生外部碎片;段很长时,很难找到一块连续内存放整个段
段页式 = 分段 + 分页,结合两者优点:对进程:先按逻辑模块分段(满足程序员逻辑、共享保护);对每个段:再切分成固定大小的页(消除外部碎片)。

地址变换流程

逻辑地址:段号 S,页号 P,页内偏移 W

  1. 用段号 S和段表寄存器里的段表长度 M 做越界判断:S≥M → 越界中断
  2. 第一次访存:访问内存里的段表,查到该段对应的页表起始地址 + 页表长度
  3. 用页号 P和页表长度做越界判断:P≥页表长度 → 越界中断
  4. 第二次访存:访问内存里的页表,查到对应的内存块号
  5. 拼接:内存块号 + 页内偏移 W,得到物理地址
  6. 第三次访存:访问目标内存单元

👉段页式每个段都有独立页表,不是整个进程一张页表
👉两次越界检查:①段号越界;②页号越界,两处都要判断
👉段页式依然存在页内碎片,没有外部碎片
👉段对用户可见,页对用户不可见。程序员只需要给出段号,系统自动拆分页号和页内偏移

虚拟内存的基本概念

传统内存管理的痛点
❌一次性
❌驻留性

虚拟内存的定义与三大特征

  1. 多次性:作业不用一次性全部装入内存,可以分多次调入内存。(对应传统的 “一次性”)
  2. 对换性:作业运行期间,不需要一直常驻内存,可以换入、换出。(对应传统的 “驻留性”)
  3. 虚拟性:逻辑上扩充内存容量。物理内存大小没变,但是用户可用的地址空间变大。

传统非连续分配:基本分页、基本分段、基本段页。❗基本分页 / 分段:依然要求整个作业一次性装入内存,不支持请求调入,不属于虚拟内存。

内存管理总分支:

  1. 内存分配回收(连续 / 非连续)
  2. 内存扩充
    • 覆盖技术、交换技术(早期)
    • ✅虚拟存储技术(现代 OS 核心)
  3. 地址转换
  4. 存储保护

请求分页存储管理
请求分页三大核心组成:页表机制、缺页中断机构、地址变换机构

一、页表机制(请求分页 vs 基本分页)

基本分页页表:只有【页号 | 内存块号】请求分页页表,在原有基础上新增 4 个字段:

一句话理解新增字段:状态位:判断要不要缺页中断;外存地址:缺页去哪里读;访问位:选谁换出去;修改位:换出去要不要写回磁盘。

假设一个场景:进程一共 10 个逻辑页面;物理内存最多只能给这个进程分配 2 个页框
👉请求页表,必须有 10 个页表项,一页对应一条表项。

每个逻辑页号(0~9),都要有独立的描述信息:

对比基本分页:基本分页 10 页,必须一次性全部装入内存,10 条页表项全部有效。
请求分页:10 条页表项永久存在内存(多级页表的话,页表本身也可以换出外存,但页表项条目总数不变),只是大部分页面的状态位是 0。

延伸:那这 10 条页表本身存在哪里

缺页中断处理流程

  1. 中断发生,保留 CPU 现场;
  2. OS 查询页表,得到缺页在外存的地址;
  3. 检查内存:
    • ✔内存有空块:直接分配空块,从外存调入缺页;
    • ❌内存无空块:执行页面置换算法,选出一个旧页面淘汰:
      • 如果被淘汰页面修改位 = 1 → 先写回外存;
      • 修改位 = 0 → 直接丢弃,不用写回;
  4. 将需要的页面从外存调入内存块;
  5. 修改页表:状态位置 1,填入内存块号;
  6. 恢复 CPU 现场,重新执行触发缺页的那条指令。

注意:缺页中断处理全程是操作系统内核完成,用户态感知不到。缺页时进程会阻塞,调页完成唤醒。

地址变换机构
在基本分页地址变换流程基础上增加两步:缺页判断 + 页面置换。完整地址变换步骤(有快表):

  1. 给出逻辑地址,拆分为:页号 + 页内偏移;
  2. 先查快表 TLB(高速缓存,存放近期页表项):
    • ✔快表命中:直接拿到内存块号,拼接页内偏移得到物理地址;更新访问位;结束。
    • ❌快表未命中 → 去内存查慢表(请求页表);
  3. 查询慢表:
    • 情况 A:状态位 = 1(页面在内存)
      • 更新访问位;把这条页表项加入快表;拼接物理地址;访问内存。
    • 情况 B:状态位 = 0(页面不在内存)→ 触发缺页中断,进入上面缺页中断处理流程;页面调入后,更新页表,页表项写入快表,重新执行指令。

补充细节:快表保存的一定是当前在内存中的页面;页面被置换淘汰出内存时,快表中对应的表项也要清除,否则会访问错误。

具体场景:
场景:TLB(快表)缺失 → 访问内存里的页表 → 发现有效位 = 0,缺页异常中断

  1. CPU 查 TLB,TLB 不命中 → MMU 去内存读页表(慢表)

  2. 读取页表项,发现有效位为 0:缺页,触发缺页中断(属于内中断 / 异常)

  3. CPU 保存当前指令现场,转入操作系统内核的缺页中断处理程序

  4. OS 做几件事:

    • 找一个空闲页框;没有空闲页框,执行页面替换算法,淘汰旧页面,必要时写回磁盘(脏位判断)
    • 从外存(磁盘)把需要的页面调入刚才选中的物理页框
    • 更新内存里的页表项:填入新页框号,有效位置 1,脏位清零等
    • ✅ 更新 TLB(快表):把新的「虚拟页号→物理页框号」映射写入 TLB
  5. 中断处理完成:恢复 CPU 现场,返回,重新执行那条触发缺页的指令

  6. 重新执行这条访存指令:
    再次查 TLB → TLB 命中,直接拿到页框号,完成地址转换,访问物理内存

  7. 是重新执行触发异常的那条指令,不是下一条!
    普通 IO 中断是做完之后执行下一条指令;缺页属于故障(fault),指令执行一半卡住,页面进来之后,要重新跑这一条访存指令,这是选择题超级爱挖坑的地方。

  8. TLB 什么时候更新?
    缺页处理完、修改完内存页表之后,OS(或者硬件)把新映射装入 TLB。

少数硬件架构:也可以不主动写 TLB,等到重新执行指令 TLB 缺失时,再从内存页表自动填充 TLB。
👉 408 考研默认:缺页调入页面、更新页表后,TLB 会被更新,再次执行指令 TLB 命中,按这个结论做题。

页面置换算法
请求分页:程序运行时,需要访问的页面不在内存,OS 才把页面从外存调入内存;内存装满时,要选择一个旧页面换出外存,这个选择页面的算法就是页面置换算法。
页面置换算法一共 5 种:OPT、FIFO、LRU、CLOCK(时钟 / NRU)、改进型 CLOCK。
页面换入换出会触发磁盘 I/O,开销很大。置换算法的核心目标:尽可能降低缺页率。

  1. OPT 最佳置换算法
    规则:淘汰未来最长时间不再访问 / 永远不会访问的页面。✅优点:缺页率最低,性能理论最优❌缺点:无法实现。因为程序运行前无法预知未来页面访问序列,只能作为衡量其他算法性能的理论标杆。

做题技巧:遇到要淘汰页面时,看内存里现存的几个页号,往后看访问序列,最后才出现的那个页,就是 OPT 要淘汰的。

  1. FIFO 先进先出置换算法

规则:淘汰最早进入内存的页面,页面按调入顺序排成队列,换页直接踢队头。✅优点:实现最简单,只需要记录页面调入顺序❌缺点:

  1. 性能差,页面调入早晚和页面是否会被频繁访问没有关系;
  2. 唯一会产生 Belady 异常:分配给进程的物理块(内存块)越多,缺页次数反而变多。
    Belady 异常——当为进程分配的物理块数增大时,缺页次数不减反增的异常现象。

Belady 异常考点:只有 FIFO 会出现,LRU、OPT、时钟算法都不会!

  1. LRU 最近最久未使用置换算法

规则:淘汰最近一段时间最久没有被访问过的页面。✅优点:性能很好,十分接近 OPT;符合程序局部性原理❌缺点:实现困难,硬件开销大,需要专门硬件(寄存器 / 栈)记录每个页面上次访问到现在的时间。

做题技巧:需要淘汰页面时,反向看访问序列,内存中现存页号,逆向最晚出现的就是要淘汰的。

  1. CLOCK 时钟置换算法(NRU,最近未使用算法)

背景:平衡 FIFO 简单、LRU 性能好的折中方案,开销不大,性能尚可。

目的就是要先置换没有访问的页面 就是需要首先将访问位为0的页面换出去

内存页面组织成循环队列,每个页面增加一位访问位:访问过置 1,没访问置 0。

置换时,指针循环扫描:

  1. 访问位 = 0,直接淘汰;
  2. 访问位 = 1,把访问位改成 0,指针继续往下走;一轮扫完所有页访问位都是 1,就第二轮重新扫描,一定会找到访问位 0。👉最多两轮扫描找到淘汰页。

缺点:只考虑页面有没有被访问,不考虑页面是否被修改。如果被淘汰页修改过,换出时要写回磁盘,I/O 开销大。

  1. 改进型 CLOCK(改进时钟算法)

简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上,如果被淘汰的页面没有被修改过,就不需要执行I/O操作写回外存。只有被淘汰的页面被修改过时,才需要写回外存

现在考虑两个因素 访问位和修改位

访问位 访问过1 未访问0
修改位 修改过1 未修改0

最终目标淘汰 即未访问 又未修改 也就是 0 0

第一轮遇到 0 0 就淘汰 不修改任何
第二轮淘汰 0 1 也就是淘汰未访问 但是已经修改 当前如果访问位是1 就改成0 为下一轮做准备
第三轮淘汰 0 0 也就是 已经访问 但是未修改 不改任何标志
第四轮淘汰 0 1 也就是又修改 又访问
因此改进型CLOCK置换算法选择一个淘汰页面最多会进行四轮扫描

性能排序(理论):OPT > LRU > 改进CLOCK > CLOCK > FIFO

  1. Belady 异常:选择题高频坑点,仅 FIFO 存在。
  2. LRU 和 OPT 做题区分:OPT 看未来,LRU 看过去。
  3. 时钟算法:简单时钟最多两轮;改进时钟最多四轮扫描

页面分配策略

一些关键的概念
驻留集:OS 分配给这个进程的物理块集合(当前在内存里属于该进程的页面)。

页面分配置换策略

固定分配:操作系统为每个进程分配一组固定数目的物理块,在进程运行期间不再改变。即,驻留集大小不变

可变分配:先为每个进程分配一定数目的物理块,在进程运行期间,可根据情况做适当的增加或减少。即,驻留集大小可变

局部置换 发生缺页时只能选进程自己的物理块进行置换。

全局置换 可以将操作系统保留的空闲物理块分配给缺页进程,也可以将别的进程持有的物理块置换
到外存,再分配给缺页进程。

两个维度组合:分配(固定 / 可变) + 置换(局部 / 全局),一共 3 种合法策略:

固定分配 + 全局置换:不可能。固定分配意味着进程物理块数量不能变,全局置换会抢走别的进程块,矛盾。

表格

策略 核心规则 考点理解
✅固定分配 + 局部置换 进程运行前分配固定数量物理块,全程不变;缺页时只能淘汰自己驻留集里的页面 难点:一开始很难预估该分配多少块。分配少了抖动,分配多了浪费。
✅可变分配 + 全局置换 初始分配若干块;缺页优先拿系统空闲块;空闲块用光,可以淘汰任意进程(包括别的进程)的页面。👉缺页进程的驻留集会变大;被抢页面的进程驻留集变小,缺页率上升。 全局:可以跨进程抢内存,系统全局视角。
✅可变分配 + 局部置换 初始分配若干块;缺页只能淘汰自己的页面。OS 监测缺页频率:缺页频繁 → 给它增加物理块;缺页很少 → 回收减少物理块。 局部:只能动自己的页面;驻留集大小动态根据进程自身缺页情况调整。

可变分配全局置换和可变分配局部置换区别
可变分配全局置换:刚开始会为每个进程分配一定数量的物理块。操作系统会保持一个空闲物理块队
列。当某进程发生缺页时,从空闲物理块中取出一块分配给该进程;若已无空闲物理块,则可选择一
个未锁定的页面换出外存,再将该物理块分配给缺页的进程。采用这种策略时,只要某进程发生缺页,
都将获得新的物理块,仅当空闲物理块用完时,系统才选择一个未锁定的页面调出。被选择调出的页
可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的物理块会减少,缺页率会增加。

可变分配局部置换:刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页时,只允许从该
进程自己的物理块中选出一个进行换出外存。如果进程在运行中频繁地缺页,系统会为该进程多分配
几个物理块,直至该进程缺页率趋势适当程度;反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块。
重点区分两件完全不一样的事:
可变分配 + 局部置换:缺页时只允许从「当前进程自己已经分到的物理块」里淘汰页面;但如果缺页率太高,操作系统可以额外给这个进程增加物理块数量,新增的块是从系统空闲块池拿,不是抢别的进程已经占有的块。

  1. 页面置换(缺页中断时):局部置换 → 只在本进程的块内淘汰,不会抢别的进程内存块
  2. 动态增加物理块(缺页率过高时):操作系统分配系统空闲物理块给这个进程;只有当系统空闲块耗尽的时候,才会出现全局层面资源紧张,才会和其他进程产生资源竞争。

可变分配全局置换:只要缺页就给分配新物理块
可变分配局部置换:要根据发生缺页的频率来动态地增加或减少进程的物理块

何时调入页面
调入页面的时机(什么时候把磁盘页面拉进内存)

  1. 预调页策略(运行前调入)
    基于空间局部性:访问一块内存,大概率会访问相邻内存。一次性预调入一批相邻页面。
    • 缺点:预判不准,调入很多永远不会访问的页面,浪费 IO;
    • 适用:进程刚开始启动的时候。
  2. 请求调页策略(运行时调入,最常用)
    进程执行访问页面,发现缺页中断的时候,才调入这一页。
    • 优点:调入的页面一定会被访问;
    • 缺点:每次缺页都触发磁盘 IO,开销大;一次只调入一页。

考点区分:预调页是提前预判;请求调页是 “缺了才调”。

从何处调入页面(磁盘分两块:对换区、文件区)

  1. :进程运行前,把进程数据从文件区复制到对换区;页面调入调出都在内存 ↔ 对换区之间,速度快

  2. 后面两种好像都是一开始就放在文件去 然后一开始从文件区调 然后有修改就放回对换区 然后再从对换区读

抖动(颠簸)现象
抖动定义:刚换出去的页面马上又要调入,刚调入的页面立刻又要换出,系统疯狂页面置换,CPU 几乎都耗在磁盘 IO 上,进程几乎无法有效工作
根本原因:分配给进程的物理块太少,小于进程当前工作集,进程频繁访问的页面数量 > 可用物理块数。

工作集
驻留集:OS 分配给进程的物理块集合(客观分配了多少内存)
工作集:一段时间窗口内,进程实际访问过的页面集合(进程真实需要多少页面)

公式关系:驻留集大小 ≥ 工作集大小,否则必然抖动。
窗口尺寸:统计工作集的时间窗口;窗口越大,统计出来的工作集页面越多。
工作集大小 ≤ 窗口尺寸(会有重复访问页面)。

工作集思路:OS 持续监测进程工作集,给进程分配的物理块数量至少不小于它的工作集,避免抖动。
拓展:可以基于工作集做置换算法,优先淘汰不在当前工作集中的页面。

说明:工作集大小可能小于窗口尺寸,实际应用中,操作系统可以统计进程的工作集大小,根据工作集大小
给进程分配若干内存块。如:窗口尺寸为5,经过一段时间的监测发现某进程的工作集最大为3,那么
说明该进程有很好的局部性,可以给这个进程分配3个以上的内存块即可满足进程的运行需要。一般来说,驻留集大小不能小于工作集大小,否则进程运行过程中将频繁缺页

内存映射文件

理解从

传统文件方式方式和内存映射的区别

传统的文件访问方式
传统读操作:第一步磁盘读入内核缓冲区,第二步内核缓冲区拷贝一份到用户内存空间。
用户程序不能直接访问磁盘文件,必须借助系统调用、读写指针操作这块独立的缓冲区。
**读写指针(文件偏移量)是内核在文件打开时,在「文件表项」里维护的一个变量,存在内核内存,不在磁盘!lseek 只是修改这个内存里的偏移数值,**磁盘 IO 完全不发生 **。

其实读写指针就是一个变量 初始为零 然后需要读文件的时候 给出offset 计算出便宜 直接去磁盘的对应位置读取到内核缓冲区

和 mmap 对比
mmap 没有这个 “读写指针”!mmap 是页表映射,访问哪个虚拟地址,就对应文件哪个偏移,靠缺页中断调入页面,没有 lseek 这个操作

内存映射文件 mmap

流程:open打开文件 → mmap:把磁盘上这个文件,映射到进程虚拟地址空间一段连续的虚拟地址。
✅ 缺页中断自动完成:访问虚拟地址,如果页面不在内存,OS 自动从磁盘文件读入内存;页面被修改变成脏页,OS 在合适时机(进程关闭文件、munmap)自动把脏页写回磁盘文件区。

记得ppt上的流程

open 系统调用——打开文件
mmap 系统调用——将文件映射到进
程的虚拟地址空间
• 以访问内存的方式访问文件数据
• 文件数据的读入、写出由操作系统
自动完成
• 进程关闭文件时,操作系统自动将文件被修改的数据写回磁盘

多个进程可以映射同一个文件,实现共享
在物理内存中,一个文件对应同一份数据,当一个进程
修改文件数据时,另一个进程可以立马“看到”

连续内存分配:4 种动态分区分配算法(文字整理)
≡