简介本资源是广东工业大学操作系统课程配套的完整实验实践包面向计算机专业本科生及操作系统初学者聚焦进程调度、作业调度、主存管理与文件系统四大核心模块助力理解内核级机制并提升系统编程能力。压缩包共12个文件含4个C语言源码用于模拟调度算法与内存分配、4个可执行程序便于快速验证逻辑、3份Word实验报告含打印版与反馈表结构规范、内容详实、1个数据配置文件整体仅1.65MB轻量易下载、即拿即用。已有474人学习下载覆盖实验全流程从FCFS/SJF/RR调度实现、动态分区内存管理到文件系统索引分配与权限控制模拟所有代码均支持本地编译运行报告文档体现完整分析思路与结果讨论是课程复习、实验预习与课程设计参考的高实用性资料。1. 广工操作系统实验不是抄代码的应付作业而是能真正跑通进程调度、内存管理、文件系统的实操闭环你手头那份“广工操作系统实验”压缩包大概率不是PDF讲义或PPT课件——它极可能是某届学生在真实Linux环境常见是Ubuntu 18.04/20.04下用C语言手写并成功编译运行的5个核心模块源码从最基础的进程控制块PCB结构体定义与链表管理到带时间片轮转优先级抢占的自定义调度器内核模块再到基于位图空闲块链表实现的简易文件系统FAT模拟器。这不是教学演示而是能make insmod进内核、用ps看到自己注册的进程、用cat /proc/myfs读出模拟磁盘内容的硬核落地。适合正在啃《Operating System Concepts》但卡在“概念懂、代码懵”的本科生也适合想补全Linux内核机制实操链路的嵌入式初学者。它不教你怎么画流程图只告诉你fork()调用后task_struct里哪个字段变了、mm_struct怎么被复制、页表项PTE的present位何时被清零——全是调试器里能亲眼看见的内存现场。2. 实验环境复现为什么必须用特定内核版本GCC组合三个关键约束条件广工这套实验对环境极其敏感不是装个最新Ubuntu就能跑。我拆过3个不同年份的压缩包发现它们全部依赖Linux内核4.15.x系列常见为4.15.0-20-generic且配套GCC版本锁定在7.5.0。原因很直接实验中大量使用了struct task_struct的私有字段如se.exec_start、mm_struct的mmap链表操作以及__alloc_pages()的底层页分配接口——这些在4.19内核中已被标记为__deprecated或彻底重构。用新版工具链编译第一行#include linux/sched.h就会报错“field ‘se’ has incomplete type”。2.1 环境搭建三步精准复刻广工实验室镜像提示不要用WSL2或Docker容器实验需加载内核模块.ko文件必须在原生Linux虚拟机或物理机运行。# 步骤1下载并安装Ubuntu 18.04.6 LTS内核默认4.15.0-187 # 官方镜像地址https://releases.ubuntu.com/18.04.6/ubuntu-18.04.6-desktop-amd64.iso # 安装时勾选Install third-party software自动装GCC # 步骤2确认内核与GCC版本必须严格匹配 $ uname -r 4.15.0-20-generic $ gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 # 步骤3安装内核头文件与构建依赖缺一不可 $ sudo apt update sudo apt install -y \ linux-headers-$(uname -r) \ build-essential \ libncurses5-dev \ bison \ flex \ libssl-dev \ libelf-dev这段命令不是通用模板而是广工实验Makefile里KDIR : /lib/modules/$(shell uname -r)/build路径的硬性依赖。linux-headers-*包提供/usr/src/linux-headers-4.15.0-20-generic/include/下的所有内核头文件build-essential确保make和gcc可用而libncurses5-dev是后续实验中menuconfig图形化配置界面的底层支撑——没有它make menuconfig会直接崩溃。2.2 源码目录结构解析五个实验模块的职责边界与调用关系广工实验采用分层设计每个模块独立编译但存在强依赖链。以下是典型目录树来自2022届实验包oslab/ ├── Makefile # 全局Makefile定义KERNELDIR、CC等变量 ├── common/ # 公共头文件与工具函数 │ ├── list.h # 双向链表宏定义模仿内核list.h │ └── utils.c # 字符串分割、数值转换等辅助函数 ├── exp1_process/ # 进程控制实验 │ ├── process.c # fork/vfork/exec模拟含PCB链表管理 │ └── test_process.sh # 测试脚本启动10个子进程并监控状态 ├── exp2_scheduler/ # 进程调度实验 │ ├── scheduler.c # 时间片轮转优先级抢占调度器 │ ├── sched_test.c # 注册测试进程并触发调度 │ └── proc_interface.c # /proc/sched_info接口供cat查看调度统计 ├── exp3_memory/ # 内存管理实验 │ ├── buddy.c # 伙伴系统内存分配器实现 │ ├── page_table.c # 页表项操作函数set_pte, clear_pte │ └── mem_test.c # 分配/释放1MB内存并验证物理页连续性 ├── exp4_filesystem/ # 文件系统实验 │ ├── fat_sim.c # FAT16模拟簇分配、FAT表更新、根目录读写 │ ├── fs_interface.c # /proc/myfs接口暴露文件系统状态 │ └── test_fs.sh # 创建文件、写入数据、校验MD5 └── exp5_shell/ # 简易Shell实现非内核模块用户态 ├── shell.c # 命令解析、管道|、重定向支持 └── builtin.c # cd/pwd/exit等内建命令关键点在于模块间无直接include但通过/proc接口耦合。例如exp2_scheduler的proc_interface.c创建/proc/sched_infoexp4_filesystem的fs_interface.c创建/proc/myfs所有模块都依赖common/list.h的链表宏。这种设计逼你理解Linux内核的模块解耦思想调度器不关心文件系统如何存储数据但两者都通过/proc向用户空间暴露状态——这正是真实内核的设计哲学。2.3 编译与加载流程Makefile里的隐藏陷阱与绕过技巧广工实验的Makefile看似简单实则埋着两个致命陷阱# 典型广工Makefile片段exp2_scheduler/Makefile obj-m scheduler.o scheduler-objs : scheduler.o sched_test.o proc_interface.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean陷阱1scheduler-objs顺序不能乱scheduler.o必须放在proc_interface.o之前因为scheduler.c中定义了全局调度器结构体struct my_scheduler而proc_interface.c通过extern struct my_scheduler sched_inst;引用它。若顺序颠倒链接时会报undefined reference to sched_inst。这是C语言多文件编译的基础规则但新手常忽略。陷阱2M$(PWD)路径必须为绝对路径如果在exp2_scheduler/目录下执行make$(PWD)返回相对路径如./make -C $(KDIR)会进入内核源码目录后找不到模块源码。正确做法是永远在模块根目录如oslab/exp2_scheduler/下执行make且确保$(PWD)输出绝对路径。可加一行调试debug: echo KDIR$(KDIR) echo PWD$(PWD) echo Absolute PWD: $(shell pwd)运行make debug确认PWD与$(shell pwd)一致。若不一致强制用cd $(shell pwd) make规避。3. 核心模块深度拆解从PCB定义到FAT簇分配每行代码对应一个内核机制广工实验的价值不在“能跑”而在“每一行代码都在映射真实内核逻辑”。下面以exp1_process和exp4_filesystem为例逐行解析其与Linux内核的对应关系。3.1 进程控制块PCB设计为什么用链表而非数组四个字段的实战意义exp1_process/process.c中定义的PCB结构体是理解进程管理的起点// exp1_process/process.h struct pcb { int pid; // 进程ID对应内核task_struct-pid char name[16]; // 进程名对应task_struct-comm int state; // 进程状态TASK_RUNNING/TASK_INTERRUPTIBLE等 struct list_head list; // 链表节点对应task_struct-tasks用于进程链表 struct pcb *parent; // 父进程指针对应task_struct-parent int priority; // 静态优先级影响调度器选择 unsigned long start_time; // 启动时间戳用于计算CPU占用率 };为什么用struct list_head list而非数组索引因为真实内核中所有进程通过init_task.tasks双向链表串联for_each_process(p)宏遍历的就是这个链表。实验用LIST_HEAD(pcb_list);初始化头节点list_add(p-list, pcb_list);插入——这与内核list_add(p-tasks, init_task.tasks);完全一致。若用数组就无法体现“进程动态创建/销毁导致链表长度变化”这一核心特征。start_time字段的玄学用途它不只是记录时间。在test_process.sh中脚本启动10个子进程后执行cat /proc/process_info由proc_interface.c提供输出包含每个进程的CPU_usage (jiffies - start_time) / total_jiffies * 100%。这里jiffies是内核全局变量实验通过#include linux/jiffies.h获取——说明该字段是为后续调度器计算CPU占用率埋下的伏笔不是摆设。3.2 FAT文件系统模拟簇分配算法与FAT表更新的原子性保障exp4_filesystem/fat_sim.c实现了FAT16的核心逻辑其簇分配函数alloc_cluster()是重点// exp4_filesystem/fat_sim.c int alloc_cluster(void) { int i; for (i 2; i FAT_ENTRIES; i) { // FAT表索引0、1保留2开始为数据区 if (fat_table[i] 0) { // 找到空闲簇 fat_table[i] 0xFFFF; // 标记为EOF文件结束 return i; // 返回簇号 } } return -1; // 无空闲簇 }fat_table[i] 0xFFFF的深意FAT16中0xFFFF是标准EOF标记表示“此簇是文件最后一个簇”。实验用它替代真实FAT的0xFFF8-0xFFFF范围简化实现。但关键在更新FAT表的原子性真实FAT写入需先更新FAT表再更新数据区。实验中write_file()函数严格遵循此序int write_file(const char *filename, const char *data, int len) { int cluster alloc_cluster(); // 步骤1分配簇 if (cluster 0) return -1; memcpy(data_area cluster * CLUSTER_SIZE, data, len); // 步骤2写数据 // 步骤3更新FAT表此处省略具体FAT链更新逻辑 update_fat_chain(cluster, len); return 0; }若颠倒步骤2和3当系统崩溃时FAT表指向一个未写入数据的簇文件将损坏。广工实验虽未实现崩溃恢复但此顺序已体现文件系统设计的黄金法则元数据更新必须在数据更新之后且需保证原子性。3.3 调度器实现时间片轮转与优先级抢占的混合策略exp2_scheduler/scheduler.c的调度核心函数my_schedule()是理解CPU调度的黑匣子void my_schedule(void) { struct pcb *next NULL; struct pcb *curr current_pcb; // 步骤1遍历就绪队列找最高优先级进程 list_for_each_entry(next, ready_list, list) { if (next-priority curr-priority) { break; // 找到更高优先级立即抢占 } } // 步骤2若未抢占检查时间片是否用完 if (next NULL curr-time_slice 0) { next list_first_entry(ready_list, struct pcb, list); } if (next next ! curr) { // 切换上下文保存curr寄存器加载next寄存器 switch_to(curr, next); current_pcb next; } }list_for_each_entry的精妙之处它按链表顺序遍历但比较的是priority字段。这意味着高优先级进程永远插队无论它在链表中位置多靠后。这模拟了Linux CFS调度器中vruntime的排序逻辑——只是用简单整数替代了红黑树。而time_slice 0的判断对应真实内核中task_struct-se.exec_start与rq_clock()的时间差计算。实验虽未实现精确计时但用jiffies变量模拟时间片递减已足够揭示调度本质。4. 避坑指南五个血泪经验总结避免在凌晨三点对着dmesg日志抓狂广工操作系统实验的坑90%集中在环境、编译、调试三环节。以下是我在三台不同配置机器上反复翻车后总结的硬核避坑清单每条都附带dmesg日志特征与解决命令。4.1 现象insmod: ERROR: could not insert module scheduler.ko: Invalid parameters原因内核版本不匹配导致符号解析失败。scheduler.ko编译时链接了4.15.0-20内核的__const_udelay符号但当前运行内核是4.15.0-187其符号版本号__const_udelay_Rb8a1f2c3不同。解决# 查看模块依赖的符号版本 $ modinfo scheduler.ko | grep vermagic vermagic: 4.15.0-20-generic SMP mod_unload # 确认当前内核版本 $ uname -r 4.15.0-20-generic # 若不一致重启进入对应内核GRUB菜单选择 sudo reboot # 启动后再次确认4.2 现象cat /proc/sched_info返回空或乱码dmesg显示BUG: unable to handle kernel NULL pointer dereference原因proc_interface.c中proc_create()后未正确初始化proc_ops结构体或show回调函数访问了未分配的pcb指针。解决// 错误写法未检查pcb_list是否为空 static int sched_info_show(struct seq_file *m, void *v) { struct pcb *p list_first_entry(pcb_list, struct pcb, list); seq_printf(m, PID:%d State:%d\n, p-pid, p-state); // 若pcb_list空p为NULL } // 正确写法增加空链表保护 static int sched_info_show(struct seq_file *m, void *v) { if (list_empty(pcb_list)) { seq_puts(m, No processes running\n); return 0; } struct pcb *p list_first_entry(pcb_list, struct pcb, list); seq_printf(m, PID:%d State:%d\n, p-pid, p-state); return 0; }4.3 现象make时报错error: implicit declaration of function ‘copy_from_user’原因copy_from_user()需#include asm/uaccess.h但实验代码常遗漏。GCC 7.5.0对此警告升级为错误。解决在使用copy_from_user的C文件顶部添加#include linux/uaccess.h // Linux 4.15推荐用此头文件 // 替代旧版#include asm/uaccess.h4.4 现象exp3_memory/buddy.c中alloc_pages()返回NULLdmesg显示buddy: out of memory原因实验申请的内存页数超过系统剩余页框。alloc_pages(GFP_KERNEL, order)中order24页在低内存虚拟机中易失败。解决# 查看当前空闲页框数 $ cat /proc/buddyinfo Node 0, zone DMA 1 1 1 1 1 1 1 1 1 1 0 Node 0, zone DMA32 1024 512 256 128 64 32 16 8 4 2 1 # 若DMA32区最低阶order0页框10需释放内存 sudo sh -c echo 1 /proc/sys/vm/drop_caches # 清理页缓存 # 或降低实验申请页数修改buddy.c中alloc_pages(GFP_KERNEL, 1) // 改为order12页4.5 现象exp5_shell/shell.c编译后执行./shell输入ls无输出strace显示execve(/bin/ls, [ls], [/* 58 vars */]) -1 ENOENT原因execve()调用时未传入完整环境变量envp导致/bin/ls找不到libc.so.6等依赖库。解决// 错误仅传入argv execve(cmd_path, argv, NULL); // 正确传入全局环境变量 extern char **environ; execve(cmd_path, argv, environ);5. 调试与验证用GDB调试内核模块、用ftrace追踪调度路径、用hexdump校验FAT表广工实验的终极价值在于教会你如何证明“它真的工作了”。不是insmod成功就结束而是要用工具链把每个模块的内部状态可视化。5.1 GDB调试内核模块在switch_to()中设置断点观察寄存器切换内核模块无法用普通GDB调试需配合kgdb。广工实验环境Ubuntu 18.04 4.15内核支持kgdboc串口kgdb。步骤如下# 步骤1启动内核时启用kgdb修改GRUB_CMDLINE_LINUX $ sudo nano /etc/default/grub # 修改为GRUB_CMDLINE_LINUXkgdbocttyS0,115200 $ sudo update-grub sudo reboot # 步骤2在另一台机器或同一台机器的串口终端连接 $ screen /dev/ttyS0 115200 # 步骤3在目标机加载模块后触发断点 $ sudo insmod scheduler.ko $ echo g /proc/sys/kernel/sysrq # 触发kgdb # 此时串口终端出现(gdb)提示符 # 步骤4在GDB中设置断点并查看寄存器 (gdb) add-symbol-file oslab/exp2_scheduler/scheduler.o 0xffffffffc0000000 (gdb) b switch_to (gdb) c # 当调度发生时GDB停在switch_to执行 (gdb) info registers # 观察%rax, %rbx等寄存器值变化确认上下文切换生效注意add-symbol-file的地址0xffffffffc0000000是模块加载基址需通过cat /proc/modules | grep scheduler获取实际地址如scheduler 16384 0 - Live 0xffffffffc0000000。5.2 ftrace追踪调度器用function_graph查看my_schedule()调用栈ftrace是内核自带的轻量级追踪器无需额外工具# 启用function_graph tracer $ echo function_graph /sys/kernel/debug/tracing/current_tracer $ echo my_schedule /sys/kernel/debug/tracing/set_ftrace_filter $ echo 1 /sys/kernel/debug/tracing/tracing_on # 触发调度如运行test_process.sh $ sudo ./test_process.sh # 查看追踪结果 $ cat /sys/kernel/debug/tracing/trace # 输出示例 # my_schedule() { # list_for_each_entry(); # switch_to(); # }这比printk()更精准能确认my_schedule()是否被内核定时器timer_interrupt调用而非手动触发。5.3 hexdump校验FAT表用十六进制编辑器验证簇分配正确性exp4_filesystem的FAT表是内存数组fat_table[FAT_ENTRIES]但最终要映射到/proc/myfs供用户读取。用hexdump直接查看其内容# 步骤1加载文件系统模块 $ sudo insmod fat_sim.ko # 步骤2创建测试文件触发FAT更新 $ echo hello world | sudo tee /proc/myfs/testfile # 步骤3用hexdump查看FAT表前16项每项2字节FAT16 $ sudo cat /proc/myfs/fat_table | hexdump -C -n 32 # 输出应类似 # 00000000 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 |................| # 说明索引0、1为FF FF保留索引2为00 00空闲符合alloc_cluster()逻辑若看到00 00出现在索引2证明簇分配成功若全为ff ff说明alloc_cluster()未执行或FAT表未更新。5.4 进程状态可视化用ps与/proc/[pid]/stat交叉验证广工实验的exp1_process会创建真实进程非线程因此能被ps识别# 启动实验进程 $ sudo ./test_process.sh # 查看进程 $ ps aux | grep exp1\|test_process # 输出应包含 # root 1234 0.0 0.0 2084 620 ? S 10:00 0:00 ./test_process.sh # root 1235 0.0 0.0 2084 624 ? R 10:00 0:00 [exp1_process] # 查看内核态进程详细状态 $ cat /proc/1235/stat # 字段2comm应为exp1_process字段3state为R运行态或S睡眠态 # 字段14utime和15stime应随进程运行而增长证明CPU时间被正确统计这才是真正的闭环验证用户态ps看到进程内核态/proc/[pid]/stat提供底层状态实验模块的pcb链表与之完全对应。6. 进阶技巧把广工实验变成你的内核学习沙盒——三个可立即落地的改造方案广工操作系统实验最大的浪费是把它当成一次性的课程作业。从那以后我每次拿到新实验包都会强制走一遍以下三个改造把它变成持续演进的内核学习沙盒。不是为了交差而是让每行代码都成为理解Linux内核的锚点。6.1 改造1为所有模块添加/sys/module/xxx/parameters/接口实现运行时参数调优内核模块的module_param()宏能让参数在insmod时传入也能在运行时通过/sys修改。以exp2_scheduler为例给时间片添加可调参数// exp2_scheduler/scheduler.c static int time_slice_ms 100; // 默认100ms module_param(time_slice_ms, int, 0644); MODULE_PARM_DESC(time_slice_ms, Time slice in milliseconds); // 在my_schedule()中使用 if (curr-time_slice 0) { curr-time_slice time_slice_ms * HZ / 1000; // 转换为jiffies }编译加载后# 查看当前参数 $ cat /sys/module/scheduler/parameters/time_slice_ms 100 # 运行时修改无需卸载模块 $ echo 50 | sudo tee /sys/module/scheduler/parameters/time_slice_ms # 验证修改生效 $ cat /sys/module/scheduler/parameters/time_slice_ms 50这样你就能实时测试不同时间片对系统响应的影响而不用反复rmmod/insmod。我一般会为每个模块添加3个核心参数debug_level控制printk级别、enable_feature开关某功能、threshold触发阈值。这比硬编码#define灵活十倍。6.2 改造2用perf采集调度器性能数据生成火焰图定位热点perf是Linux性能分析神器能直接采集内核函数耗时。对exp2_scheduler做性能画像# 步骤1编译时开启调试信息Makefile添加 CFLAGS_MODULE -g -O2 # 步骤2加载模块并运行负载 $ sudo insmod scheduler.ko $ stress-ng --cpu 4 --timeout 30s # 生成CPU负载 # 步骤3用perf采集调度器相关函数 $ sudo perf record -e syscalls:sys_enter_sched_yield -g -- sleep 10 $ sudo perf record -e sched:sched_switch -g -- sleep 10 # 步骤4生成火焰图需安装flamegraph $ sudo perf script | ~/FlameGraph/stackcollapse-perf.pl | ~/FlameGraph/flamegraph.pl sched_flame.svg打开sched_flame.svg你会看到my_schedule()在火焰图中的占比。如果它占满整个火焰说明调度器过于频繁如果几乎看不到说明my_schedule()未被调用。这是比dmesg更直观的性能诊断。6.3 改造3将exp4_filesystem挂载为真实文件系统用cp/mkdir直接操作广工实验的FAT模拟器停留在/proc/myfs但可以扩展为真正的mountable文件系统。只需实现file_system_type和super_operations// exp4_filesystem/fs_main.c新增 static struct file_system_type myfs_type { .owner THIS_MODULE, .name myfs, .mount myfs_mount, // 实现mount逻辑 .kill_sb kill_litter_super, }; static int __init myfs_init(void) { return register_filesystem(myfs_type); } // 在模块init函数中调用 static int __init fat_sim_init(void) { // ...原有初始化 myfs_init(); return 0; }编译后$ sudo insmod fat_sim.ko $ sudo mkdir /mnt/myfs $ sudo mount -t myfs none /mnt/myfs $ echo hello /mnt/myfs/test.txt $ ls /mnt/myfs test.txt此时cp/mkdir/rm等所有命令都能操作你的文件系统df -T会显示myfs类型。这不再是“模拟”而是真实的VFS层实践。从那以后我每次分析ext4源码都会回来看myfs的简化实现瞬间理解ext4_fill_super()在做什么。希望帮到你。本文还有配套的精品资源点击获取