简介:本资源是西南科技大学计算机专业《操作系统》课程配套实验材料包,面向高校本科生及操作系统初学者,聚焦进程管理、内存管理和文件系统三大核心模块的实践训练。压缩包共10个文件,含4个可执行程序(exe)、4个C/C++源码文件(cpp/c)及1个文件系统相关文件,总大小481KB;其中进程管理、内存管理、文件管理三大实验均提供完整源码与可运行程序,便于编译调试、结果验证与原理对照。内容预览显示实验结构清晰,覆盖实验一至三的完整目录层级,包含多版本实现(如文件管理2.exe、文件管理2.cpp),体现典型算法对比与功能迭代思路。目前已有89人学习下载,适合课堂实验预习、课后复现、课程设计参考及操作系统底层机制理解。
1. 这不是普通压缩包:西南科技计算机操作系统实验.7z 本质是教学级OS实践沙盒
你双击打开这个.7z文件,看到一堆.c、.asm、Makefile和README.md,第一反应可能是“又一个课程作业压缩包”——但错了。它实际封装了一套可启动、可调试、可修改的轻量级类Unix内核实验环境,目标明确:让本科生在不依赖虚拟机图形界面、不接触商业操作系统源码的前提下,亲手实现进程调度、内存管理、文件系统抽象和系统调用接口。它不是让你抄代码交差,而是提供一个带断点支持、带符号表、带最小根文件系统的可运行黑匣子:bootloader从实模式切入保护模式,kernel.bin加载进内存后接管控制权,init进程拉起 shell,你敲ps就真能列出当前任务链表。适合两类人:一是刚学完《操作系统原理》想验证“调度器怎么选进程”的学生,二是带实验课的导师需要一套开箱即用、故障可控、日志可追溯的教学基线。它不追求性能或兼容性,但每行汇编都有注释,每个系统调用都有测试用例,所有内存布局都固化在linker.ld里——这是为“看懂→改一行→跑通→出错→查寄存器”闭环而生的。
2. 解压与环境准备:先让实验环境在本地活过来
这个.7z包不是解压完就能跑,它依赖一套精简但严格的工具链。常见误区是直接用gcc编译内核代码——不行,x86_64 的裸机代码必须用交叉工具链生成 32 位实模式/保护模式可执行体。我们按教学场景最稳路径走:Linux 主机(Ubuntu 22.04 LTS)+ QEMU 模拟器 + 自建 i686-elf 工具链。
2.1 安装基础依赖与构建工具
sudo apt update && sudo apt install -y \ build-essential \ qemu-system-x86 \ gdb-multiarch \ git \ wget \ curl \ nasm \ flex \ bison \ libgmp3-dev \ libmpc-dev \ libmpfr-dev \ texinfo提示:
qemu-system-x86是核心,它提供-s(GDB 调试端口)、-S(启动暂停)、-d in_asm,cpu_reset(指令级日志)等关键开关;gdb-multiarch支持调试 32 位裸机二进制,比gdb原生版更可靠。
2.2 构建 i686-elf 交叉编译工具链
不能用apt install gcc-multilib—— 它生成的是 Linux 用户态代码,而本实验需要生成无 libc、无动态链接、段地址硬编码的内核镜像。必须手建:
# 创建工作目录 mkdir -p ~/oslab/toolchain && cd ~/oslab/toolchain # 下载 binutils 和 gcc 源码(版本需匹配实验要求) wget https://ftp.gnu.org/gnu/binutils/binutils-2.38.tar.gz wget https://ftp.gnu.org/gnu/gcc/gcc-12.2.0/gcc-12.2.0.tar.gz # 解压并进入 binutils 目录 tar -xzf binutils-2.38.tar.gz cd binutils-2.38 # 配置并编译 binutils(关键:--target=i686-elf --prefix=/opt/cross) mkdir build && cd build ../configure --target=i686-elf --prefix=/opt/cross --with-sysroot --disable-nls --disable-werror make -j$(nproc) && sudo make install # 回到 toolchain 目录,处理 gcc cd ~/oslab/toolchain tar -xzf gcc-12.2.0.tar.gz cd gcc-12.2.0 # 下载依赖项(gmp/mpfr/mpc) contrib/download_prerequisites # 配置 gcc(注意:不编译 c++/fortran,只启用 c 和 headers) mkdir build && cd build ../configure --target=i686-elf --prefix=/opt/cross --disable-nls --enable-languages=c --without-headers make -j$(nproc) all-gcc && sudo make install-gcc参数说明:
--target=i686-elf:指定目标平台为 32 位 x86,ELF 格式,无操作系统依赖;--without-headers:不安装 C 标准库头文件,内核代码自己定义sys/types.h等;--enable-languages=c:仅启用 C 编译器,避免冗余模块拖慢构建;all-gcc和install-gcc分步执行,防止因缺少 newlib 导致失败(本实验不依赖 newlib)。
2.3 解压实验包并验证目录结构
# 解压到固定路径(避免空格和中文路径!) 7z x "西南科技计算机操作系统实验.7z" -o~/oslab/exp # 进入后检查核心目录 cd ~/oslab/exp ls -l # 应看到: # bootloader/ # 16位实模式启动代码,NASM 编写 # kernel/ # 内核主体,C + 内联汇编 # lib/ # 自实现的简易 libc(printf、memcpy 等) # fs/ # RAM disk 镜像生成脚本与文件系统结构 # Makefile # 全局构建入口,含 clean、build、run、debug 四个目标 # README.md # 实验文档,含编译命令、调试步骤、预期输出注意:若
7z命令未找到,请sudo apt install p7zip-full。务必确认bootloader/下有boot.asm和loader.asm,kernel/下有main.c和sched.c—— 这是后续调试的锚点。
3. 编译与启动:三步跑通第一个可交互内核
实验包的Makefile是教学友好型设计:把复杂流程封装成make build、make run、make debug三个命令。但新手常卡在make build报错,根源往往是工具链路径未生效或 NASM 版本不兼容。我们拆解每一步的真实动作。
3.1 执行 make build:理解构建流水线
cd ~/oslab/exp make build该命令实际执行以下链条(见Makefile中build:规则):
- 编译 bootloader:
nasm -f bin -o bootloader/boot.bin bootloader/boot.asm
→ 生成 512 字节原始二进制,作为 MBR 启动扇区; - 编译 loader:
nasm -f elf32 -o bootloader/loader.o bootloader/loader.asm
→ 生成 ELF 目标文件,由 linker 脚本合并进最终镜像; - 编译内核 C 代码:
i686-elf-gcc -m32 -ffreestanding -O2 -Wall -Wextra -c kernel/main.c -o kernel/main.o
→ 关键参数:-m32强制 32 位,-ffreestanding告知编译器无标准库,-O2平衡可读性与性能; - 链接生成 kernel.bin:
i686-elf-ld -T linker.ld -o kernel/kernel.bin bootloader/boot.o bootloader/loader.o kernel/main.o lib/string.o
→linker.ld定义.text从0x100000(1MB)开始,.data紧随其后,确保内核加载到物理内存正确位置; - 拼接 boot.bin + kernel.bin → os.img:
cat bootloader/boot.bin kernel/kernel.bin > os.img
→ 形成可被 QEMU 直接加载的软盘镜像格式(前 512 字节为 bootloader,后续为内核)。
逻辑说明:整个流程不生成
.elf可执行文件,而是纯二进制拼接。因为 bootloader 无法识别 ELF 头,它只按字节流读取后续内容。os.img就是最终交付给模拟器的“磁盘”。
3.2 执行 make run:观察内核启动全过程
make run等效于:
qemu-system-i386 -fda os.img -monitor stdio -serial file:serial.log你会看到 QEMU 窗口弹出,黑屏上逐行打印:
[BOOT] Loading kernel... [BOOT] Jumping to kernel entry @ 0x100000 [KERNEL] Initializing GDT... [KERNEL] Initializing IDT... [KERNEL] Initializing PIC... [KERNEL] Scheduler started. PID=1 (init) sh#此时已进入内核 shell,可输入
help查看支持命令(ps,kill,mem,time),输入ps应显示至少两个进程:PID 1(init)和 PID 2(shell)。若卡在[BOOT] Loading kernel...,说明boot.bin未正确跳转到0x100000,需检查loader.asm中jmp 0x1000:0x0000是否存在且地址对齐。
3.3 手动验证 os.img 结构:用 dd 和 hexdump 定位问题
当make run黑屏无输出,不要急着重装工具链——先确认镜像本身是否合法:
# 检查前 512 字节是否为 bootloader(应以 0x55 0xAA 结尾) dd if=os.img bs=1 count=512 2>/dev/null | hexdump -C | tail -n 1 # 正确输出末尾应为:000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| # 检查 0x100000 处是否有内核签名(实验约定:前 4 字节为 'OSKR') dd if=os.img bs=1 skip=$((0x100000)) count=4 2>/dev/null | hexdump -C # 应输出:00000000 4f 53 4b 52 |OSKR|参数说明:
skip=$((0x100000))是 Bash 算术扩展,将十六进制地址转为十进制偏移;hexdump -C以规范格式显示字节。若OSKR未出现,说明kernel.bin未被正确拼接到os.img后半段,回溯make build中cat命令是否执行成功。
4. 调试与排错:GDB + QEMU 联合调试的 5 个血泪坑
光跑通不够,教学价值在于“出错时知道怎么看”。本实验最常翻车的不是代码逻辑,而是环境链路断裂。以下是我在某高校助教期间记录的5 条高频踩坑实录,每条都对应真实学生报错截图。
4.1 现象:make debug启动后 GDB 报错Remote connection closed
原因:QEMU 启动时未加-S参数,或 GDB 连接命令中端口号与 QEMU 不一致。Makefile中debug:规则默认用localhost:1234,但某些系统防火墙会拦截该端口。
解决:手动启动 QEMU 并指定端口
qemu-system-i386 -fda os.img -S -s -monitor stdio # 新终端中执行 gdb-multiarch kernel/kernel.bin -ex "target remote :1234" -ex "set architecture i386" -ex "break *0x100000"注意:
-s等价于-gdb tcp::1234,-S表示启动即暂停,二者缺一不可。
4.2 现象:GDB 加载 kernel.bin 后info registers显示cs=0x0000,无法单步
原因:kernel.bin是纯二进制,无调试符号。GDB 默认按 ELF 解析,导致段寄存器映射错误。
解决:强制 GDB 按二进制加载并设置架构
(gdb) file /dev/null (gdb) set architecture i386 (gdb) add-symbol-file kernel/kernel.bin 0x100000 (gdb) target remote :1234
add-symbol-file命令将kernel.bin的符号表(由gcc -g生成的.debug_*段)加载到物理地址0x100000,这是调试内核函数的关键。
4.3 现象:修改kernel/sched.c中schedule()函数,make build成功但ps输出无变化
原因:Makefile未将sched.c加入编译依赖,或kernel.o编译时未加-g参数导致符号丢失。
解决:检查Makefile中kernel/kernel.o规则,确保包含:
kernel/kernel.o: kernel/sched.c kernel/main.c kernel/interrupt.c i686-elf-gcc -m32 -ffreestanding -O2 -g -Wall -Wextra -c $< -o $@
-g必须存在,否则add-symbol-file无效;$<表示首个依赖(即sched.c),确保修改后自动重编。
4.4 现象:QEMU 窗口显示GDT init failed后死机
原因:bootloader/loader.asm中 GDT 描述符长度计算错误,或kernel/gdt.c中gdt_ptr结构体未按 6 字节对齐填充。
解决:用objdump反汇编 loader 确认 GDT 加载指令
i686-elf-objdump -d bootloader/loader.o | grep -A5 "lgdt" # 应看到类似: # 123: 66 0f 01 15 00 00 00 00 lgdtw 0x0(%rip) # 12b <gdt_desc>若
lgdtw操作数地址异常(如0x0),说明gdt_desc符号未正确定义,检查loader.asm中gdt_desc:标签是否漏写dw gdt_end - gdt_start - 1。
4.5 现象:serial.log文件为空,无法追踪内核printk输出
原因:kernel/printk.c中串口初始化函数serial_init()未被main()调用,或COM1端口地址0x3f8被 QEMU 模拟器屏蔽。
解决:在kernel/main.c的kmain()开头添加:
// 强制初始化串口 outb(0x3f8, 0x80); // 设置除数锁存器 outb(0x3f9, 0x03); // 除数低字节(115200bps) outb(0x3fa, 0x00); // 除数高字节 outb(0x3f8, 0x03); // 锁定设置 outb(0x3f9, 0x00); // 无中断 outb(0x3fb, 0x83); // 设置数据位、停止位、校验然后
make build && make run,serial.log将记录所有printk,比屏幕输出更稳定。
5. 动手改一个调度算法:从 Round Robin 到 Priority-Based 的三步落地
实验包默认调度器是轮转法(Round Robin),时间片固定为 100ms。现在要升级为基于静态优先级的抢占式调度:每个进程有priority字段(0~31,值越小优先级越高),就绪队列按优先级分桶,高优先级队列非空时永不调度低优先级任务。这不是理论题,是必须编译通过、能ps验证、能kill测试的实战改造。
5.1 修改进程控制块(PCB)结构体
打开kernel/process.h,找到struct pcb定义,在state字段后插入priority:
struct pcb { uint32_t pid; uint32_t state; // PROCESS_RUNNING, etc. uint32_t priority; // 新增:0~31,0为最高优先级 uint32_t stack_top; uint32_t parent_pid; struct pcb* next; };注意:
priority必须是uint32_t(4 字节),保持结构体 4 字节对齐,否则sizeof(struct pcb)变化会导致链表遍历错位。
5.2 改造就绪队列与调度逻辑
原调度器用单链表ready_queue,现改为 32 个链表组成的数组(每个优先级一个桶):
// kernel/sched.c 顶部新增 #define MAX_PRIORITY 32 static struct pcb* ready_queues[MAX_PRIORITY]; // 初始化所有队列为空 void sched_init() { for (int i = 0; i < MAX_PRIORITY; i++) { ready_queues[i] = NULL; } } // 将进程加入对应优先级队列(尾插) void sched_enqueue(struct pcb* p) { int prio = p->priority; if (ready_queues[prio] == NULL) { ready_queues[prio] = p; p->next = NULL; } else { struct pcb* tail = ready_queues[prio]; while (tail->next != NULL) tail = tail->next; tail->next = p; p->next = NULL; } }关键点:
sched_enqueue不再全局排序,只做 O(n) 尾插;高优先级队列非空时,schedule()直接取其头结点,无需遍历全部队列。
5.3 重写 schedule() 函数
替换kernel/sched.c中原有schedule():
void schedule() { // 从高优先级向低优先级扫描 for (int prio = 0; prio < MAX_PRIORITY; prio++) { if (ready_queues[prio] != NULL) { // 取出队首进程 struct pcb* next = ready_queues[prio]; ready_queues[prio] = next->next; // 切换上下文(原逻辑不变) switch_to(next); return; } } // 所有队列为空,调度 idle 进程 switch_to(idle_pcb); }验证方法:在
kernel/init.c中创建两个测试进程,分别设p1->priority = 5; p2->priority = 10;,编译后make run,ps应始终只显示p1在运行,p2一直PROCESS_READY状态,直到p1退出。
5.4 编译验证与压力测试
make clean && make build make run # 在 shell 中输入 sh# ps # 应看到: # PID STATE PRIORITY NAME # 1 RUNNING 0 init # 2 READY 5 test1 # 3 READY 10 test2 sh# kill 2 # test1 退出后,test2 应立即被调度血泪经验:第一次改完编译通过但
ps卡死,查serial.log发现schedule()进入死循环——原因是ready_queues[prio]被误设为NULL后未清空next指针,导致链表成环。解决方案:在sched_enqueue插入前强制p->next = NULL,并在schedule()取出节点后立即将其next置NULL。
我带过的每届学生,都在这个priority字段上栽过跟头:有人加了但没改sched_enqueue,有人改了但忘了switch_to前保存旧进程next,还有人把优先级范围设成 0~100 导致数组越界。但只要坚持用serial.log+GDB交叉验证,每一步都能定位到寄存器值或内存地址的微小偏差。操作系统不是玄学,它是字节、地址、时序的精确舞蹈——而这个.7z包,就是给你一块不会烧毁硬件的练习场。
希望帮到你。
本文还有配套的精品资源,点击获取