请求分页管理方式

请求分页 = 基本分页 + 请求调页 + 页面置换。

它是虚拟存储最常用的实现方式。这一节讲清三件事:页表要多存什么、缺页了怎么办、页从哪儿调进来。

机制

请求页表:多出四个字段

基本分页的页表项只需存页框号。要支持”页可能不在内存”,必须补充信息:

页号页框号存在位 P访问位 A修改位 M外存地址
  • 存在位 P:该页是否已调入内存。这是缺页判断的唯一依据。
  • 访问位 A:该页近期是否被访问过,供置换算法参考。
  • 修改位 M(脏位):该页调入内存后是否被修改过。未修改的页被淘汰时不必写回外存,直接丢弃即可——这能省掉一次磁盘写。
  • 外存地址:该页在外存中的存放位置,缺页时按它去取。

四个字段各服务于一个目的:P 管”在不在”,A 和 M 管”该淘汰谁”,外存地址管”去哪儿取”。

缺页中断处理流程

访问某页时若发现 P = 0,硬件产生缺页中断:

flowchart TB
    A["访问逻辑地址,查页表"]:::n
    B{"存在位 P = 1?"}:::c
    C["直接访问,结束"]:::ok
    D["<b>产生缺页中断</b><br/>保护现场,转中断处理程序"]:::warn
    E{"内存有空闲页框?"}:::c
    F["<b>置换</b>:按算法选一页淘汰"]:::warn
    G{"被淘汰页 M = 1?"}:::c
    H["写回外存"]:::io
    I["从外存调入所需页"]:::io
    J["修改页表:P←1,填页框号<br/>并在快表中增加表项"]:::n
    K["恢复现场,<b>重新执行被中断的指令</b>"]:::ok

    A --> B
    B -->|"是"| C
    B -->|"否"| D --> E
    E -->|"有"| I
    E -->|"无"| F --> G
    G -->|"是"| H --> I
    G -->|"否"| I
    I --> J --> K

    classDef n fill:#f1f5f9,stroke:#94a3b8,color:#334155
    classDef c fill:#e0e7ff,stroke:#6366f1,color:#312e81
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef io fill:#fee2e2,stroke:#dc2626,color:#7f1d1d

流程里有四个必考的点:

① 缺页中断属于”内中断”中的”故障(fault)”。

它是在指令执行期间产生的,由当前指令本身引起,因此属于内中断(异常)而非外中断。而且它是故障类型——故障的特点是可以被修复,修复后重新执行原指令。这与”陷阱(trap,如系统调用,执行下一条指令)“和”终止(abort,不可恢复)“不同。

② 处理完毕后要”重新执行”被中断的那条指令,而不是执行下一条。

因为那条指令根本没执行成功——它要的数据当时不在内存。页调进来之后必须让它从头再来一遍。这是”故障”类异常的共同特征。

③ 一条指令可能产生多次缺页中断。

例如一条指令跨越两个页面存放,其操作数又分别在另外两个页面上,最多可能引发 4 次缺页。这说明缺页中断与”一条指令一次中断”没有必然联系。

④ 只有被淘汰页的 M = 1 时才需要写回。

这正是修改位存在的价值——把”必须写回”变成了”可能不必写回”,缺页处理的平均代价因此显著下降。

调页策略:何时调入

请求调页:只在缺页时才调入所需页面。

  • 优点:只调真正用到的页,不浪费。
  • 缺点:每次只调一页,缺页中断频繁,I/O 开销大。

预调页:预计将来会用到,提前一并调入。

  • 依据是空间局部性——既然访问了某页,附近的页多半也快用到。
  • 若预测准确,可大幅减少缺页次数;预测失败则是纯浪费。

疑问点:预调页是否真的由程序员指定页面

教材原文:“预调页策略主要用于进程的首次调入,由程序员指出先调入哪些页。” 程序员真的可以指定吗?

教材这句话描述的是理论模型,实际系统中程序员通常不逐页指定,但确实可以向内核表达调入意图。

分三层说清楚:

理论层(考纲口径):预调页主要用于进程首次调入——此时进程还没运行过,系统没有任何访问历史可供参考,无从预测,所以只能依靠程序员(或编译器/链接器)事先给出提示。这个口径要记住,考试按它答。

实践层:现代系统提供的是”建议”而非”指定”。程序员能做的是告诉内核自己的访问意图,由内核决定要不要照办:

  • madvise(addr, len, MADV_WILLNEED) —— 告诉内核”这段我马上要用”,内核可提前预读
  • posix_fadvise() —— 对文件访问模式给出提示(顺序/随机/即将使用)
  • mmap() 的 MAP_POPULATE 标志 —— 映射时就把页表填好、把页面预先调入

注意这些都是按”地址区间”给建议,不是按页号逐页点名。

现实层:真正起主要作用的是内核的自动预取。内核会检测访问模式,一旦识别出顺序访问就自动向前多读若干页。这比程序员手工指定有效得多,因为它基于运行时的真实行为。

一句话:“程序员能表达意图”是真的;“逐页指定”在现代系统中不是常规做法。

从何处调入页面

疑问点:三种调入来源的记忆方法与工程现状

教材把外存分为文件区与对换区,并给出三种调入情形。 这部分应当如何记忆?实际工程中是否也这样实现?

