
简介面向操作系统课程设计场景的 GeekOS 实验报告以 Project0、Project1、Project2 三个递进项目为主线覆盖环境搭建、设计原理、代码实现与运行验证适合计算机专业学生应对课程设计、实验报告撰写及考试复习。资源为单个 865KB 的 PDF 文件内容包含完整目录结构从实验目的、项目设计要求、开发环境建立到项目设计原理、具体实现、系统编译运行结果与问题总结均有清晰呈现。已有 410 人学习下载适用于需要参考报告框架、实验步骤和排错思路的学习者。报告不仅梳理了进程管理、内存管理、文件系统管理等操作系统核心知识点还结合项目具体实现细致介绍了进程创建与同步、内存分配与保护、文件系统接口与实现并收录了开发环境配置、常见问题及解决方法、课程设计总结等模块同时对操作系统的分类、架构与组件也有基础性说明。整体可作为深入理解 GeekOS 内核机制、规划实验实施路径以及提升技术报告完整度与规范性的实用范例。1. 从课程设计报告看 GeekOS 的真正难点GeekOS 是操作系统课程设计里少见的「真系统」它不是一个模拟器上的玩具而是能直接在 x86 模拟器里启动、有中断处理、有内核线程、有虚拟内存骨架的真实教学内核。很多同学对着这个项目写报告最终交上去的往往是一份「代码抄录 截图拼贴」原因是不知道课程设计要呈现的不是代码本身而是你如何把进程调度、内存管理和文件系统这几个抽象概念变成可运行的内核行为。这篇博文写给正在刷操作系统期末复习、或准备把 GeekOS 作为毕业设计底子的工程师我会从项目构建、核心模块实现、报告证据链三个层面讲清一份能经得起答辩追问的 GeekOS 课程设计报告应该怎么组织。标题里的「完美版」不是指排版而是指报告里的每一个结论都能被命令和日志现场复现。2. GeekOS 环境搭建与启动流程先把构建跑通再谈报告2.1 构建环境的选择Linux 虚拟机和基础工具链GeekOS 的代码量不大但对编译环境敏感。我一般会选择在 Ubuntu 20.04 或 22.04 的虚拟机上做分配 2 核和 2GB 内存就足够重点是要装齐bash、gcc、mtools、nasm、qemu-system-i386和gdb。其中mtools用于操作 FAT 格式的系统镜像这个包经常被漏掉导致make images阶段找不到mcopy。如果你在用联网环境可以这样安装sudo apt update sudo apt install -y build-essential mtools nasm qemu-system-i386 gdb这段命令里build-essential提供 gcc 和 makemtools负责把编译后的内核写入虚拟软盘镜像nasm是汇编器qemu-system-i386用于模拟 32 位 x86 机器gdb配合 QEMU 的调试接口做内核源码级调试。初次出错最多的不是 gcc 版本而是缺少mtools所以先把它装上。如果你拿到的 GeekOS 源码包带着makefile不要急着改代码先跑一次最干净的编译。常见做法是进入源码根目录依次执行清理和构建make clean make dep makemake clean删除旧的 .o 文件和镜像避免上次构建的中间产物干扰make dep生成头文件依赖关系GeekOS 的工程有相当多的全局头文件这一步不做会出现改了.h却不重编的怪问题make编译所有内核模块并链接出内核文件。构建成功后目录下会生成geekos/geekos.img之类的镜像文件那就是你要跑起来的目标。2.2 编译与启动的最小命令序列很多报告只写「编译通过」这不够。我会建议你在流程描述里加入具体的启动命令因为这份命令本身就是可复现性的证据。QEMU 启动 GeekOS 最常见的方式是qemu-system-i386 -kernel geekos/geekos -serial stdio -display none这里-kernel指定直接加载内核映像不走 BIOS 读软盘的路径调试时启动更快-serial stdio把串口输出重定向到当前终端这是 GeekOS 打印日志的主通道-display none关闭图形窗口因为大部分课程设计只需要串口文本输出就能验证行为。如果启动后看到Welcome to GeekOS或类似的内核自检信息说明环境已经通了。如果要用 GDB 调试需要先开 QEMU 的调试服务再另开终端连上去。启动命令要加-s -Sqemu-system-i386 -kernel geekos/geekos -serial stdio -s -S-s让 QEMU 在 TCP 1234 端口开放 GDB stub-S表示启动后暂停在 CPU 复位状态等待 GDB 接管。注意在 macOS 或新版 Linux 上端口可能被占用可以用-gdb tcp::1235改成其他端口。很多报告在这里会漏掉-S导致 GDB 连上去时内核已经跑过关键初始化没法观察第一条指令。2.3 启动异常时如何区分编译错、链接错和运行时错启动阶段遇到异常先看错误出现的时机。编译期错误通常会给出文件名和行号比如未声明的函数、结构体字段名写错链接期错误则以undefined reference开头说明某个符号没有实现或者实现文件没有加入makefile的源文件列表运行时错误表现为 QEMU 窗口里出现异常寄存器 dump或者串口输出中断在某一行。我在实际调试里最常见的运行时错误是*Exception*加上一段寄存器值这往往是中断处理函数没有正确保存现场。一个比较有用的做法是给内核的 panic 代码加上额外的串口打印输出当前EIP和栈顶值。你可以在错误处理函数里临时加入这样的代码void panic(const char *fmt, ...) { Console_PutStr(KERNEL PANIC: ); Print(fmt); DisableInterrupts(); StopProcessor(); }Panic打印内容里应当包含EIP寄存器的值这样配合 GDB 的x/10i $eip命令就能反汇编出出问题的那条指令。如果报告里能贴出 panic 地址和对应的 C 源码行比任何描述都有说服力。3. 核心模块实现进程调度、内存管理和文件系统的报告素材3.1 进程调度优先级队列的初始化与抢占点GeekOS 的线程调度器核心是一个优先级队列每个 CPU 维护多个双向链表队列下标对应优先级。报告里最重要的不是贴出Schedule函数的源码而是说明你改了什么、为什么改。典型的课程设计任务是给调度器增加周期唤醒或按时间片轮转。你可以这样描述你的线程控制块生命周期void MyScheduler_AddThread(Thread *thread) { if (thread-priority MAX_PRIORITY) { thread-priority MAX_PRIORITY - 1; } InsertThreadTail(readyQueueArray[thread-priority], thread); thread-state THREAD_STATE_READY; }这里MAX_PRIORITY是队列层数InsertThreadTail保证同优先级内的先来后到THREAD_STATE_READY用于后续的Schedule判断。报告要写清楚如果Priority是数值越大越优先还是越小越优先并给出一张映射表。这张表要放在报告正文不要只写在代码注释里。优先级值队列位置典型用途0队首空闲线程1中间普通用户线程2队尾中断处理底半部调度器的另一个重头是抢占点。GeekOS 默认在时钟中断触发后检查Preempt变量只有在进程从内核态返回用户态时才执行Schedule。如果你想实现用户态抢占就需要在TimerInterrupt里设置标志位并在系统调用返回前检查这个标志。我建议在报告里用两张图来表示「中断前栈布局」和「返回后恢复现场」比任何文字描述都直观。3.2 内存管理页目录与空闲页面的统计方法GeekOS 的内存管理项目通常要求实现物理页分配器或虚拟地址映射。报告的核心证据是分配器和释放器的工作次数。你可以在AllocatePage里加一个静态计数器static unsigned long alloc_count 0; void *AllocatePage(void) { void *page; if (free_page_count 0) { Print(AllocatePage: out of memory!\n); return 0; } page free_page_head; free_page_head *(void **)page; free_page_count--; alloc_count; Print(AF: %p count%lu total%lu\n, page, alloc_count, total_pages - free_page_count); return page; }AF是 alloc free 的缩写这种日志格式可以方便地用脚本统计分配地址范围。要注意free_page_head本身存放在空闲页面的开头 4 字节里这是一种「freelist 内嵌」的经典实现不需要额外数据结构。报告里需要解释为什么可以把空闲页的第一项当作 next 指针因为空闲页尚未被写入用户数据内核可以安全复用这 4 字节。内存项目里最容易翻车的点是页对齐。AllocatePage返回的地址必须是 4096 对齐否则写页目录时会出错。检查方法很简单if ((unsigned long)page 0xFFF) { Print(Misaligned page %p\n, page); return 0; }在报告中把这个检查写出来说明你意识到对齐约束并给出了防御性代码这就是加分项。如果你的报告里只贴了分配函数的 5 行代码而没有对边界条件的描述答辩时很容易被追问。3.3 文件系统以管道 pipe 或 vfork 为例的关联分析GeekOS 的文件系统项目没有统一标准有些是让实现一个简单的管道有些是实现fork有些是加入内存文件。我的建议是不要只按功能模块写而要抓住「文件描述符如何穿越进程边界」这条主线。管道就是一个最好的例子它让写进程和读进程共享一个File对象但各自的OpenFileDescription指向同一个管道缓冲区。机制写端表现读端表现GeekOS 中的关键函数FIFO 管道缓冲不足时睡眠缓冲空时睡眠Pipe_Read/Pipe_Write内存文件直接写内存页从内存页拷贝MemFile_Write设备文件触发设备写读取设备 DMA 缓冲Device_Read报告里可以写一个简单的测试程序调用你实现的pipe系统调用然后在父线程和子线程之间互相发送字符串最后打印收到的字节数。关键是要给出「期望值」和「实测值」的表。比如期望收到 100 字节实测收到 100 字节这是正确性测试再给出同时发送 1000 次时的丢包率这是压力测试。如果压力测试失败就把串口日志里缓冲满的段贴出来说明你定位到了是管道容量导致的阻塞而不是调度器饥饿。这样的报告读起来才像一篇工程报告而不是实验报告誊写。4. 报告结构与数据呈现把实验记录变成可复现的工程量4.1 课程设计报告的建议骨架很多课程设计报告被老师打回是因为结构像「代码仓库 README」缺少问题定义和验证逻辑。我建议按下面六段式组织每一段都要有输出物问题定义这门课设要解决什么比如「实现一个可抢占的线程调度器」。环境描述包括 Linux 发行版、gcc 版本、QEMU 版本以及 make 输出的最后 10 行。关键设计描述数据结构和算法用一个表格列出每个结构体字段的作用。实现细节只贴跟设计相关的代码片段而不是整文件。验证方案给出启动命令、测试输入、期望输出和实际输出。局限与改进列出已知问题比如「管道缓冲区固定 4KB在高负载下写线程会忙等」。这里最关键的是第 3 和第 5 段。很多报告第 3 段写得太少第 5 段贴了五张串口截图但没有说明截图里哪一行是关键输出。我一般会建议每张截图下方用引用块格式写两行字第一行是「我要证明什么」第二行是「截图里哪个字段代表结果」。例如提示下图证明调度器在时间片耗尽后切换到低优先级线程切换点处的线程 id 从 0x1001 变为 0x1002。这样老师不需要逐行看日志就能快速找到你设计的验证路径。4.2 用 GDB 脚本和串口日志采集运行证据写报告时最缺的是「证据」而不是代码。你可以把 GDB 命令写成一个脚本文件免得每次手工敲。下面这份脚本可以放在项目根目录的trace.gdb里target remote :1234 handle SIGTRAP nostop noprint break Kernel::Startup commands silent printf KernelStartup reached, eip%p\n, $eip continue end continuetarget remote :1234连接 QEMU 的调试 stubhandle SIGTRAP nostop noprint避免停在断点信号上break Kernel::Startup是断在内核初始化入口commands之后的printf和continue表示每次命中断点都打印 EIP 并继续运行。如果你不知道Kernel::Startup的确切符号可以在 GDB 里用info functions Startup确认。脚本跑完后把输出重定向到文件gdb -batch -x trace.gdb geekos/geekos trace.log 21-batch让 GDB 执行完脚本后自己退出 trace.log把日志保存下来。这份日志就成为了报告附录里的原始证据。串口日志也可以用类似方式采集启动 QEMU 时加-serial file:serial.log所有Console_PutStr输出会写进文件方便用 grep 过滤关键行。4.3 性能数据表格与图表生成命令调度器或内存分配器的性能数据最好用脚本生成 CSV再用可视化工具画图。不要手工从 QEMU 窗口抄数字。下面这段 bash 命令可以从串口日志里提取每次分配的地址和计数grep ^AF: serial.log | awk {print $3, $5} | sed s/count//; s/total// alloc.csvgrep ^AF:只保留我们自己打印的地址分配日志awk {print $3, $5}输出第三个字段地址和第五个字段sed去掉count和total前缀最终得到一个两列 CSV。再用 gnuplot 画一个地址分布散点图gnuplot -e set terminal png; set output alloc.png; plot alloc.csv using 1:2图中横轴是分配次数纵轴是分配计数如果出现平台期说明内存耗尽如果出现跳变说明有释放后重分配。在报告里插入这张图接着一段话说明趋势比贴 200 行日志更有效。记住所有图表必须有轴标签和单位否则老师会问「纵轴到底代表什么」。5. 提交前检查清单与答辩追问的四个切入点5.1 报告内的自查清单交付前用下面这张表自查每一项都对应可执行的动作。不要在文档里说「测试过」要写清楚用哪条命令测的、期望值是什么、实际结果是什么。检查项执行动作失败时处理构建可复现用make clean make dep make重跑一遍记录缺的依赖包启动无 panic执行qemu-system-i386 -kernel geekos/geekos -serial stdio用 GDB 回溯 EIP调度计数正确运行压力线程后 grepAF:数量检查线程栈是否溢出日志有时间戳确认Print输出带[CPU0]前缀修改Console_PutStr图表有轴说明打开 png 查看坐标轴文字重新生成 gnuplot 脚本提交前最后一步是检查 PDF 里的代码块是否被换行截断。很多「完美版」报告因为从 IDE 直接复制代码在 PDF 里出现竖排的字符错位。我一般会在终端里用sed把代码里的 tab 替换成 4 个空格再确认每行不超过 80 个字符避免排版翻车。5.2 答辩时高频追问与应对做法答辩时间通常只有 10 分钟与其准备一整套 PPT不如预演下面四个提问。第一个是「系统调用进入内核的完整路径」。你需要对着调用链讲清楚用户态调用Syscall库函数触发软中断或sysenter进入内核公共入口保存用户寄存器分发到具体处理函数。回答时能现场用 GDB 在SyscallHandler下断点再运行你的测试程序命中后打印栈回溯就是最硬的回答。第二个是「调度器如何保证互斥」。这个问题考察你是否意识到RunQueue是多线程共享数据。答案的核心是关中断或自旋锁。你可以说在Schedule函数入口执行DisableInterrupts退出前恢复因为 GeekOS 内核线程在单 CPU 上只要关中断就不会被抢占。第三个是「内存碎片怎么处理」。如果你实现的是空闲页链表诚实回答「当前没有做合并所以会产生外碎片」然后补充一句「如果加入 buddy 系统可以缓解」。千万不要假装自己实现了边界合并因为答辩老师会来回翻代码。第四个是「测试用例怎么证明不是碰巧通过」。最好的回答是展示你跑过两个极端测试一个是高频创建线程直到内存耗尽另一个是空载运行 10 分钟没有 panic。把两次测试的串口日志尾部贴上并用上面提到的trace.gdb脚本在答辩电脑上当场重跑一次。保存好你的 GDB 脚本和命令行历史现场重放就是比任何口头描述都有效的佐证。本文还有配套的精品资源点击获取