新闻详情

新闻详情

首页 / 资讯中心 / 详情

MIPS架构下从零手写操作系统:异常处理、进程调度与内存管理全解析

发布时间:2026/9/20 16:38:25来源:尧图网络
MIPS架构下从零手写操作系统:异常处理、进程调度与内存管理全解析
简介北航“小操作系统”课程实验lab1〜lab6的完整工程代码面向计算机专业学生、考研复试者及操作系统入门开发者。实验以MIPS架构为主线从底层中断处理、内存管理到进程调度、同步互斥、文件系统与虚拟化逐步递进适合在SPIM或QEMU仿真环境中对照学习。压缩包共123个文件以c源文件、h头文件、s汇编文件为主配合makefile构建脚本、链接脚本及少量img系统镜像和工具程序整体仅1.3MB目录结构清晰便于按实验模块查阅。已有1910人学习下载。通过学习这套代码读者能看到中断向量如何建立、页表与进程切换如何衔接以及文件系统和系统调用如何串联起来借助配套构建脚本可在本地复现实验环境并进行二次修改调试对理解操作系统核心原理和积累底层编程经验很有帮助。 如果你正在为北航的MIPS小操作系统实验发愁或者单纯想知道“自己从零写一个能跑用户进程的操作系统”到底是什么体验那这篇文章应该能帮到你。BUAA-MIPS-OS是我把lab1到lab6完整做下来之后整理的一套实验代码与实现思路覆盖了从异常处理、系统调用、时钟中断、进程调度一路到内存管理和并发信号量的全部核心模块。相比网上零散的代码片段和只讲结果的实验报告我更想把每个lab背后的设计逻辑、踩坑过程和调试手段讲清楚适合正在做这套实验的学弟学妹也适合对MIPS架构下OS原理感兴趣的读者。1. 这套实验设计背后的用意lab1到lab6到底让你经历什么1.1 从“裸机程序”到“操作系统”的距离第一次看到实验要求时我其实没意识到事情有多严重。前几周还在用MARS写单周期CPU的实验突然切换成“写一个操作系统”感觉跨度大得离谱。但做完之后再回头想lab1到lab6并不是要你实现Linux那种巨型内核它真正想训练的是在没有操作系统的情况下一台MIPS机器上有哪些机制是必须由软件自己补上的。你回想一下平时写的MIPS汇编程序从main开始顺序执行用到内存就直接lw/sw需要输出就调MARS提供的syscall功能号。但这一切能工作是因为MARS这个模拟器替你隐藏了两件事中断和异常处理入口在哪里以及系统调用是怎么从用户程序跳到内核代码的。BUAA-MIPS-OS的第一个test就是让你亲手写一段异常处理程序挂到0x80000180这个地址上然后触发一个异常看看会发生什么。用一句概括就是操作系统本质上是一个“永远不会返回的大号异常处理程序”。后面的所有功能——进程切换、文件操作、输出字符——都是在不停地回答一个问题CPU收到了异常/中断下一步执行谁、怎么恢复现场。1.2 六个lab的能力目标拆解lab1搭建异常处理基础框架掌握CP0协处理器中Status、Cause、EPC等寄存器的读写写一个能捕获异常的通用入口。lab2实现系统调用也就是提供syscall指令对应的内核服务比如打印字符、读取输入、进程退出。这一步让你体验“用户态请求进入内核态服务”的完整路径。lab3引入时钟中断实现基于时间片的进程调度。多个用户进程轮流占用CPU涉及定时器设置、进程控制块PCB、上下文切换三件套。lab4在进程调度基础之上增加进程阻塞与唤醒机制进程不再是无脑轮转而是可根据事件睡眠和苏醒开始有“状态机”的雏形。lab5进程间通信的同步互斥典型需求是生产者消费者问题需要实现信号量这个东西的锁表、等待队列、调度配合。lab6虚拟内存管理完成虚实地址转换相关支持包括TLB异常处理、多级页表或简化的内存分配策略。从依赖关系上lab1和lab2是地基lab3是承重墙lab4到lab6是不同方向的加固。如果lab1的异常现场保存得不够干净后面的进程切换、系统调用、信号量全都会莫名其妙崩溃且问题极其难查。1.3 为什么北航要选MIPS而不是x86/ARM很多人不理解这个选择觉得MIPS太古老、实际工业界用得少。但从教学角度看MIPS的指令集规整、异常流程简单、寄存器数量适中几乎没有x86那种历史包袱分段、特权级多层嵌套、各种模式切换。在MIPS上用户态/内核态切换就是改CP0.Status寄存器中的UX/SX/KX位异常入口直接跳转到固定地址规则少反而能把“操作系统的核心概念”本身暴露得更清楚。MARS和Logisim的配合也是一大原因。MARS能直观看到寄存器、内存、标签地址的变化Logisim可以从硬件层面连出CPU的数据通路并加载同一份指令软硬件实验能无缝衔接。这一点是x86无法比拟的因为x86模拟器里你很难直观理解“异常入口地址怎么来的”。2. 最容易被低估的基础工程异常入口与上下文切换2.1 异常入口0x80000180处发生了什么MIPS架构规定当CPU收到异常或中断时会跳转到两个可能的地址0x80000000或0xBFC00000取决于硬件设计复位入口也可用于部分异常。0x80000180通用异常入口。所有的syscall指令、算术溢出、TLB缺失、时钟中断等最终都汇聚到这里。这意味着你只有一个入口却要处理N种异常来源。所以myOS的核心入口代码必须做一个分诊操作读取Cause寄存器的ExcCode字段判断异常类型再跳转到对应的处理函数。# .set noreorder mfc0 k0, Cause andi k0, k0, 0x007c .set noreorder bne k0, zero, check_syscall nop # 默认中断处理 j general_exception_handler nop check_syscall: li k1, 0x0008 # ExcCode8 是syscall bne k0, k1, check_other nop j syscall_handler nop这里有个MIPS经典坑异常入口地址0x80000180属于内核段kseg0CPU在异常发生时并不会自动切到内核栈也不会自动保存通用寄存器。所有现场保存工作都是程序员自己写汇编完成。如果忘记保存某个寄存器后续的C代码一调用函数那个寄存器就会被覆盖系统行为会变得完全不可预测。2.2 寄存器现场保存的最小集合MIPS有32个通用寄存器。但在异常处理里不能全部压栈吗物理上可以但没必要。规范做法是区分“调用者保存”和“被调用者保存”调用者保存caller-saved$t0-$t7、$t8-$t9、$a0-$a3、$v0-$v1、$at。异常处理函数内部如果使用它们不需要额外保存但如果你要返回用户态并保证用户程序看到的寄存器值不变就需要保存。被调用者保存callee-saved$s0-$s7、$fp、$ra。这些必须在调用C函数之前保存好因为C函数假设可以任意修改它们。我实际实现时采用“两条路结合”异常入口处把全部通用寄存器压到内核栈进入interrupt_helper后按需使用退出时统一恢复。虽然性能上有浪费但通篇实验代码可维护性非常高调试时不用猜哪个寄存器被内核改过。等到lab3做进程切换你会感谢当时这么做了——因为每个进程的上下文快照就是你保存的这些寄存器。2.3 我踩过的第一个大坑EPC没处理导致死循环第一次写异常处理时我的sleep系统调用怎么都sleep不成功。现象是进程睡下去之后马上被唤醒然后继续跑跟没sleep一样。查了一个多小时最后定位到原因我没有对EPC做“4”处理。MIPS的syscall指令执行后EPC寄存器保存的是syscall指令本身的地址。异常处理完返回时如果直接jr epcCPU会重新执行同一条syscall指令于是无限循环触发系统调用。必须手动执行addiu k0, k0, 4让EPC指向下一条指令再返回。这是所有写MIPS OS的人都会遇到的第一课也解释了为什么异常处理程序里一定要对EPC分场景处理遇到非法指令异常可能要终止进程遇到syscall要跳过当前指令遇到中断则不需要调整EPC。3. 系统调用实现的几种边界情况3.1 syscall功能分发设计lab2要求实现的系统调用其实不复杂但设计上要有一个清晰的调度表。我维护了一个函数指针数组因为MIPS的syscall号通常从0开始递增这样syscall_handler只需把$v0里的功能号作为索引查表typedef int (*syscall_fn)(uint32_t arg0, uint32_t arg1, uint32_t arg2); static const syscall_fn syscall_table[] { [SYS_NONE] sys_none, [SYS_PRINT_STR] sys_print_str, [SYS_GETCHAR] sys_getchar, [SYS_EXIT] sys_exit, [SYS_SLEEP] sys_sleep, [SYS_GET_TICKS] sys_get_ticks, };参数传递约定走MIPS的C调用规则$a0、$a1、$a2依次是前三个参数返回值放$v0。但要注意一点用户进程传入的字符串地址是用户虚拟地址内核在访问前必须确认这个地址落在合法的用户数据段内否则一个恶意程序传入0xFFFFFFFF就会把内核踩爆。我在lab6做TLB的时候专门加了一层access_ok()校验。3.2 访问用户内存的一个高频bug有一次测试打印字符串总是莫名打印出一堆乱码。查了很久发现问题不在打印逻辑而在用户地址访问方式。用户程序通过hid系统调用打印字符串时sys_print_str会按字节读取用户传入的缓冲区。但在lab6之前我们并没有启用TLB所有地址都直接用物理地址访问所以读取没问题。可是我在实现中犯了一个错误用memcpy直接把内核缓冲区的地址返回给用户用户侧拿到的根本不是一个合法地址。正确做法是让用户程序传缓冲区的虚拟地址内核使用copy_from_user或copy_to_user来搬数据同时在拷贝前检查长度的合法性。这一步边界条件非常多字符串可能没有\0结尾、长度可能超出段尾、访问可能跨页。我在lab6里为这个写了一个专门的备份机制发生TLB异常时能动态加载页表项避免直接懒加载导致的数据错乱。3.3 系统调用与进程调度的交互点做到lab3之后系统调用就不再是“执行完就返回”这么简单了。sleep系统调用的执行过程会直接改变当前进程的状态sys_sleep(int ticks) { current-state TASK_SLEEPING; current-wakeup_tick ticks_count ticks; schedule(); // 主动让出CPU }这个设计意味着系统调用处理函数并不是总在用户上下文里做完所有事有些操作会触发进程切换。所以写系统调用时脑子里要有“当前进程可能被换走换回来之后还要继续往下走”的模型。如果你在sys_sleep里用了全局变量的临时值进程被切换回来时那个值可能已经不是之前的值了。我在写lab3的sys_exit时就遇到这个问题进程退出前释放PCB但schedule()还得依赖当前PCB选出下一个进程。最后解决办法是先选出下一个进程再释放当前PCB顺序不能反。4. 时钟中断与进程调度的配合4.1 时钟中断是操作系统的“心跳”在没有任何外部事件的情况下进程会一直占着CPU不放。要让多个进程轮流跑必须有一个“定时炸弹”周期性引爆——这就是时钟中断。MIPS的计数器Count寄存器每个时钟周期加1比较器Compare寄存器与它相等时触发一次中断。void setup_timer_irq(uint32_t interval) { uint32_t count get_cp0_count(); set_cp0_compare(count interval); set_cp0_status(get_cp0_status() | (1 15)); // 开启IM7中断 }4.2 调度器怎么选择下一个进程lab3的核心不是“如何切换上下文”而是“上下文切换的时机”。我用的调度算法是极简的时间片轮转维护一个就绪队列每次时钟中断把当前进程放回队尾从队头取下一个进程。但队列操作的临界区保护很重要——时钟中断可能在进程正在入队/出队的瞬间触发导致链表损坏。我用了一个全局的“调度锁”void schedule(void) { disable_irq(); // 关中断 // 选择next进程 // 切换上下文 enable_irq(); // 恢复中断 }4.3 用户上下文切换的实现从lab3起每个进程都拥有独立的内核栈但用户栈是共享物理内存的地址空间。上下文切换的逻辑是当前进程在内核态执行异常处理栈我们把它的通用寄存器压入当前内核栈然后用一个结构体记录sp再从下一个进程的PCB中恢复sp和通用寄存器最后jr回它的EPC4处。这段汇编每次看都容易晕但核心不外乎保存旧现场到旧内核栈恢复新现场从新内核栈最后返回用户态。我写lab3时遇到过的奇葩bug是两个进程同时使用同一个$gp寄存器全局指针导致其中一个进程的静态数据全部解析失败。最终在PCB中多存了一份$gp每次上下文切换时重新设置问题才解决。5. 虚实地址转换与TLB异常处理5.1 MIPS内存布局MIPS把地址空间分成几个固定区域任何程序都必须遵守这个分段规则0x00000000 - 0x7fffffff用户态可访问的kuseg。0x80000000 - 0x9fffffff内核段kseg0有缓存但无需TLB映射。0xa0000000 - 0xbfffffff内核段kseg1无缓存。0xc0000000 - 0xffffffff内核段kseg2需要TLB映射。lab6要求的是用户进程地址空间相互隔离不能让进程A读到进程B的数据。这意味着所有用户地址都要通过TLB走一遍翻译。另外MARS模拟器对TLB支持不够完善需要在硬件模拟环境或特定配置下才能完整验证。5.2 TLB缺失处理流程当CPU访问一个TLB中不存在的虚拟页时产生TLB Refill异常ExcCode2。操作系统要做两件事从当前进程的页表中找到对应的物理页。把映射关系写入TLB然后重新执行那条导致异常的指令。代码框架大致如下void tlb_miss_handler(uint32_t badvaddr, uint32_t context) { pte_t *entry find_pte(current-pgd, badvaddr); if (entry entry-present) { syscall_tlb_write(entry-ppn, badvaddr, entry-flags); } else { page_fault(badvaddr); // 缺页或非法访问 } }这里有一个容易忽略的细节TLB是全局共享的切换进程时必须把当前进程自己的映射刷进TLB。简单做法是每次schedule()后都tlb_flush()也可以给每个TLB Entry带上ASID地址空间标识符来避免刷。我在实验中先用全局刷的办法功能没问题但性能极差。5.3 内存管理中最容易翻车的场景lab6里我犯的错误非常典型在用户地址分配物理页时用错了分配器。内核里明明有一堆可用的内存我却只在一个固定数组里找导致用户程序稍微跑大一点就分配失败。后来我把内核物理内存做成一个简单的位图每次分配时扫描第一个空闲页。另外一个设计教训是页表本身必须由物理页构成不能用动态分配得到的内核虚拟地址直接当物理地址用。这是我之前没意识到的如果直接存内核地址一旦TLB开启后那个地址反而会再被翻译一次出现循环映射。6. 同步与信号量的实现细节6.1 临界区为什么难写lab5是并发同步典型题目是“生产者-消费者”。你可能会想既然MARS是单核模拟那不就是顺序执行吗还需要锁吗问题在于中断会随时插入。一个进程正在修改共享缓冲区时时钟中断触发CPU切换到了另一个进程这个进程也试图修改同一个缓冲区数据就乱了。所以临界区保护的本质是在进入临界区时关闭中断出临界区时恢复中断。6.2 信号量的经典实现我实现的信号量底层依赖关中断void sem_wait(semaphore_t *s) { disable_irq(); while (s-count 0) { s-wait_queue_head task_sleep(s-wait_queue_head); enable_irq(); schedule(); disable_irq(); } s-count--; enable_irq(); } void sem_signal(semaphore_t *s) { disable_irq(); s-count; if (s-wait_queue_head ! NULL) { task_wakeup(s-wait_queue_head); s-wait_queue_head s-wait_queue_head-next; } enable_irq(); }注意sem_wait里的while循环不能写成if因为进程可能被虚假唤醒或等待条件再次不成立必须重新检查状态。这也是我在实验报告中特别强调的点。6.3 优先级反转和死锁北航这套实验不会强制要求实现优先级继承但你测试时很容易遇到“低优先级进程占着锁高优先级进程一直等”的假死。我在实验时通过简单的调整调度策略缓解了这个问题当唤醒一个进程时把它放到就绪队列头部而不是队尾。这样被唤醒的进程能尽快运行并释放锁。死锁也是实验报告中喜欢问的内容。我总结的死锁条件是两个进程在持有锁的同时分别等待对方持有的锁。调试方法很土但有效——在每个sleep处打印一条调试日志看停在哪一步。配合MARS的断点功能可以依次观察每个进程从运行到阻塞的完整生命周期判断是否有一方永远无法醒来。7. 调试经验MARS单步、日志与复现7.1 MARS模拟器的调试技巧MARS支持断点、单步、查看寄存器、查看内存这是初始阶段最方便的调试手段。但lab3之后的用户进程调度复杂了MARS的单步反而会打乱时序因为中断指令和异常指令下的单步行为和真实CPU不完全一样。我后来更多靠输出日志来调试// 红色串口输出保证不换行影响后续排版 void debug_trace(char *msg, uint32_t value) { print_str([TRACE] ); print_str(msg); print_str( 0x); print_hex(value); print_str(\n); }7.2 硬件联调时的痛苦体验logisim联调时CPU时钟比MARS慢得多而且出现问题时很难定位是软件逻辑还是硬件电路问题。我的经验是先走一遍MARS上的同程序再逐步替换硬件实现每次都只改一小块。比如先只处理syscall跑通后再加中断加完中断再加多进程。7.3 最让我受益的设计习惯做完整个lab1到lab6我最大的心得是把实验代码当成真正的工程来管理而不是每次临时复制粘贴。我给每个lab建立独立的git分支共用底层的异常入口、上下文切换代码和基础日志库新增功能时只动高层逻辑。这样改坏了随时回滚也方便对比不同版本之间的行为差异能极大提高调试效率。最后分享一个小建议这套实验不要求你把每个细节都做到工业级但一定要亲手把每一条汇编指令、每一个状态位的含义弄明白。我做完这六个lab以后再反过来看Linux内核中关于系统调用入口、schedule()、do_exit()这些代码理解速度和深度比以前翻倍因为你已经亲手把这些模块从零实现了一遍。】本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

