
简介这份资源是面向高校操作系统课程学习者与系统软件开发入门者的HNU_ostep实验合集围绕进程管理、内存管理、文件系统与I/O系统等核心模块展开帮助读者在理论学习之外通过动手编码理解操作系统的内部机制。压缩包共收录2000个文件以910个C源文件与760个头文件为主体辅以Python脚本、Shell脚本、CMake与Make构建文件、Markdown与TXT说明文档以及少量PDF、DOCX讲义资料整体约42.51MB目录结构便于按实验模块检索。其中ostep_code目录提供实验相关源码蓝本readme.txt给出实验总览、环境搭建与任务要求读者可据此修改调度算法、观察不同策略对进程执行效率的影响并借助附书资料加深对虚拟内存、页面置换、文件权限控制等机制的理解。目前已有165人学习适合需要系统完成操作系统实验、积累底层开发经验的学生参考。1. 操作系统实验到底在练什么一张从内核到用户态的知识地图操作系统实验这门课很多人第一次翻开实验指导书时是懵的——明明理论课刚讲完进程调度、虚拟内存、文件系统实验却让你去读一段几百行的 C 代码还要在某个TODO处填几行。它到底想练什么梳理下来会发现操作系统实验的核心不是让你从零写一个内核而是让你在已有的骨架代码上亲手补全那些理论课上被一笔带过的关键机制进程怎么被创建和切换、内存怎么被映射和回收、文件怎么被打开和读写、系统调用怎么从用户态陷入内核态。整门实验的知识地图大致分四层最底层是引导与中断中间是进程与调度再往上是内存管理最上层是文件系统与系统调用接口。每一层都对应理论课的一个章节但实验要求你看到真实的代码路径而不是只记住概念。适合谁适合已经学过操作系统原理、能看懂 C 和汇编、但一遇到「上下文切换到底保存了哪些寄存器」「页表项里那几个标志位分别管什么」就卡住的人。这份笔记就是把这条路径上的重点、难点和易漏点系统整理一遍让你复习时不用再翻散落的实验报告。2. 进程与调度从 PCB 到上下文切换的复习重点2.1 进程控制块到底存了什么进程控制块PCB是操作系统里最核心的数据结构之一实验里通常要求你补全它的字段定义和初始化逻辑。梳理下来会发现PCB 里真正影响调度和切换的字段就那么几类标识信息pid、父进程 pid、状态信息就绪、运行、阻塞、上下文信息寄存器快照、栈指针、程序计数器、调度信息优先级、时间片剩余、内存信息页表基址、代码段和数据段边界、文件信息打开文件描述符表。实验里最容易翻车的地方是上下文信息的保存顺序——如果保存和恢复的顺序不一致进程切换后就会跑到错误的地址去执行。// 典型的上下文保存结构示意 struct context { uint64_t ra; // 返回地址决定切换后从哪继续执行 uint64_t sp; // 栈指针指向该进程的内核栈 uint64_t s0; // 被调用者保存寄存器 uint64_t s1; // ... 其余被调用者保存寄存器 };这段结构体的关键在于ra和sp必须成对保存和恢复s0~s11这些被调用者保存寄存器由切换函数负责而调用者保存寄存器由编译器在函数调用时自行处理。实验里如果只保存了ra忘了sp切换后栈就乱了表现为进程跑飞或直接触发异常。参数说明ra在 RISC-V 里是返回地址寄存器sp是栈指针不同架构名字不同但作用一致。2.2 调度器不是越复杂越好实验里常见的调度算法有时间片轮转、优先级调度和多级反馈队列。很多人一上来就想写多级反馈队列觉得高级结果时间片和优先级升降规则没调好反而出现进程饿死。血泪经验是先把时间片轮转跑通确保每个进程都能被切换到再加优先级。调度器的核心逻辑其实就三步从就绪队列选一个进程、保存当前进程上下文、恢复目标进程上下文。重点在于就绪队列的维护——进程从阻塞变就绪时要入队时间片用完时要重新入队进程退出时要移出队列。易错点是忘记在时钟中断里递减时间片导致轮转永远不触发。调度算法关键参数适用场景实验常见问题时间片轮转时间片长度通用分时时间片过大退化成先来先服务优先级调度优先级数值实时任务低优先级进程饿死多级反馈队列队列级数、升降规则混合负载规则复杂调试困难2.3 上下文切换的完整路径一次完整的上下文切换发生在这些时刻时钟中断、系统调用、进程主动让出。路径通常是中断/异常入口 → 保存用户态上下文到内核栈 → 进入内核处理 → 调度器选新进程 → 切换内核栈和 PCB → 恢复新进程上下文 → 返回用户态。实验里最容易忽略的是内核栈的切换——每个进程有自己的内核栈切换时必须同时切换栈指针否则新进程会用旧进程的内核栈数据互相覆盖。梳理下来会发现上下文切换的代码通常很短但每一步都不能少少一步就是玄学崩溃。3. 内存管理页表、缺页与地址转换的易错点3.1 页表项里每个标志位都要会用虚拟内存实验的核心是页表。页表项PTE里除了物理页号还有一堆标志位有效位 V该映射是否有效、读写位 R/W是否可写、用户位 U用户态是否可访问、访问位 A是否被访问过、脏位 D是否被写过。实验里经常要求你实现按需分页这时有效位就是关键——初始时把所有页的有效位清零访问时触发缺页异常在异常处理里分配物理页并置有效位。最容易错的是忘记刷新 TLB改了页表项之后如果 TLB 里还有旧缓存CPU 还是会用旧的映射表现为改了页表但程序行为不变。补法是在修改页表后执行sfence.vma或等效的 TLB 刷新指令。// 缺页异常处理的核心逻辑示意 void handle_page_fault(uint64_t fault_addr) { pte_t *pte walk_page_table(fault_addr); if (!(*pte PTE_V)) { // 分配物理页建立映射 void *page alloc_page(); *pte PA2PTE(page) | PTE_V | PTE_R | PTE_W | PTE_U; flush_tlb(); // 关键刷新 TLB } else { // 权限错误或其他异常 kill_process(); } }这段代码里walk_page_table负责逐级查找页表PA2PTE把物理地址转成页表项格式flush_tlb是必须的收尾动作。参数说明fault_addr是触发缺页的虚拟地址通常从异常寄存器里读。实验里如果缺页处理写对了但没刷新 TLB会出现「第一次访问正常第二次访问又缺页」的诡异现象。3.2 地址转换的每一步都要能手工推实验考试或调试时经常需要你手工把一个虚拟地址翻译成物理地址。步骤是从页表基址寄存器拿到一级页表物理地址用虚拟地址的各级索引逐级查表最后加上页内偏移。梳理下来会发现出错最多的地方是索引位数算错——比如 39 位虚拟地址、4KB 页大小、三级页表每级索引 9 位页内偏移 12 位。如果记成每级 10 位整个转换就全错了。建议在实验前把所在架构的地址划分画一遍标清楚每一级的位数和含义。3.3 内存分配器的碎片问题实验里如果要求实现简单的内存分配器重点不是写得多花哨而是避免碎片。常见做法是维护空闲链表分配时找足够大的块释放时合并相邻空闲块。易漏点是合并时只合并了后一块忘了前一块导致空闲块越切越碎。补法是在空闲块头部记录大小和状态释放时检查前后相邻块是否空闲是则合并。这个逻辑在实验里通常几十行就能写完但边界条件很多建议用几个固定场景手工推演一遍。4. 文件系统与系统调用从打开到读写的完整链路4.1 文件描述符表的层级关系文件系统实验里一个容易绕晕的点是文件描述符、打开文件表项、inode 三者之间的关系。梳理下来会发现进程的 PCB 里有一张文件描述符表每个表项指向一个系统级的打开文件表项打开文件表项里记录了读写偏移和指向 inode 的指针inode 里才是文件的元数据和数据块指针。实验里常考的是fork之后文件描述符的共享问题——父子进程共享打开文件表项所以读写偏移是共享的但文件描述符表是各自独立的。如果实验里要求实现dup就是让两个文件描述符指向同一个打开文件表项偏移也共享。层级存储位置关键内容共享范围文件描述符表进程 PCB指向打开文件表项的指针进程内打开文件表项系统全局读写偏移、状态标志、inode 指针可跨进程共享inode磁盘/内存缓存文件元数据、数据块指针全局唯一4.2 系统调用的陷入与返回系统调用是用户态和内核态之间的桥梁。实验里通常要求你补全系统调用的入口和分发逻辑。路径是用户态把系统调用号放入指定寄存器执行陷入指令CPU 切换到内核态入口代码保存用户态上下文根据系统调用号查分发表执行对应处理函数把返回值放回寄存器恢复上下文返回用户态。最容易错的是返回值处理——很多实验框架约定返回值放在特定寄存器如果放错寄存器用户态拿到的就是垃圾值。补法是先看实验框架的文档或注释确认返回值寄存器和错误码约定。// 系统调用分发逻辑示意 long syscall_handler(long num, long arg0, long arg1, long arg2) { switch (num) { case SYS_READ: return sys_read(arg0, (void *)arg1, arg2); case SYS_WRITE: return sys_write(arg0, (void *)arg1, arg2); case SYS_OPEN: return sys_open((const char *)arg0, arg1); case SYS_CLOSE: return sys_close(arg0); default: return -ENOSYS; // 未知系统调用返回错误 } }这段分发逻辑的关键是参数传递顺序和错误码约定。参数说明num是系统调用号arg0~arg2是从用户态寄存器里取出的参数返回值负数表示错误。实验里如果sys_read忘了检查用户态缓冲区地址是否合法就可能被恶意程序利用这也是易漏的安全检查点。4.3 文件读写与缓存一致性文件读写实验里重点在于缓存和磁盘的一致性。如果实现了页缓存或块缓存写操作先写缓存什么时候写回磁盘就是个策略问题。实验里常见做法是写穿透write-through或写回write-back。写穿透简单但性能差写回性能好但要在适当时机刷盘。易错点是关闭文件时忘记刷盘导致数据丢失。补法是在close系统调用里强制把脏块写回。梳理下来会发现文件系统的实验代码量通常比进程和内存大但核心逻辑就是「查缓存、没有就加载、改了标记脏、该刷就刷」这四步。5. 实验调试与查漏补缺那些最容易忽略的细节5.1 易漏点一中断使能状态易漏点在临界区里忘记关中断或者出了临界区忘记开中断。为什么漏中断是异步的调试时如果没触发就感觉不到问题一旦触发就是随机崩溃。怎么补在操作共享数据结构前后成对使用关中断和开中断并在实验报告里标注哪些代码段属于临界区。梳理下来会发现很多玄学 bug 都是中断没管好。5.2 易漏点二栈对齐易漏点上下文切换或异常入口没有保证栈指针对齐。为什么漏某些架构要求栈指针 16 字节对齐不对齐时访问浮点或向量指令会触发异常但普通整数指令可能正常导致问题隐蔽。怎么补在保存上下文前检查栈指针对齐必要时调整。实验里如果遇到「明明逻辑对但就是跑不通」先查栈对齐。5.3 易漏点三页表刷新时机易漏点修改页表后没有立即刷新 TLB。为什么漏TLB 是硬件缓存刷新指令执行了但可能被乱序执行绕过或者刷新范围不对。怎么补修改页表后执行全量刷新或按地址刷新并确保刷新指令在修改之后执行。这个点在内存实验里出现频率极高。5.4 易漏点四系统调用参数校验易漏点直接使用用户态传来的指针没有校验地址合法性。为什么漏实验环境通常信任用户程序但考试或评分时可能用非法地址测试。怎么补在系统调用入口检查指针是否落在用户地址空间范围内越界直接返回错误码。这是从实验走向真实内核的必经一步。5.5 易漏点五进程退出时的资源回收易漏点进程退出时只释放了部分资源比如释放了内存但没关闭文件或者没回收 PCB。为什么漏单个进程退出时问题不明显反复创建退出后才暴露资源泄漏。怎么补在进程退出路径上逐项检查内存、文件描述符、PCB、内核栈确保全部回收。梳理下来会发现资源回收的代码通常分散在多处建议画一张回收清单对照检查。6. 复习清单从知识地图到动手验证的最后一公里复习操作系统实验最有效的方式不是反复看代码而是按知识地图逐层自测。下面这份清单按模块列出每一条都对应一个可以动手验证的动作。第一层引导与中断能说清楚启动流程从加电到进入内核的每一步能手工写出中断入口保存了哪些寄存器能解释时钟中断如何触发调度。第二层进程与调度能画出 PCB 的关键字段能手工推演一次上下文切换的寄存器变化能说出时间片轮转和优先级调度的区别以及各自的问题。第三层内存管理能手工翻译一个虚拟地址到物理地址能解释页表项每个标志位的作用能说出缺页异常处理的完整步骤和 TLB 刷新时机。第四层文件系统与系统调用能画出文件描述符到 inode 的层级关系能说出fork和dup对打开文件表项的影响能写出系统调用从陷入到返回的完整路径。模块自测动作通过标准引导与中断画出中断入口的寄存器保存顺序能解释每个寄存器的用途进程与调度手工推演一次上下文切换能说出 ra 和 sp 的变化内存管理手工翻译一个虚拟地址每级索引位数正确文件系统画出 fd → 表项 → inode 关系能解释 fork 后的共享行为系统调用写出分发逻辑的伪代码参数和返回值寄存器正确查漏补缺的具体技巧找一个没跑通的实验不要直接看答案而是在关键路径上加打印。比如上下文切换实验在保存和恢复前后各打印一次寄存器值对比是否一致缺页实验在缺页处理前后打印页表项看有效位和物理页号是否按预期变化。梳理下来会发现大部分 bug 不是逻辑写错而是某个步骤没执行或执行顺序不对。从那以后我每次做实验都强制走一遍「先画路径、再标关键点、最后加打印验证」的流程比盲目改代码快得多。希望这份清单能帮你在复习时少走弯路把时间花在真正需要动手验证的地方。本文还有配套的精品资源点击获取