ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

操作系统课程设计实战:从信号量到LRU模拟器

操作系统课程设计实战:从信号量到LRU模拟器 简介这是一份面向高等院校计算机专业学生的操作系统课程设计指导文档由课程组编写适合正在准备操作系统实践环节、需要完成选题与报告的学生使用。文档系统梳理了课程目标、对应毕业要求指标点、可选题目、设计任务与考核方式等内容涵盖进程管理、内存管理、文件系统、设备驱动等核心模块并给出新增系统调用、修改文件系统、添加驱动、统计缺页次数等具体题目方向同时涉及虚拟机安装、Linux内核重编译、基于模块编译的实操要求以及报告撰写规范与答辩考核权重。资源包内共1个PDF文件大小约255KB内容结构清晰既可作为课程设计全过程的重要参考也可帮助读者快速理解任务边界与评分标准。目前已有107人浏览学习对于希望系统梳理操作系统课程设计流程、合理规划实践任务的同学具有一定参考价值。1. 操作系统课程设计造轮子的同时先想清楚三个边界操作系统课程设计是计算机类专业的“硬骨头”之一。大多数人的第一反应是“我要写出一个能引导启动的 mini 内核”但按经验能在两周内做到这一点的学生极少更多时间花在了进程调度模拟器、内存管理模拟器、并发同步问题上。课程设计真正考查的是三件事模型抽象、资源冲突处理、以及把理论算法翻译成可运行代码的能力。换句话说你不是在写一个操作系统而是在操作系统边界内做一次“局部还原”。这门课适合两类人一类是考研或期末复习阶段希望把《操作系统概念》或王道笔记里的知识变成动手经验的人另一类是工作回归基础想补上并发与内存管理短板的工程师。标题里的“课程设计”决定了它有明确交付约束——要有可运行程序、要有测试数据、要有验收答辩。因此本文的路线是先确定选题边界再搭开发环境接着用信号量和 LRU 模拟器覆盖并发与内存两大考点最后落在答辩防御点和报告写法上。2. 操作系统课程设计的选题规划四类经典题与最小开发环境2.1 最常被选的四类课程设计题目2.1.1 进程调度模拟器常见做法是模拟 FCFS、SJF、时间片轮转、优先级调度输入一组进程的到达时间和服务时间输出每个算法的完成时间、带权停留时间、平均等待时间。这个题目的落点不是“画流程图”而是如何用同一个结构体抽象进程以及如何统计每个时间点的就绪队列状态。2.1.2 内存管理模拟器实现分区分配、页式存储、请求调页中的至少一种页面置换算法通常要求写 OPT、FIFO、LRU、Clock 并对比缺页率。这个题最能体现“数据结构的合理性”因为置换算法本质上就是在一个固定大小的帧数组中处理访问序列。2.1.3 文件系统与磁盘调度模拟 inode 节点分配或 SCAN、C-SCAN 磁盘寻道算法。相对独立适合偏向底层存储的同学。需要注意输入输出格式要稳定否则答辩时很难复现实验数据。2.1.4 并发与同步问题生产者与消费者、读者-写者、哲学家进餐要求用信号量或互斥锁实现并且不能死锁。这四类题中它理论最深但程序量最小适合想深入理解“管程”和“协程”背后同步语义的人。选哪类建议结对讨论后再定核心是看自己的算法基础和时间预算。如果只有一周选并发题或磁盘调度如果有三周再考虑内存管理加一个调度器组合做出对比图表。2.2 开发环境的最小起点Ubuntu、GCC 和 Makefile无论选哪类题目开发环境我一般统一建议Ubuntu 22.04 或 20.04 系统加 GCC不要为了“真实感”强行用模拟器引导自制内核。课程设计评分的核心是可运行和可解释不是引导过程。先建立项目骨架mkdir os_course cd os_course touch main.c scheduler.c memory.c sync.c MakefileCC gcc CFLAGS -Wall -Wextra -g -pthread TARGET os_course OBJS main.o scheduler.o memory.o sync.o all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ clean: rm -f $(OBJS) $(TARGET)说明-Wall -Wextra是打开所有常见编译警告-g表示生成调试符号给 GDB 用-pthread用于链接 POSIX 线程库是并发题必需参数。$表示目标文件$^表示所有依赖项这是 Makefile 里的自动变量不要省略。完成这一步后make ./os_course应该能跑出一个空程序。之后每增加一个功能模块建议边写变编译避免课程设计结束时才发现sem_init参数传错。3. 并发同步核心用信号量和互斥锁实现生产者-消费者模型3.1 为什么课程设计里用 POSIX 信号量而不是自旋锁进程调度实验通常不涉及实质并发但并发题必须真正创建线程。用 C 语言写时POSIX 线程库提供的pthread_mutex_t和sem_t是最直观的工具。为什么不推荐直接用自旋锁课程设计答辩时很少有人能解释清楚自旋锁在单核 CPU 上的 spin 过程容易追问后面露怯。互斥锁在无法获得锁时会让线程睡眠语义更符合经典同步问题的假设。在动手前需要明确信号量的三个字段初值、等待操作、释放操作。初值用来表示资源数量也是管程和协程设计中“资源状态”的一种体现。很多实现挂掉不是因为锁用错而是因为把sem_wait和sem_post的顺序写反或者漏掉sem_destroy。3.2 可运行的完整代码五个线程、两个信号量、一个互斥锁下面是一份可以直接编译运行的生产者-消费者代码缓冲区大小为 2生产者 3 个消费者 2 个循环轮次为 5#include stdio.h #include pthread.h #include semaphore.h #define BUFFER_SIZE 2 #define PRODUCERS 3 #define CONSUMERS 2 #define ITERS 5 sem_t empty_slots, full_slots; pthread_mutex_t mutex; int buffer[BUFFER_SIZE]; int in_pos 0, out_pos 0; int stop 0; void *producer(void *arg) { int id *(int *)arg; for (int i 0; i ITERS; i) { sem_wait(empty_slots); pthread_mutex_lock(mutex); buffer[in_pos] id * 100 i; printf(生产: 第%d个生产者写入 %d 到槽位 %d\n, id, buffer[in_pos], in_pos); in_pos (in_pos 1) % BUFFER_SIZE; pthread_mutex_unlock(mutex); sem_post(full_slots); } return NULL; } void *consumer(void *arg) { int id *(int *)arg; while (1) { sem_wait(full_slots); pthread_mutex_lock(mutex); if (stop) { pthread_mutex_unlock(mutex); sem_post(full_slots); break; } int val buffer[out_pos]; printf( 消费: 第%d个消费者取出 %d 从槽位 %d\n, id, val, out_pos); out_pos (out_pos 1) % BUFFER_SIZE; pthread_mutex_unlock(mutex); sem_post(empty_slots); } return NULL; } int main() { pthread_t p[PRODUCERS], c[CONSUMERS]; int ids[PRODUCERS CONSUMERS ? PRODUCERS : CONSUMERS]; sem_init(empty_slots, 0, BUFFER_SIZE); sem_init(full_slots, 0, 0); pthread_mutex_init(mutex, NULL); for (int i 0; i PRODUCERS; i) { ids[i] i; pthread_create(p[i], NULL, producer, ids[i]); } for (int i 0; i CONSUMERS; i) { ids[i PRODUCERS] i; pthread_create(c[i], NULL, consumer, ids[i PRODUCERS]); } for (int i 0; i PRODUCERS; i) pthread_join(p[i], NULL); pthread_mutex_lock(mutex); stop 1; pthread_mutex_unlock(mutex); for (int i 0; i CONSUMERS; i) pthread_join(c[i], NULL); sem_destroy(empty_slots); sem_destroy(full_slots); pthread_mutex_destroy(mutex); return 0; }代码逻辑说明empty_slots初始值为缓冲区大小每生产一条就占用一个空位full_slots初始值为 0每消费一条就归还一个空位。互斥锁只保护缓冲区的读写操作本身信号量负责“何时可以做”的条件判断。主线程在最后一个生产者结束后设置stop标志并唤醒消费者退出避免消费者永远阻塞在sem_wait。参数说明中容易被忽略的是sem_init的第二个参数pshared这里传 0 表示线程间共享而不是进程间共享。如果传非 0 值信号量会放到共享内存中课程设计里一般不需要这个能力。另外sem_wait必须放在pthread_mutex_lock之前否则锁持有期间线程会阻塞导致其他线程无法进入临界区出现假死锁现象。3.3 从信号量到管程再到协程的设计递进这道题完成后可以顺手把它扩展成两种形态答辩时会显得理解到位。第一种是把同步逻辑封装成管程结构用结构体包裹缓冲区、互斥锁和两个条件变量只暴露put和get方法。第二种是用户态协程版本用ucontext或汇编切换保存上下文不依赖内核调度。这两种扩展都不需要重写业务逻辑本质上是把“什么时候等待、什么时候唤醒”从调用方迁入同步原语内部。这部分是对标考研和期末复习热点的核心考点。大多数教材讲“管程和协程”时只给伪代码课设里能说出“管程是语言级封装信号量是内核级原语协程是用户态上下文切换”这三层递进就能在同组人里拉开差距。4. 内存管理核心从缺页率到 LRU 与 Clock 的完整模拟器4.1 页面置换模拟器的评价指标与输入设计内存管理模拟器的输出不是“成功替换页面”这句话而是缺页次数和缺页率。缺页率定义是缺页次数除以内存访问总次数。这个指标要在不同帧数量下做对比所以程序需要一个参数接口页面数量、进程访问序列长度、物理块数。常见做法是让用户输入帧数或者用命令行参数传入生成一组可复现的测试数据。测试数据不能完全随机否则局部性原理体现不出来。我一般按照“70% 的访问落在最近访问过的 5 个页面内30% 均匀散布在 20 个页面中”的规则生成这样能观察到 LRU 相对于 FIFO 的明显优势。4.2 Clock 算法的 C 语言实现与参数边界LRU 理论上最优但课程设计手写双向链表加哈希表成本偏高。实际工程和操作系统中更常用的是 Clock 算法它不精确维护访问时间而是通过一个引用位近似 LRU很适合做教学展示。下面代码可以直接编译测试#include stdio.h #include stdlib.h void clock_algorithm(int page_stream[], int n, int frame_num) { int *frame calloc(frame_num, sizeof(int)); int *rbit calloc(frame_num, sizeof(int)); int ptr 0, faults 0, used 0; for (int i 0; i n; i) { int page page_stream[i]; int hit 0; for (int j 0; j used; j) { if (frame[j] page) { rbit[j] 1; // 命中时置引用位为1 hit 1; break; } } if (hit) continue; faults; if (used frame_num) { frame[used] page; rbit[used] 1; used; } else { while (rbit[ptr] 1) { rbit[ptr] 0; // 清引用位给页面第二次机会 ptr (ptr 1) % frame_num; } frame[ptr] page; rbit[ptr] 1; ptr (ptr 1) % frame_num; } } printf(帧数: %d 缺页次数: %d 缺页率: %.2f%%\n, frame_num, faults, faults * 100.0 / n); free(frame); free(rbit); } int main() { srand(42); int seq[100]; for (int i 0; i 100; i) { if (rand() % 10 7) seq[i] rand() % 5; // 近 5 个页面内 else seq[i] rand() % 20; // 全范围 } for (int f 3; f 8; f) { clock_algorithm(seq, 100, f); } return 0; }这段代码里的ptr是时钟指针它只在替换发生时移动。rbit数组维护每个帧的引用位页面被访问后置 1被扫描时清 0。替换规则是指针遍历帧数组遇到引用位为 0 的帧就替换遇到 1 就清成 0 继续找。这个行为等价于给每个页面一次“维护期”比 FIFO 更能保留热点页。参数需要留意两个细节。frame_num小于等于访问序列中的页面种类数时才有比较意义否则所有算法缺页率都是 0。随机种子建议固定我用srand(42)是为了让答辩时的结果可复现如果你用时间种子每次运行结果都不一样测试报告很难解释。生成访问序列时可以写上“按局部性假设生成”而不是“随机生成”这两句话在答辩时的可信度完全不同。4.3 为什么操作系统中实际用的是近似 LRU 而不是精确 LRU精确 LRU 需要记录最后一次访问时间当帧数较大时排序和查找成本会吃掉收益。Clock 的代价只有一位引用位开销远低于链表操作这是它被 Linux 内核沿用的原因。如果课设时间充裕可以再用一个 FIFO 算法做对照实验比较同一序列在相同帧数下的缺页率差异。这时候报告可以给出“当局部性越强LRU 相对 FIFO 优势越明显”的结论这就从代码实现上升到了实验分析。5. 操作系统课程设计答辩的验收要点与防御性设计5.1 答辩前的自测清单操作系统课程设计最常见丢分点不在代码跑不通而在“只做了实现没有解释边界”。建议在提交前逐项过一遍下面的自测表考察点需要能回答的问题同步语义信号量初值为什么是缓冲区大小先 wait 还是先 lock死锁条件这组并发场景是否满足互斥、持有并等待、不可剥夺、循环等待调度指标平均等待时间和平均带权周转时间分别取决于什么参数页面置换Clock 算法什么时候出现 Belady 异常FIFO 有LRU 理论上没有数据复现测试输入是固定种子还是时间种子结果是否有截图为证代码构建make clean make后能否一次通过是否依赖绝对路径这些问题大部分都在《计算机操作系统》教材和期末复习资料里但课程设计答辩不会直接考定义而是让你对着自己的输出数据解释异常现象。比如时间片轮转的平均等待时间比 SJF 高这不是程序写错了而是轮转调度本质上更注重响应时间再比如 FIFO 在某些局部性强的访问序列下缺页率反而低于 LRU这属于局部性模型与算法特性不匹配需要当场讲出假设条件。5.2 报告的写作顺序与验证结果的组织方式报告按“设计目标、模块划分、数据结构、核心流程、实验结果、总结”六个部分写即可。重点放在实验结果至少三组输入参数每组至少两个对比算法用表格列出缺页率或平均等待时间再用一两句话解释差异原因。答辩时可以直接把终端输出截图贴进报告标注运行环境和编译命令。这种写法通过“多组对比 边界条件说明”拼出完整实验认知让评分人看到能设计实验、能分析差异的能力。课程设计不是竞技场你的目标是让每个提出的参数都有人能用一条命令复现——把操作步骤写到 README 里而不是口头描述“我当时这么跑的”。这样即便代码有小瑕疵也让系统在验收环节保持解释权。本文还有配套的精品资源点击获取
返回列表