抖音批量下载实战指南:douyin-downloader 五步搞定无水印下载、主页归档与直播录制 2026/9/20 18:50:54

抖音批量下载实战指南:douyin-downloader 五步搞定无水印下载、主页归档与直播录制

抖音批量下载实战指南:douyin-downloader 五步搞定无水印下载、主页归档与直播录制 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication,…

阅读更多 →
CANN PyPTO `pl.simt.ceil`:SIMT 标量向上取整运算的用法与底层实现解析 2026/9/20 18:50:54

CANN PyPTO `pl.simt.ceil`:SIMT 标量向上取整运算的用法与底层实现解析

CANN PyPTO pl.simt.ceil:SIMT 标量向上取整运算的用法与底层实现解析 【免费下载链接】pypto PyPTO(发音: pai p-t-o):Parallel Tensor/Tile Operation编程范式。 项目地址: https://gitcode.com/cann/pypto 导读 pypto_…

阅读更多 →
Coding Agent效率翻倍:10个实战验证的Skills推荐与避坑指南 2026/9/20 18:50:54

Coding Agent效率翻倍:10个实战验证的Skills推荐与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
临床级多模态脑功能监测系统:fNIRS与EEG同步采集实践 2026/9/20 18:50:54

临床级多模态脑功能监测系统:fNIRS与EEG同步采集实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
结构疲劳分析基础:S-N曲线、应力集中与累积损伤工程指南 2026/9/20 18:50:54

结构疲劳分析基础:S-N曲线、应力集中与累积损伤工程指南

简介:这是疲劳与断裂力学课程第4章“结构疲劳分析基础”的教学课件,面向机械、航空航天、土木等专业学生及从事结构强度设计的工程师,系统讲解结构在循环载荷下的疲劳损伤规律与寿命评估方法。课件基于作者的课程讲义整理,围绕疲劳…

阅读更多 →
Learun.Framework 7.0.2实战:从源码解压到二次开发全流程解析 2026/9/20 18:47:54

Learun.Framework 7.0.2实战:从源码解压到二次开发全流程解析

简介:Long.Learun.Framework(力软)7.0.2是一套基于ASP.NET与C#的敏捷开发框架源码包,适合需要在.NET环境中快速迭代企业级Web应用,或以此为基础进行二次开发的团队与个人开发者。压缩包含4366个文件,总大小…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