news 2026/8/12 16:42:51

MIT 6.S081 xv6 lazy page allocation 实验篇(lab5):Lazy allocation (moderate)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIT 6.S081 xv6 lazy page allocation 实验篇(lab5):Lazy allocation (moderate)

Lazy allocation (moderate)


实验目标

这是 Lab5Lazy page allocation的第二步,也是真正让"惰性分配"落地的核心关。

上一步(Eliminate allocation from sbrk)我们删掉了sbrk里的实际分配,只记账、不建映射,结果访问新地址就panic。本步的目标:在缺页发生时,由内核当场补上这块内存

练习目标:

  1. usertrap()里识别用户态缺页异常scause == 1315),为其现场分配一页物理内存并建好映射,让进程从缺页指令处无缝继续。
  2. 修改uvmunmap(),让它在解除映射时遇到"未映射的页"不再 panic,而是直接跳过(否则上一步埋下的"账记了、页没给"在进程退出时仍会炸)。
  3. 最终让echo hi这类简单命令跑通,理解 lazy allocation 的完整闭环:申请时只记账 → 访问时缺页 → 内核补分配 → 继续运行

官方 lab 原文把这一步描述为:

Modify the code intrap.cto respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing. You should add your code just before theprintfcall that produced the “usertrap(): …” message.


前置知识

1. 缺页异常落在usertrap

承接上一步:进程访问sbrk新记大的、却没映射的地址 → MMU 发现 PTE 无效 → CPU 触发页错误异常,经uservec陷入内核、进入usertrap()。所以在usertrap里插桩,正是处理它的正确位置。

scause编码(xv6kernel/riscv.h):

scause含义
8(0x8)系统调用(ecall
13(0xd)访问缺页(load page fault)
15(0xf)访问缺页(store/AMO page fault)

r_stval()返回触发缺页的虚拟地址r_sepc()返回缺页指令地址(处理完从这里重执行)。

2. lazy handler 的"三板斧"

为缺页地址补一页,本质是照抄uvmalloc()的核心逻辑(官方 hint 就是"Steal code from uvmalloc()"):

  1. kalloc():从空闲链表抠出一页物理内存
  2. memset(pa, 0, PGSIZE)必须清零。xv6 的物理页可能此前装过内核/其他进程数据,不清零会把残留内容(甚至内核机密)暴露给用户进程——这是安全与正确性的硬要求。
  3. mappages():在页表里把"缺页虚拟地址(页对齐)↔ 这块物理页"建好映射,并赋予正确权限。

3. PTE 权限位

kernel/riscv.h里定义的页表项标志位:

含义
PTE_VValid,映射有效
PTE_R可读
PTE_W可写
PTE_X可执行
PTE_U用户态可访问(U-mode 能访问)

用户进程的堆页必须带PTE_U才能被用户态访问;R|W是读写所需。

4. 为什么必须改uvmunmap

上一步我们讲透过:panic发生在进程退出、内核uvmunmap(pagetable, 0, sz, 1)解映射时——它走到sbrk记大却从未建映射的那段,发现 PTE 无效就panic("uvmunmap: not mapped")

现在惰性分配下,"[0, sz)里有空洞(未分配页)"是常态而非错误。所以要让uvmunmap遇到无效 PTE 时跳过而非 panic——这是本步必须的配套改动。

5. 本步改动文件

文件改动
kernel/trap.cusertrap()新增缺页分支(lazy handler)
kernel/vm.cuvmunmap()遇无效 PTE 改continue跳过

实现思路

整体闭环:

进程访问未映射地址 ──(MMU 缺页)──> usertrap(scause=13/15) │ ├─ 取 fault_va = r_stval() ├─ 校验合法性:fault_va 在"栈顶之上、sz 之下"(属于合法堆区) ├─ kalloc() 拿一页物理内存 ├─ memset(pa, 0, PGSIZE) 清零 ├─ mappages() 建映射(R|W|X|U) └─ usertrapret() 返回用户态,从缺页指令处重新执行 ──> 这次访问成功

关键设计点:

  1. 插入位置:在usertrap原有if(scause==8)(系统调用)、else if(devintr)(设备中断)之后,else(unexpected)之前,新增else if(r_scause()==13 || r_scause()==15)分支。这样缺页会被优先识别、不再落进"unexpected"兜底。
  2. 合法性判断PGROUNDUP(p->trapframe->sp) - 1 < fault_va && fault_va < p->sz。含义:缺页地址必须高于用户栈顶sp之上才是堆区)、且低于进程大小sz(在sbrk记账的范围内)。这一步把"合法惰性分配请求"和"真正非法的访问"区分开。
  3. 分配 + 清零 + 映射:三板斧一气呵成;mappages失败(极少见,如页表满)则释放刚拿到的物理页并标记进程killed
  4. uvmunmap配套:两处panic改为continue,让解映射对空洞鲁棒。

