尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MIT 6.S081 traps 实验篇(lab4):Alarm (hard)

MIT 6.S081 traps 实验篇(lab4):Alarm (hard) Alarm (hard)实验目标本实验要实现两个系统调用sigalarm与sigreturn为用户进程增加周期性通知能力进程每使用 CPU 若干个 tick 后内核就自动把它拽去执行一个用户态的回调函数handler执行完再原封不动地退回原处继续跑。本质上这是用户态的定时中断 / 异常处理——内核模拟了硬件中断的语义但 handler 运行在用户空间。练习目标打通sigalarm(n, fn)注册每n个 tick 调一次fn与sigreturn()handler 结束后恢复现场的完整链路。吃透trapframe 的保存与恢复——这是整个实验的灵魂为什么必须单独存一份alarm_trapframe处理好重入re-entrancyhandler 自己还没返回时闹钟又响了怎么办用alarm_goingoff标志挡住嵌套对应官方test2。这一关是 lab4 的硬骨头直接考验你对 trap 全流程用户态↔内核态切换、现场保存恢复的理解是否真的落地。前置知识1. 什么是 trapframe为什么需要另一份trapframe是 xv6 为每个进程准备的一页结构体陷入内核时用户寄存器现场epc、ra、sp、a0-a7、s0-s11…全部原样保存在这里返回用户态时再搬回去。结构见kernel/trapframe.h本质上是按 RISC-V 规范排布的一串寄存器槽位。关键在于handler 是用户函数它运行期间如果又做了系统调用比如sigreturn本身甚至printf内核会再次覆盖同一个trapframe。所以一旦决定要跳去执行 handler就必须先把被打断那一刻的用户现场另存一份到alarm_trapframe否则原程序的寄存器现场就永远丢了、回不去了。alarm_trapframe就是为此而生的备份现场。2. 定时器中断路径usertrapxv6 的时钟中断来自 CLINTusertrap()里通过which_dev 2判定是定时器中断。每次时钟 tickticks全局计数器 1随后usertrap调用yield()让出 CPU。我们要做的就是在这条路径上插桩tick 到点了把epc改成 handler 地址用户态返回时就会跑去 handler 而非原指令。3. 重入问题与alarm_goingoff如果 handler 执行时间较长期间又过了若干 tick闹钟可能再次到期。若此时直接再次跳去 handler会覆盖alarm_trapframe上一次现场还没恢复导致第一次调用永远无法返回——灾难。解决用一个标志alarm_goingoff表示当前已有 handler 在跑未返回前绝不再触发把这次到期顺延到 handler 结束后。这正是官方alarmtest的test2要测的场景。4. 系统调用注册链路Lab2 复习新增一个系统调用需要同步改 6 处详见本博客代码实现末节user/user.h声明→user/usys.pl生成usys.S桩→kernel/syscall.h分配编号→kernel/syscall.c分发表 extern 声明→kernel/sysproc.csys_xxx实现→kernel/defs.h内核内声明。5. 需要改动 / 新增的文件文件改动kernel/proc.h在struct proc增加 5 个 alarm 字段kernel/proc.callocproc分配并初始化、freeproc释放kernel/sysproc.csys_sigalarm/sys_sigreturn取参并调用内核实现kernel/trap.csigalarm/sigreturn真正实现usertrap中插桩触发user/user.huser/usys.plkernel/syscall.hkernel/syscall.cMakefilekernel/defs.h系统调用注册链路实现思路整体数据流如下注册用户调sigalarm(n, fn)→ 内核把n/fn存进进程并把alarm_ticks剩余倒计时初始化为n。触发每次定时器中断usertrap里alarm_ticks倒计时归零且alarm_goingoff0时把当前trapframe备份到alarm_trapframe、把trapframe-epc改成 handler 地址、alarm_goingoff1。随后照常usertrapret回到用户态——但这次回去是去执行 handler。返回handler 干完活调sigreturn()→ 内核把alarm_trapframe拷回trapframe现场复原清alarm_goingoff0。再usertrapret回去就回到了被打断的那条指令原程序无感知地继续。代码实现kernel/proc.h—— 进程结构体新增字段/* * kernel/proc.h */// Per-process statestructproc{...// 时钟相关intalarm_interval;// 时钟周期为 0 表示禁用时钟void(*alarm_handler)();// 时钟回调处理函数intalarm_ticks;// 当前时钟信号数(ticks数)structtrapframe*alarm_trapframe;// 时钟中断时刻进程的陷阱帧用于恢复中断前的状态intalarm_goingoff;// 是否已经有一个时钟中断正在执行且未返回};五个字段各司其职alarm_interval是周期0 即停用、alarm_handler是回调、alarm_ticks是剩余倒计时、alarm_trapframe是现场备份、alarm_goingoff防重入。kernel/proc.c—— 分配与释放allocproc里在分配完普通trapframe之后紧接着为alarm_trapframe也kalloc一页并初始化所有 alarm 字段/* * kernel/proc.c */staticstructproc*allocproc(void){...found:p-pidallocpid();// Allocate a trapframe page.if((p-trapframe(structtrapframe*)kalloc())0){release(p-lock);return0;}// 为 alarm_trapframe 分配陷阱帧if((p-alarm_trapframe(structtrapframe*)kalloc())0){release(p-lock);return0;}// 进程创建时初始化 alarm 相关变量p-alarm_interval0;p-alarm_handler0;p-alarm_ticks0;p-alarm_goingoff0;...}freeproc负责回收:/* * kernel/proc.c */staticvoidfreeproc(structproc*p){if(p-trapframe)kfree((void*)p-trapframe);p-trapframe0;// 释放 alarm_trapframeif(p-alarm_trapframe)kfree((void*)p-alarm_trapframe);p-alarm_trapframe0;if(p-pagetable)proc_freepagetable(p-pagetable,p-sz);p-pagetable0;p-sz0;p-pid0;p-parent0;p-name[0]0;p-chan0;p-killed0;p-xstate0;p-alarm_interval0;p-alarm_handler0;p-alarm_ticks0;p-alarm_goingoff0;p-stateUNUSED;}kernel/sysproc.c—— 两个系统调用的入口从用户态取出参数后转交给trap.c里的真正实现。argint取整型nargaddr取函数指针fn在用户空间是地址/* * kernel/sysproc.c */uint64sys_sigalarm(void){intn;// n 个 ticksuint64 fn;// 时钟回调函数if(argint(0,n)0)// 获取第一个参数return-1;if(argaddr(1,fn)0)// 获取第二个参数return-1;returnsigalarm(n,(void(*)())(fn));// 调用并返回 sigalarm 函数}uint64sys_sigreturn(void){returnsigreturn();}kernel/trap.c—— 核心实现与触发点sigalarm注册、sigreturn恢复现场/* * kernel/trap.c */// 设置进程中时钟的相关属性intsigalarm(intticks,void(*handler)()){structproc*pmyproc();p-alarm_intervalticks;p-alarm_handlerhandler;p-alarm_ticksticks;return0;}// 将进程恢复到时钟中断前的状态intsigreturn(void){structproc*pmyproc();*p-trapframe*p-alarm_trapframe;p-alarm_goingoff0;return0;}注意sigreturn把整份alarm_trapframe拷回trapframe包括epc——于是返回用户态时sepc 原指令地址原程序从被打断处重新执行xv6 的 alarm 不跳过指令设计如此。a0等寄存器也一并复原handler 的返回值对用户而言并不存在因为它根本没返回而是被整体替换回原现场。在usertrap的定时器分支里插桩触发逻辑这是把整套机制串起来的关键/* * kernel/trap.c */voidusertrap(void){...if(which_dev2){if(p-alarm_interval!0--p-alarm_ticks0p-alarm_goingoff0){/* 是否设置了时钟 时钟倒计时是否结束 没有其他时钟正在运行 * 如果一个时钟到期的时候已经有一个时钟处理函数正在运行 * 则会推迟到原处理函数运行完成后的下一个 tick 才触发这次时钟 */p-alarm_ticksp-alarm_interval;*p-alarm_trapframe*p-trapframe;// 保存当前进程陷阱帧p-trapframe-epc(uint64)p-alarm_handler;// 跳转到时钟回调函数p-alarm_goingoff1;// 标记当前已经有时钟在运行}yield();}usertrapret();}逻辑要点倒计时--alarm_ticks 0归零、且没有 handler 在跑goingoff0才触发触发时先备份现场、再改epc指向 handler、置goingoff1、重置倒计时为周期。最后无条件yield()让出 CPU定时器中断的常规动作。系统调用注册链路Lab2 复习建议自己默写/* * user/user.h */// system calls...intsigalarm(int,void(*)());intsigreturn(void);.../* * user/usys.pl */...entry(sigalarm);entry(sigreturn);/* * kernel/syscall.h */#defineSYS_sigalarm22#defineSYS_sigreturn23/* * kernel/syscall.c */...externuint64sys_sigalarm(void);externuint64sys_sigreturn(void);staticuint64(*syscalls[])(void){...[SYS_sigalarm]sys_sigalarm,[SYS_sigreturn]sys_sigreturn,};/* * Makefile */ifeq($(LAB),traps)UPROGS\ $U/_call\ $U/_bttest\ $U/_alarmtest\ $U/_usertests endif/* * kernel/defs.h */...// trap.cexternuint ticks;voidtrapinit(void);voidtrapinithart(void);externstructspinlocktickslock;voidusertrapret(void);intsigalarm(int,void(*)());intsigreturn(void);...注系统调用编号在你本机可能因已有 syscall 数量略有不同以kernel/syscall.h里现有最大值为准顺延即可标准 xv6-2020 traps 环境下SYS_uptime21所以 22/23 是正确且空闲的。验证方式一跑官方测试程序makeqemu在 xv6 shell 里执行alarmtest应依次通过test0基础定时回调、test1不会过早触发、test2handler 未返回期间不重入靠goingoff保证。方式二评分脚本./grade-lab-traps alarm应看到alarmtest相关子测试全部 OK。若test2失败优先检查usertrap里是否漏了alarm_goingoff 0守卫或freeproc释放逻辑是否合理alarm_trapframe必须单独释放且只释放一次。复盘本实验解决了什么真正打通了用户态中断的完整闭环注册sigalarm→ 触发usertrap 改 epc→ 执行handler→ 恢复sigreturn 还原 trapframe。这条链路把 Lab2 学的系统调用、Lab4 前半段学的 trap 流程全部串起来了。trapframe 备份的思想是核心收获一旦意识到handler 里再陷内核会覆盖 trapframe就必须存一份独立备份——这个现场保存/恢复的抽象和后面学上下文切换context switch、进程调度是同一个套路。重入保护goingoff是工业级代码的必备意识任何回调 / 信号处理 / 中断机制都必须考虑回调执行期间事件再次到来的情况否则现场互相覆盖、程序崩溃。与真实操作系统信号signal的对比xv6 的 alarm 是极度简化版的 Unix signal真实 Linux 的SIGALRM同样靠内核在定时器到期时打断进程、跳转 handler但 Linux 信号有屏蔽字mask、可重入也可嵌套、有sa_restart等复杂语义。xv6 这里不跳过被打断的指令恢复后从原epc重执行也没有信号嵌套/排队理解起来更直观性价比极高。收获trapframe 与上下文保存能讲清为什么需要单独一份alarm_trapframe这是 OS 面试里 trap/interrupt 章节的高频追问。用户态↔内核态切换全流程ecall陷入 →usertrap保存现场 → 处理 →usertrapret恢复 →sret返回能把 epc/trapframe/sepc 的关系说顺。重入与竞态goingoff标志是用一个布尔变量防止回调重入的最小可用范例可延伸到中断下半部可重入函数等话题。系统调用链路6 处同步修改已能闭眼默写是后续任何 syscall 实验的基础肌肉记忆。建议把前置知识里的 trapframe 备份图和usertrap触发逻辑存下——后面做调度、做 COW fork、甚至以后读 Linux 信号源码这套现场保存 / 恢复 / 防重入的思维模型会反复出现。
返回列表