先记住那个前提,三种情形就都能推出来:

对换区采用连续分配、文件区采用离散分配,因此对换区的 I/O 速度更快。

于是原则很清楚:能放对换区就放对换区(因为快);放不下的时候才做取舍。 三种情形就是”对换区够不够”的三档:

① 对换区空间充足 → 全部从对换区调入。为此在进程运行前,先把相关文件从文件区复制到对换区。用一次拷贝换取此后每次调页都走快速通道。

② 对换区空间不足 → 按”会不会被修改”分流:

  • 不会被修改的(代码段、只读数据):直接从文件区调入。换出时根本不必写回——文件区里那份原本就是最新的,直接丢弃即可。
  • 可能被修改的:换出时必须写到对换区,以后从对换区调入。

这一档的记忆抓手是:只读的东西可以留在文件区(反正不用写回),会改的东西必须进对换区(因为要写回,而对换区快)。

③ UNIX 方式 → 按”有没有运行过”分流:

  • 未运行过的页:从文件区调入(还没被改过,文件区那份就是原样)
  • 运行过又被换出的页:从对换区调入(换出时已写到了对换区)
  • 共享页若已被其他进程调入内存:不必再调,直接映射即可

工程现状:第 ③ 种就是现代 Linux 的做法,而且它把这个思想推到了更彻底的形态——按页的来源分成两类:

  • 文件映射页(file-backed):内容来自某个文件(可执行代码、mmap 的文件、页缓存)。回收时若是干净页直接丢弃,脏页写回原文件,完全不占用 swap。
  • 匿名页(anonymous):没有对应文件(堆、栈、匿名 mmap)。它们无处可写,只能换出到 swap。

对照一下就会发现,这正是教材第 ② 档”按会不会被修改分流”的工程版本:有文件作后盾的页不需要 swap,没有后盾的才需要。

关联对照:Linux 的 swap 与 macOS 的内存管理

fallocate 与 swap 不是一回事。fallocate 只是预分配一个指定大小的文件(占住磁盘块),它本身没有任何”交换”语义。要让这个文件成为交换空间,还需要 mkswap 格式化、swapon 启用。fallocate 造的是容器,swap 是用途。

“服务器上设置了 swap 却从来没用过”是正常的,而且是好事。 swap 只在内存压力大到需要换出匿名页时才会被使用。使用率长期为 0 说明内存充裕,没有触发换出。Linux 的 swappiness 参数正是用来调节”多大压力下开始换出匿名页”的倾向。

macOS 很少用 swap,是因为它在 swap 之前多了一层:内存压缩。不活跃的页先被就地压缩留在内存中,只有压缩也扛不住时才真正写到磁盘。这比磁盘 I/O 快得多。

“已缓存文件特别高”则完全是另一回事——那是页缓存(file-backed pages),属于内存映射文件的范畴。这部分内存随时可回收(干净页直接丢弃、脏页写回),因此”已用内存高”并不代表内存紧张。这也是为什么 Linux 有句名言:空闲的内存是被浪费的内存——与其空着,不如拿来缓存文件。

边界

缺页中断 vs 一般中断

缺页中断一般(外)中断
来源当前指令执行期间,内部产生外部设备发出
类别内中断 · 故障(fault)外中断
检测时机指令执行过程中一条指令执行完之后
处理后重新执行被中断的指令执行下一条指令
一条指令可产生几次可能多次通常一次

“处理后重新执行原指令”是缺页中断最独特的一点,因为那条指令压根没成功。

请求分页 vs 基本分页

判据:页表项是否含存在位、是否有缺页中断机构。

基本分页要求所有页都必须在内存,页表项里没有”不在内存”这种状态;请求分页允许部分页在外存,因此必须有存在位、缺页中断和置换算法。

修改位的价值

修改位不影响”淘汰谁”的正确性,只影响”淘汰的代价”。

即使完全不看修改位,淘汰后无脑写回也不会出错,只是每次都多一次磁盘写。修改位的作用是把这次写在多数情况下省掉——因为大部分被访问的页(尤其是代码页)根本没被改过。

这也解释了改进型 Clock 算法为什么要同时看 A 位和 M 位。

对照速查

请求页表新增字段作用
存在位 P该页是否在内存——缺页判断的唯一依据
访问位 A近期是否被访问——供置换算法参考
修改位 M是否被修改——决定淘汰时要不要写回
外存地址缺页时去哪儿取
调页策略何时调入代价
请求调页缺页时才调缺页中断频繁
预调页提前批量调入预测失败则浪费;主要用于首次调入
调入来源判据
① 对换区充足全部从对换区(运行前先从文件区拷过去)
② 对换区不足只读的走文件区(不必写回),可改的进对换区
③ UNIX 方式未运行过走文件区,换出过走对换区;共享页已在内存则不调

考点

  • 请求页表的四个新增字段及各自用途
  • 缺页中断是内中断中的”故障”,处理后重新执行原指令
  • 一条指令可能引发多次缺页中断
  • 只有 M = 1 才需写回外存
  • 预调页主要用于进程首次调入
  • 三种调入来源,判据分别是”对换区够不够”和”会不会被修改 / 有没有运行过”

链接