操作系统学习19:缺页异常 #PF 与按需分页

操作系统学习19:缺页异常 #PF 与按需分页

    • 一、为什么第 19 课学 #PF?
    • 1. #PF是什么
    • [2. 当前能力链](#2. 当前能力链)
    • [二、#PF 时 CPU 做了什么?](#PF 时 CPU 做了什么?)
      • [2.1 CPU 触发 #PF 的条件:](#PF 的条件:)
        • [1. 页不存在(P=0)](#1. 页不存在(P=0))
        • [2. 权限违规(P=1)](#2. 权限违规(P=1))
        • [3. 保留位错误](#3. 保留位错误)
      • [2.2 错误码各位](#2.2 错误码各位)
      • [2.3 恢复执行的关键点](#2.3 恢复执行的关键点)
      • [2.4 #PF 的硬件流程](#PF 的硬件流程)
      • [2.5 OS 在 #PF 中能做什么?](#PF 中能做什么?)
    • 三、本课实现结构
    • 四、源码要点
      • [4.1 注册 `register_interrupt_handler`](#4.1 注册 register_interrupt_handler)
      • [4.2 汇编桩与错误码](#4.2 汇编桩与错误码)
      • [4.3 `page_fault_handler`](#4.3 page_fault_handler)
      • [4.4 按需映射](#4.4 按需映射)
      • [4.5 VGA 卷屏](#4.5 VGA 卷屏)
    • 五、构建与运行
    • 六、建议实验
    • 七、小结

承接 lesson17 VMM / lesson18 kmalloc

本课目标:挂上 向量 14(#PF) ,在访问未映射页时不再三重故障重启;并演示最简单的 demand paging(按需映射)

lesson17 开分页后,访问页表里不存在的地址会直接崩溃。真正的 OS 靠缺页异常决定:是报错杀掉进程,还是临时分配一页让指令重试------这正是「虚拟内存」里「虚」字落地的地方。


一、为什么第 19 课学 #PF?

1. #PF是什么

#PF 是 CPU 在访问某个线性地时发现该地址对应的页表项无效或权限不符时触发的异常。

它是虚拟内存系统的核心机制:

  • 不是所有内存都必须提前分配
  • 访问时再分配(按需分页)才是现代 OS 的常态

虚拟内存的目标是让进程看到一个"巨大的、连续的、干净的"地址空间,但物理内存有限且碎片化。如果没有 #PF:

  • 所有虚拟地址必须提前映射 → 浪费内存
  • 程序访问未映射地址 → 直接三重故障重启

无法实现:

  • 按需分页
  • 写时复制
  • 匿名映射
  • 用户态栈自动增长
  • 内存映射文件

所以 #PF 是虚拟内存的"调度中心":

CPU 发现问题 → 抛异常 → OS 决定如何处理。

2. 当前能力链

text 复制代码
PMM → VMM → kmalloc → ★ #PF/按需分页 → 用户态 / 每进程页表
 16     17      18           19                20+
备选主题 为何本课优先 #PF
用户态 + 系统调用 跨度大(TSS、Ring3、门),建议缺页先稳
每进程页表 依赖异常路径与更完整的地址空间策略
#PF 直接补齐 VMM 承诺,一课能跑通「故障→处理→恢复」

二、#PF 时 CPU 做了什么?

2.1 CPU 触发 #PF 的条件:

1. 页不存在(P=0)
  • 页表项的 Present 位为 0
  • 最常见:按需分页、未分配、未加载
2. 权限违规(P=1)
  • 写入只读页(W/R=1)
  • 用户态访问内核页(U/S=1)
  • 执行 NX(No-Execute)页(在 x86-64)
3. 保留位错误
  • 页表项中某些保留位被错误设置(严重错误)
  • 访问线性地址时,MMU 查页表失败或权限不够 → 触发 异常向量 14
  1. 把故障线性地址写入 CR2
  2. 压栈:错误码、EIP、CS、EFLAGS(同特权级)
  3. 跳到 IDT14 指向的处理程序

2.2 错误码各位

名称 意义
0 P 0=页不存在;1=页存在但权限/保留位错误
1 W/R 0=读;1=写
2 U/S 0=内核态;1=用户态
3 RSVD 页表保留位被错误使用
4 I/D 0=数据访问;1=指令取指(仅在某些 CPU)

本课按需映射只处理 P=0(not-present) 的情况。

2.3 恢复执行的关键点

处理程序若补好了映射,必须:

  1. 弹出错误码(ADD ESP, 4
  2. IRET 回到故障 EIP

CPU 会 重试 那条引发缺页的指令------所以 *p = MAGIC 看起来像「写一次」,中间其实经历了一次完整异常往返。

2.4 #PF 的硬件流程

  1. MMU 查页表失败或权限不符
  2. CPU 将故障线性地址写入 CR2
  3. CPU 生成错误码(Error Code)
  4. CPU 按异常规则压栈:
  • EIP
  • CS
  • EFLAGS
  • 错误码
  1. 跳转到 IDT14 指定的处理程序
  2. OS 处理后执行 IRET
  3. CPU 重试导致异常的那条指令

这就是为什么按需分页能"无缝恢复"。

2.5 OS 在 #PF 中能做什么?

缺页异常是 OS 的"万能入口",可以做:

  • 按需分配匿名页
  • 加载文件页(mmap)
  • 栈自动增长
  • 写时复制(COW)
  • 交换页(swap in/out)
  • 检测非法访问 → 杀掉进程(segfault)

本文即实现匿名页按需分配(demand-zero page)。


三、本课实现结构

文件 职责
page_fault.s isr_page_fault:保存寄存器、读 CR2、调 C、IRET
page_fault.c 打印诊断;可选 pmm_alloc + vmm_map
utilities.s 导出 init_idtread_cr2
main.c 0x40000000 未映射写入,验证按需恢复
text 复制代码
*p = MAGIC  (p 未映射)
    → #PF
    → isr_page_fault
    → page_fault_handler
         ├─ 打印 CR2 / 错误码
         ├─ pmm_alloc_page
         ├─ vmm_map_page(virt, phys)
         └─ return
    → IRET 重试 MOV
    → 写入成功

四、源码要点

4.1 注册 register_interrupt_handler

pf_init 里先 init_idt(),再把向量 14 指到汇编入口:

c 复制代码
init_idt();
register_interrupt_handler(PF_VECTOR, (uint32_t)isr_page_fault);

实现位于 utilities.s:按 cdecl 取 vector / handler,向内存中的 idt[vector] 写入一条 32 位中断门(每项 8 字节)。

asm 复制代码
; void register_interrupt_handler(uint32_t vector, uint32_t handler);
; cdecl: [ESP+4]=vector, [ESP+8]=handler
register_interrupt_handler:
    PUSH  EAX
    PUSH  EBX

    MOV   EBX, [ESP + 12]        ; vector(PUSH 了 2 个寄存器后偏移 +8)
    MOV   EAX, [ESP + 16]        ; handler
    LEA   EBX, [idt + EBX * 8]   ; 指向 idt[vector]

    MOV   WORD [EBX + 0], AX     ; Offset 15..0
    MOV   WORD [EBX + 2], SELECTOR_CODE
    MOV   BYTE [EBX + 4], 0
    MOV   BYTE [EBX + 5], 0x8E   ; P=1, DPL=0, 32-bit interrupt gate
    SHR   EAX, 16
    MOV   WORD [EBX + 6], AX     ; Offset 31..16

    POP   EBX
    POP   EAX
    RET

调用瞬间栈上是 [ESP]=返回地址, [ESP+4]=vector, [ESP+8]=handler;函数开头又 PUSH EAX/EBX,所以取参要用 [ESP+12] / [ESP+16]

LEA EBX, [idt + EBX*8]:每项 8 字节,算出该向量在 IDT 表中的地址。随后按 Intel 中断门布局填表:

偏移 写入 含义
+0 handler 低 16 位 Offset 15...0
+2 SELECTOR_CODE 内核代码段选择子
+4 0 保留
+5 0x8E P=1,DPL=0,类型=中断门
+6 handler 高 16 位 Offset 31...16

只改表项不够:CPU 认的是 IDTR。init_idt 内部会 LIDT [idt_ptr],所以必须先加载 IDT,再改写向量 14;本课用单独的 init_idt(),不走 init_keyboard(避免顺带 STI 开中断)。

4.2 汇编桩与错误码

asm 复制代码
isr_page_fault:
    PUSHAD
    MOV  EAX, [ESP+32]   ; error_code
    MOV  ECX, CR2
    PUSH ECX             ; 参数 cr2
    PUSH EAX             ; 参数 error_code
    CALL page_fault_handler
    ADD  ESP, 8
    POPAD
    ADD  ESP, 4          ; 丢掉 error_code(IRET 不要它)
    IRET

栈布局(PUSHAD 之后):[ESP+0..31] 保存的通用寄存器,[ESP+32] 为 CPU 压入的错误码,再往上是 EIP/CS/EFLAGS。

cdecl 从右往左压参,故先压 cr2、再压 error_code,对应 C 侧 page_fault_handler(error_code, cr2)

因为 CPU 在进入异常时压了一个错误码,这里不能漏掉 ADD ESP, 4,否则会导致 IRET 把错误码当成 EIP,立刻再次异常。

4.3 page_fault_handler

isr_page_fault 以 cdecl 调用,签名为:

c 复制代码
void page_fault_handler(uint32_t error_code, uint32_t cr2);
参数 来源 作用
error_code CPU 压栈,桩里从 [ESP+32] 取出 区分 not-present / 权限错误,以及读/写、内核/用户
cr2 桩里 MOV ECX, CR2 故障线性地址(按需映射时对齐到页)

处理流程:

  1. 更新统计:fault_count++,记下 last_cr2 / last_error
  2. 打印 #PF #N、CR2、以及解码后的错误码(not-present|protread|writekernel|user
  3. 可恢复路径demand_enabledP=0(页不存在)→ 见下节按需映射,return 后由 IRET 重试故障指令
  4. 不可恢复路径 :权限错误、未开 demand、或 alloc/map 失败 → 打印后 io_hlt 死循环,避免再次缺页导致三重故障重启

成功返回与停机的分界很明确:只有「开了按需 + not-present + 映射成功」才会回到故障 EIP;其余一律停机保平安。

4.4 按需映射

c 复制代码
if (pf.demand_enabled && (error_code & PF_ERR_PRESENT) == 0) {
    phys = pmm_alloc_page();
    vmm_map_page(cr2 & PAGE_MASK, (uint32_t)phys, PRESENT|WRITE);
    page_zero(phys);
    return;   /* 重试 */
}
/* 否则打印后 io_hlt,避免三重故障 */

cr2 & PAGE_MASK 把故障地址对齐到页起点再映射;page_zero 清掉脏页,避免读到旧物理页残留数据。

pf_set_demand(0) 时,未映射访问会停在「PF: unhandled, halt」,而不会整机重启------比 lesson17 时友好得多。

4.5 VGA 卷屏

本课日志较长(PMM 分区、VMM、HEAP、#PF 演示),超过 VGA 文本模式的 25 行

旧版 print_charY >= 25 时把光标卡在最后一行继续写 ,后几行会互相覆盖,尾部看起来像乱码(例如 _one. PMM free pages...)。

本课在 utilities.s 增加了 scroll_up:换行越界时把显存第 1~24 行上移一行并清空最后一行,光标停在第 25 行。

因此演示末尾的 after write / faults= / Done 可以完整看到;更早的 PMM 明细会滚出屏幕顶部,属正常现象。


五、构建与运行

bat 复制代码
cd lesson19
make
make install
make run

预期输出(要点)

text 复制代码
Lesson19: Page Fault (#PF) + Demand Paging
=== VMM Init ===
VMM: paging ON ...
=== PF Init ===
PF: IDT[14] ready  demand=OFF
before: 0x40000000 -> 0x00000000  (unmapped)
=== Demand Fault Demo ===
PF: demand ON
writing via unmapped virt...
#PF #1  CR2=0x40000000
  err=0x00000002 [not-present write kernel]
PF: demand-map 0x40000000 -> 0x........  OK (retry)
after write: value=0x4C313950  OK
after:  0x40000000 -> 0x........
faults=1  demand_mapped=1

错误码 0x2 = 写 + 页不存在(bit1=W,bit0=P 清零),与演示一致。


实际运行截图:

六、建议实验

  1. pf_set_demand(0) 再访问未映射页,确认停机而非重启
  2. 对已 map 的只读页做写(需改 PTE 去掉 WRITE)观察 prot 类错误
  3. 在处理函数里打印 fault_count,连续碰多个未映射页看按需次数
  4. 对照 Intel SDM Vol.3 的 Interrupt 14---Page-Fault Exception

七、小结

  • 挂上 #PF,缺页不再等于整机复位
  • 理解 CR2 + 错误码 + IRET 重试
  • 实现最小 demand paging:缺页时现分配、现映射

本文开源地址:

https://gitee.com/xundh/learn-os

相关推荐
kdxiaojie1 小时前
Linux 驱动研究 —— V4L2 (14)
linux·运维·笔记·学习
承渊政道2 小时前
Linux系统学习【掌握Ext系列⽂件系统的相关内容】
linux·运维·学习·文件系统·存储结构·inode
未知违规用户2 小时前
大模型项目: 学习FastAPI 服务器开发
服务器·人工智能·python·学习·fastapi
幸福在路上wellbeing2 小时前
AI 智能体开发 · Day 3 详细学习手册
人工智能·学习·oracle
MartinYeung513 小时前
[论文学习]自主性如何重塑个性化对LLM智能体隐私关切与信任的影响
人工智能·学习
FellAveal14 小时前
【Go语言入门学习笔记】Part18.工作池与WaitGroup(也即协程池)
笔记·学习·golang
菜鸟‍15 小时前
【论文学习】MICCAI 2024 || SGSeg:通过自引导机制实现胸部X光片语言引导分割的无文本推理
人工智能·深度学习·学习
lazy H15 小时前
Git clone 怎么用?克隆项目及常见问题完整教程
大数据·git·后端·学习·搜索引擎·github
菜鸟‍15 小时前
【论文学习】arxiv 2024 || RoSIS:基于鲁棒框架重新审视文本提示式手术器械分割
人工智能·学习·计算机视觉