注:这一步是官方定义的moderate阶段,目标是让echo hi工作。判断条件用的是"栈顶之上、sz 之下"的简化版;更严格的边界(越界kill、栈下方 Guard 页、fork 复制、系统调用传参、OOM)在后续lazytests阶段补齐,见"复盘·前瞻"。


代码实现

kernel/trap.c——usertrap新增缺页分支

/* * kernel/trap.c */voidusertrap(void){intwhich_dev=0;if((r_sstatus()&SSTATUS_SPP)!=0)panic("usertrap: not from user mode");// send interrupts and exceptions to kerneltrap(),// since we're now in the kernel.w_stvec((uint64)kernelvec);structproc*p=myproc();// save user program counter.p->trapframe->epc=r_sepc();if(r_scause()==8){// system callif(p->killed)exit(-1);// sepc points to the ecall instruction,// but we want to return to the next instruction.p->trapframe->epc+=4;// an interrupt will change sstatus &c registers,// so don't enable until done with those registers.intr_on();syscall();}elseif((which_dev=devintr())!=0){// ok}elseif((r_scause()==13||r_scause()==15)){// 惰性分配导致的缺页异常uint64 fault_va=r_stval();// 获取引发缺页异常的虚拟地址char*pa=0;// 虚拟地址对应的物理地址(待映射)// 判断虚拟地址 fault_va 是否在进程栈空间之中if(PGROUNDUP(p->trapframe->sp)-1<fault_va&&fault_va<p->sz){// 分配一页物理内存if((pa=kalloc())==0)panic("usertrap: kalloc\n");memset(pa,0,PGSIZE);// 将 fault_va 映射到 paif(mappages(p->pagetable,PGROUNDDOWN(fault_va),PGSIZE,(uint64)pa,PTE_R|PTE_W|PTE_X|PTE_U)!=0){printf("lazy alloc: failed to map page\n");kfree(pa);p->killed=1;}}}else{printf("usertrap(): unexpected scause %p pid=%d\n",r_scause(),p->pid);printf(" sepc=%p stval=%p\n",r_sepc(),r_stval());p->killed=1;}if(p->killed)exit(-1);// give up the CPU if this is a timer interrupt.if(which_dev==2)yield();usertrapret();}

逐行理解:

kernel/vm.c——uvmunmap不再为缺页 panic

/* * kernel/vm.c */voiduvmunmap(pagetable_tpagetable,uint64 va,uint64 npages,intdo_free){uint64 a;pte_t*pte;if((va%PGSIZE)!=0)panic("uvmunmap: not aligned");for(a=va;a<va+npages*PGSIZE;a+=PGSIZE){if((pte=walk(pagetable,a,0))==0)// panic("uvmunmap: walk");continue;// 惰性分配:遇到不存在的 pte 就跳过if((*pte&PTE_V)==0)// panic("uvmunmap: not mapped");continue;// 同上if(PTE_FLAGS(*pte)==PTE_V)panic("uvmunmap: not a leaf");if(do_free){uint64 pa=PTE2PA(*pte);kfree((void*)pa);}*pte=0;}}

改动极小但关键:


验证

按官方步骤启动 xv6,执行最简单命令:

$makeqemu
$echohi hi

应正常打印hi不再 panic。这说明:shell 经sbrk申请的堆(只记账),在被访问时由usertrap的 lazy handler 当场补了一页物理内存并映射,进程得以继续。

为确认"确实发生了惰性分配",可借助 Lab3pgtblvmprint(若已在该分支)或临时printf缺页分支,观察echo hi期间至少触发了一次(甚至两次)缺页——这正是 lazy 的实证。

提示:本步只要求echo hi工作。更严苛的lazytests/usertests还会测越界、fork、系统调用传参等边界,当前实现在这些场景可能仍不过——它们是下一个阶段要补的,见"复盘·前瞻"。


复盘

本实验解决了什么

  1. lazy allocation 真正闭环了sbrk记账(第一步)→ 访问缺页(CPU)→usertrap补分配(本步)→ 重执行成功。三步走完,内存从"申请即分配"变成了"用到才给"。
  2. 亲手实现了一次缺页处理:把 Lab4 学的"异常 →usertrap"框架,第一次用于真正修复一个问题(而非打印/报警)。scause/stval/sepc三个寄存器从概念变成了手里的工具。
  3. uvmunmap的鲁棒化:理解了"解映射遇到空洞"是惰性分配的固有常态,配套改成跳过——这一步让整个机制自洽。

与真实操作系统(Linux)的对比

demand paging 是现代 OS 的标配,本步就是它的极简版:

收获

前瞻:lazytests 阶段还要补的边界

当前实现能跑echo hi,但官方lazytests/usertests会测更多场景,需要继续加固:

  1. 越界要kill:若fault_va >= p->sz(超过sbrk分配的上界),应p->killed = 1而非默默分配——否则会无限缺页死循环(注意当前else if已匹配scause13/15,越界地址不会落进原else,会静默无操作 → 回用户态又缺页 → busy loop)。
  2. 栈下方 Guard 页fault_va落在栈正下方那页(非法)也要kill
  3. fork 复制uvmcopy要能处理"父进程有惰性空洞"的情况(空洞子进程也记为空洞,不能试图复制不存在的页)。
  4. 系统调用传参read/write等传入sbrk地址但内存未分配时,内核copyin/copyout也要能触发分配。
  5. OOMkalloc()失败时不应panic整个内核,而应kill当前进程(用p->killed = 1)。

建议把"前置知识里的 PTE 权限表"和"实现思路里的 lazy 闭环图"存下——Lab6 的 Copy-on-Write fork 会复用同一套缺页处理框架(只不过 handler 从"分配新页"变成"复制父页"),思维模型一脉相承。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 16:42:08

企业级网页资源智能捕获系统:5大场景完整解决方案

企业级网页资源智能捕获系统&#xff1a;5大场景完整解决方案 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;cat-catch&#xff…

作者头像 李华
网站建设 2026/8/12 16:41:14

怎么做品牌AI智能传播?传播易如何提升大模型内容可收录率?

随着AI智能搜索、大语言模型的大规模落地与普及&#xff0c;用户获取资讯、解答行业疑问、完成消费决策的信息底层逻辑已彻底重构&#xff0c;品牌内容传播的效果评判标准也迎来根本性迭代。过往市场、品牌方评估发稿价值&#xff0c;核心聚焦百度收录速度、新闻源标识、网页曝…

作者头像 李华
网站建设 2026/8/12 16:39:37

Obsidian到Notion:5分钟实现笔记双向同步的终极指南

Obsidian到Notion&#xff1a;5分钟实现笔记双向同步的终极指南 【免费下载链接】obsidian-to-notion Share obsidian markdown file to notion and generate notion share link 同步obsdian文件到notion&#xff0c;并生成notion分享链接&#xff0c;可以方便的分享obsidian的…

作者头像 李华
网站建设 2026/8/12 16:39:11

UDP协议核心原理与实时传输实践指南

1. UDP协议基础解析 UDP&#xff08;User Datagram Protocol&#xff09;作为互联网核心传输协议之一&#xff0c;与TCP共同构成了传输层的基础架构。我在实际网络调试中发现&#xff0c;许多开发者对UDP存在认知偏差——要么过度简化其价值&#xff0c;要么因"不可靠&quo…

作者头像 李华