ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

操作系统课设:用C语言实现时间片轮转调度完整指南

操作系统课设:用C语言实现时间片轮转调度完整指南 简介湖南科技大学操作系统课程设计完整资料包面向高校操作系统课程学生围绕进程管理、内存管理、文件系统、设备管理及并发线程等课程重点提供可运行的编程实践与参考实现。资源共26个文件、压缩包体积4.97MB以12个C源程序、12个配套exe可执行程序为主体另含1份设计报告docx和1个验证性实验源程序c覆盖磁盘调度、内存管理、页面置换、银行家算法、生产者消费者、读者写者等经典题目。目前已有856人学习下载。源代码与可执行文件可直接对照运行便于验证算法输出和调试细节报告文档包含设计思路、实现过程与问题解决方案适合用来完善课设报告、梳理实验框架或排查同类问题。1. 拿到湖南科技大学操作系统课设任务书的那一刻你该先做哪四件事拿到湖南科技大学操作系统课设任务书的那一刻很多人不是卡在“不会写代码”而是卡在“不知道从哪一行开始”。课设通常要求用两三周实现一个核心模块最常见的是进程调度模拟器输入进程的到达时间和服务时间输出调度顺序和平均周转时间。网上能抄的代码很多但答辩时被问“你的时间片为什么设成2个tick”就露馅了。这篇文章把整个课设拆成选型、实现、编译、排错和收尾五段目标是让你交上去的东西既能跑又能经得住追问。适合正在赶课设、想把原理和代码之间的短板补齐的人。2. 拆解课设需求先想清楚这四件事再决定写模拟器还是改内核写代码之前先把任务书读三遍。操作系统课设的坑不在算法而在需求没拆明白就开始动手。湖南科技大学的课设任务一般会写清楚方向但“模拟进程调度”和“实现进程调度器”听起来差不多实际工作量和验收标准完全不一样。这一章先把需求拆成几个判断题答案定下来再动手。2.1 课设的三种常见形态模拟器、给现有内核加功能、纯文件系统根据往届课设的常见做法操作系统课设基本分三类。第一类是调度模拟器用C、C或Java写成命令行程序输入进程列表输出调度甘特图和平均等待时间。第二类是给一个教学内核比如xv6或ucore添加系统调用或者修改内存分配策略这类需要读真实内核代码环境配置和调试成本都高。第三类是文件系统模拟在内存里做一块虚拟磁盘实现open、read、write、close几个接口。如果任务书没说必须做哪一类我一般建议选第一类。原因是边界清晰输入输出都是文本出了问题能打印中间状态。第二类看着有“含金量”但光把xv6编译过一遍、配置好调试环境就可能花掉一星期。第三类工作量看起来小但目录、索引节点、空闲块管理三个模块叠加整体复杂度不亚于调度器。2.2 选调度算法前先问任务书里有没有“实时性”这三个字调度算法的选择不是哪个难选哪个而是看任务书的验收点。如果任务书只说“模拟进程调度”最稳妥的是时间片轮转。逻辑简单参数好解释往报告里画张甘特图就完事。如果任务书提到“短作业优先”或“高响应比”那就是带优先级的调度需要动态计算每个时刻的响应比。如果出现“实时性”“截止时间”要用最早截止时间优先实现时要额外处理超时判断。对应关系是批处理系统看平均周转时间交互式系统看重响应时间实时系统看是否满足截止时间。你真正要在报告里写的不是“我实现了时间片轮转”而是“为什么选它、参数怎么支撑这个目标”。比如时间片轮转的时间片设大了响应时间变差设小了切换开销变大。这个权衡本身就是答辩时的加分点。2.3 输入输出格式定不下来后边所有脚本都没法写课设程序最容易被忽略的是输入输出格式。常见做法是读文本文件每一行代表一个进程pid、到达时间、服务时间。输出分两部分一是每个时刻谁在运行二是最终统计结果。我建议把输出样例直接写在注释里这样写代码时不会跑偏。输入文件proc.txt1 0 3 2 1 2 3 2 1期望输出tick 0: P1 running tick 3: P1 finished tick 3: P2 running ... 平均周转时间: X这个格式一旦定下来后面的Makefile、测试脚本、报告图表都能围绕它做。不要在写代码时才临时定格式那会导致后边的对比脚本、参数扫描全部重写。2.4 时间单位统一tick、毫秒还是秒直接影响参数调试很多课设代码里时间单位混乱有人用“秒”有人用真实延时函数。我建议统一用tick一个tick代表一次调度循环的基本粒度。比如时间片设为2 tick那每个进程每次最多连续运行2个tick。这样在写主循环时只需要维护一个整数变量current_tick不需要调用sleep或usleep逻辑更干净。另一个坑是到达时间的处理。进程到达时间必须和当前tick比较如果当前tick小于到达时间调度器要空转等待。很多同学在这里写死循环导致CPU占用率飙到100%这个在第五章会专门讲。先把单位统一了后面所有比较逻辑都不会因为单位换算出错。2.5 报告结构先搭好原理、设计、验证三段骨架我习惯在写代码前先列报告提纲因为课设最终要交一份实验报告。标题、摘要、背景可以后写但“算法原理”“数据结构”“实验环境”“结果验证”这几段一定要预先占住位置。结果验证部分尤其重要它要求你有输入、输出、截图、对比数据。如果你在写代码前就知道要对比三种时间片长度你就不会只写死一组参数。把报告骨架放在代码旁边写代码时就会有意识保留中间日志、记录不同参数的结果。这个习惯不花额外时间但能让最后报告从“附了段代码”变成“有一组完整的实验数据”分数差别很大。3. 用C语言实现时间片轮转调度PCB、就绪队列和主循环的完整代码这一章直接给一个能跑的最小实现。我用单文件C代码方便你在Linux终端里用gcc编译。核心是三块PCB结构体、就绪队列的入队出队、主循环里的时间推进。这里没有用高级技巧全部围绕课设验收点来写。3.1 PCB结构体pid、arrive_time、need_time、run_time、wait_time进程控制块是课设的基石。先定义结构体和全局数组#include stdio.h #include stdlib.h #define MAX_PROC 100 #define TIME_SLICE 2 /* 一个时间片占 2 个 tick */ typedef enum { NEW, READY, RUNNING, DONE } State; typedef struct { int pid; State state; int arrive_time; /* 到达 tick */ int need_time; /* 需要的总 tick 数 */ int run_time; /* 已运行 tick 数 */ int wait_time; /* 累计等待 tick 数 */ } PCB; PCB pcb_pool[MAX_PROC]; int pcb_count 0;这里用固定数组而不是malloc主要是为了调试方便。课设规模通常不超过100个进程数组完全够用。等你把逻辑跑通再改成动态数组做进阶扩展也不迟。state字段是枚举类型但实际调度中运行和就绪的区别只在“当前正在执行的这一个”所以很多判断可以用run_time和need_time的关系替代state主要用于打印日志和状态转换时检查逻辑漏洞。3.2 就绪队列用环形数组入队、出队、判空三个函数就绪队列用环形数组避免频繁移动内存。代码typedef struct { int data[MAX_PROC]; int head, tail, size; } ReadyQueue; void q_init(ReadyQueue *q) { q-head q-tail q-size 0; } int q_is_empty(ReadyQueue *q) { return q-size 0; } void q_enqueue(ReadyQueue *q, int pid) { q-data[q-tail] pid; q-tail (q-tail 1) % MAX_PROC; q-size; } int q_dequeue(ReadyQueue *q) { int pid q-data[q-head]; q-head (q-head 1) % MAX_PROC; q-size--; return pid; }环形数组有两个边界问题。第一队列满时再入队会覆盖旧数据但课设进程数远小于MAX_PROC所以这里没做满判断读者在自己扩展时最好补上。第二size和head/tail的维护必须同步如果只在入队时加size、出队时忘了减就会出现队空判断错误。我调试时经常打印这三个变量一眼就能看出哪一步没有同步。3.3 主循环时间推进、调度决策、完成判断主循环是课设的核心。我把它拆成四步到达进程入队、时间片到期换人、取队首进程运行、统计等待时间。代码void simulate() { ReadyQueue rq; q_init(rq); int current_tick 0; int completed 0; int current_pid -1; int local_run 0; /* 当前进程已经连续运行的 tick 数 */ while (completed pcb_count) { /* 1. 到达的进程从 NEW 变 READY进入就绪队列 */ for (int i 0; i pcb_count; i) { if (pcb_pool[i].arrive_time current_tick pcb_pool[i].state NEW) { pcb_pool[i].state READY; q_enqueue(rq, pcb_pool[i].pid); } } /* 2. 当前进程时间片耗尽放回队尾 */ if (current_pid ! -1 local_run TIME_SLICE) { pcb_pool[current_pid].state READY; q_enqueue(rq, current_pid); current_pid -1; local_run 0; } /* 3. 如果CPU空着从队首取新进程 */ if (current_pid -1 !q_is_empty(rq)) { current_pid q_dequeue(rq); pcb_pool[current_pid].state RUNNING; local_run 0; } /* 4. 运行一个 tick */ if (current_pid ! -1) { PCB *p pcb_pool[current_pid]; p-run_time; local_run; printf(tick %d: P%d running\n, current_tick, p-pid); if (p-run_time p-need_time) { p-state DONE; completed; current_pid -1; local_run 0; printf(tick %d: P%d finished\n, current_tick, p-pid); } } /* 5. 所有就绪且没在运行的进程等待时间加 1 */ for (int i 0; i pcb_count; i) { if (pcb_pool[i].state READY pcb_pool[i].pid ! current_pid) { pcb_pool[i].wait_time; } } current_tick; } }这里用local_run记录当前进程连续运行的tick数比取模方式直观也不会出现第一次调度时边界判断出错的问题。local_run在取新进程时归零在时间片用完时也归零两个位置容易漏建议在代码里加注释标出来。主循环的复杂度是O(n)每个tick扫描一次全部进程课设规模下完全够用。如果你要模拟几千个进程再用链式队列加时间堆优化但那是后话不是课设核心。3.4 主函数读文件、初始化PCB、调用模拟主函数负责解析输入文件初始化每个PCB然后调用simulate。int main(int argc, char *argv[]) { if (argc 2) { printf(usage: %s proc.txt\n, argv[0]); return 1; } FILE *fp fopen(argv[1], r); if (!fp) { perror(open); return 1; } while (fscanf(fp, %d %d %d, pcb_pool[pcb_count].pid, pcb_pool[pcb_count].arrive_time, pcb_pool[pcb_count].need_time) 3) { pcb_pool[pcb_count].state NEW; pcb_pool[pcb_count].run_time 0; pcb_pool[pcb_count].wait_time 0; pcb_count; } fclose(fp); simulate(); return 0; }如果你需要统计平均周转时间在simulate结束后遍历pcb_pool周转时间等于wait_time need_time然后除以pcb_count。注意wait_time只在进程处于READY状态时累加如果一个进程到达后一直没被调度它的wait_time会正确增长。如果进程在arrive_time之前它还在NEW状态不会累加等待时间这一点要和报告里的公式对上。3.5 统计指标别算错平均周转时间、平均等待时间、带权周转时间答辩必问三个指标。周转时间是进程从到达到最后完成的总时间等于need_time wait_time。等待时间是进程在就绪队列里等待的时间。带权周转时间是周转时间除以服务时间反映调度对短进程的友好程度。很多同学的课设只算了前两个第三个不会算然后被老师追问。这三个指标对应三种调度目标。批处理系统看平均周转时间交互式系统看响应时间实时系统看截止时间。你在报告里至少要给出两个时间片参数下的平均等待时间对比这样才说明你做的是“实验”不是“写了个程序”。4. 在Ubuntu/Linux上编译调试Makefile、gdb和三个必调参数代码写在编辑器里但真正的战斗在终端。这一章讲怎么编译、怎么定位崩溃、怎么让输出结果稳定可复现。操作系统课设的代码量不大但编译和调试环节经常耗掉一半时间。4.1 用Makefile管起编译gcc参数里必须有-g和-O0即使只有一个.c文件我也建议写Makefile。因为报告里经常被问“你怎么编译的”一个清晰的Makefile比一大串gcc命令更有说服力。示例CC gcc CFLAGS -Wall -g -O0 TARGET sched SRCS schedule.c $(TARGET): $(SRCS) $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) clean: rm -f $(TARGET)这里-g是必须的没有它gdb看不到函数名和变量。-O0关闭优化防止编译器把变量优化掉导致你调试时看到的值和代码对不上。-Wall开警告课设代码里的警告往往就是隐患。比如fscanf返回值没检查编译器会提示但很多同学没开-Wall直接忽略。运行make生成sched再用./sched proc.txt测试。如果你要对比不同时间片可以改Makefile里的TIME_SLICE宏或者更优雅的方式是把时间片作为命令行参数传进去。4.2 用gdb定位段错误core dump、backtrace和print进程一跑就崩Segmentation fault (core dumped)是课设最常见的崩溃。原因多半是数组越界或空指针解引用。先启用手动core dump再用gdbulimit -c unlimited ./sched proc.txt gdb ./sched core在gdb里输入bt能看到崩溃时的调用栈。如果core文件没有生成用gdb ./sched启动然后输入run proc.txt程序会在崩溃处停住输入list看附近代码。我一般会先在队列函数里加打印跟踪head和tail的变化。环形数组的越界常常表现为“队列空但不为空”或者“入队后size不对”这时候打印size和head/tail三个值能立刻发现问题。例如printf(DEBUG enqueue pid%d size%d head%d tail%d\n, pid, q-size, q-head, q-tail);4.3 固定随机种子否则结果不可复现如果你为了模拟进程到达用rand()生成到达时间那一定要固定种子srand(42); /* 固定种子确保每次运行结果一致 */否则你跑两次结果不一样报告里没法解释。随机种子可以放在main函数第一行或者在定义输入时用。注意是42还是其他数字不重要重要的是固定。时间片长度一般取1、2、3、4、5这样的小整数。你可以先用proc.txt测出三组时间片下的输出然后对比平均等待时间。不要一次把时间片设成100那会让调度退化成先来先服务报告里看不出任何调节效果。4.4 输出重定向到文件别在终端里看几百行日志模拟器跑几十个tick终端还能看清。如果模拟几百个tick日志滚屏速度远快于你阅读的速度很容易看漏关键行。我把日志重定向到文件./sched proc.txt out.log 21 tail -f out.log这样既能保留完整日志又能在结果文件里用grep快速筛选“finished”或“running”。另外在提交代码时把日志输出和统计结果分开统计结果单独打印到stdout日志打印到stderr这样别人看的时候不会一头雾水。4.5 调试技巧两则条件断点和printf日志开关gdb里的条件断点很管用。比如你想在某个进程第一次进入运行态时停下来break simulate condition 1 run_time 2不过课设代码简单我更多用printf打开关控制。定义两个宏#define DEBUG_LOG 1 #if DEBUG_LOG #define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__) #else #define LOG(fmt, ...) #endif然后代码里所有调试输出都用LOG宏。调试时把DEBUG_LOG设为1跑测试时设为0报告里不用贴一堆中间输出。这个习惯能让你的报告看起来像一个认真做的实验而不是调试时的临时人工现场。5. 课设翻车现场排查调度顺序颠倒、CPU烧高、内存泄漏和输出乱码这一章全是血泪经验。原理都懂代码也能编译但运行结果就是不对。下面五条是我在课设中见过最高频的问题每条按现象、原因、解决来写。你如果遇到类似情况直接按这里排查。5.1 调度顺序颠倒新进程总是插队老进程活活饿死现象后到达的进程先运行先到达的P1被反复后延甚至一直不结束。原因新进程入队时走了头插法也就是加到了队首。常见于用链表实现就绪队列图方便用head-next new;结果每次新进程都插到队首老进程永远排后面。时间片轮转的前提是先来先服务插队破坏了这个前提。解决所有新进程必须从队尾进入只调用q_enqueue不允许在调度逻辑里绕过头插。如果你确实需要优先级先说清楚同优先级内是否保序。我在代码里加过一行注释“入队只允许用q_enqueue禁止直接改head”之后没再犯过。5.2 CPU占用率100%调度器空转而不是阻塞等待新进程现象进程没跑完风扇先转起来。top看到sched进程CPU占用接近100%。原因在主循环里当就绪队列为空且当前没有进程运行时代码写了一个空的忙等循环比如while (1);或者for (;;);。这在操作系统原理上相当于自旋应该把CPU让出去但模拟器不需要真的让出CPU只需要把时间推进到下一个到达事件。解决把空转替换成“直接跳到下一个到达时间”。方法是扫描所有进程找到还没到达且到达时间大于current_tick的最小值然后把current_tick直接设为这个值。注意不要让current_tick跳过头否则可能跳过一些进程在同一时刻到达的顺序。这个修复能让程序在进程间隔很大的跑法中瞬间完成而不是空转几千个tick。5.3 内存泄漏PCB用malloc分配进程完成却不free现象模拟程序跑一万个tick后系统内存占用越来越大甚至卡顿。原因如果PCB不是固定数组而是malloc分配的每个新进程都分配一块进程完成却没有free就会泄漏。课设数据量小问题不大但答辩老师会检查这一点。解决最简单的方案是回到固定数组课设规模不需要动态分配。如果一定要动态分配把free放在state DONE分支里free后立刻把指针置为NULL再用时重新malloc。注意不要在进程还没结束时提前free那会导致后边的状态写入变成野指针。5.4 输出乱码字段对齐不一致报告没法分析现象打印出来的日志一会儿是“P2”一会儿是“P10”列对不齐肉眼看不出哪个时刻谁在运行。原因printf里写死了P%d进程号个位数和两位数宽度不一样。日志和分析工具没法对齐。解决用固定宽度格式化比如P%-4d。这样所有进程号都左对齐占4个宽度报告排版整齐。另外统一用stdout输出统计结果用stderr输出调试日志两者分开后处理脚本才不会被日志干扰。5.5 进程永远不结束比较时用了而不是或者need_time设为0现象程序运行很久completed一直没到pcb_count最后超时或卡死。原因一种情况是if (run_time need_time)判断时进程超出一个tick导致跳过相等直接跳到从此永远不等于。另一种是输入文件里把need_time写成了0进程一运行就满足run_time0但状态没正确转换。解决把完成判断改成同时在读文件时检查need_time 0则报错。是一种防御性写法让程序不会因为一个tick的偏差漏判。这个细节很小但非常典型。6. 收尾技巧用自动对比脚本把实验报告变成能演示的结论代码能跑只是及格课设要得高分得让结果能验证、可对比。这章讲两个技巧能把你的报告从“附了一段代码”升级成“有一套实验结论”。6.1 让时间片作为命令行参数然后批量扫描不要每次改时间片都改源码重编译直接把时间片从命令行传进来。在main函数里加一个参数int time_slice 2; if (argc 3) time_slice atoi(argv[2]);然后主循环里的TIME_SLICE宏替换成变量time_slice。这样你就能写一个Bash脚本批量测试for ts in 1 2 3 4 5; do ./sched proc.txt $ts | grep avg_wait result.txt done这个脚本跑完你能得到一张时间片长度和平均等待时间的对应表。报告里贴这张表再写一句“时间片为2时平均等待时间最短”整篇报告的说服力完全不同。6.2 导出CSV用Python画甘特图模拟器里把每个进程的运行时间段导出成三列pid、start_tick、end_tick存入gantt.csv。然后用Python画图import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(gantt.csv) plt.barh(df[pid], df[end] - df[start], leftdf[start]) plt.xlabel(tick) plt.ylabel(pid) plt.savefig(gantt.png)这段代码唯一的坑是leftdf[start]要求数据按pid排序否则画出来条形错位。我习惯在模拟器里按pid、start_tick排序后再导出省得在Python里二次排序。6.3 我的习惯先写两个进程的测试数据手算出预期结果再写主程序最后分享一个我每次做课设都会用的习惯动手写代码前先构造一个只有两个进程的输入然后手工在纸上把整个调度过程推演一遍得到预期的输出。比如进程1在tick0到达需要3个tick进程2在tick1到达需要2个tick时间片为2手推一遍就知道P1先跑到tick2时间片用完让出P2跑到tick4……等等。当你拿小程序测出结果和手推不一致时问题出在哪里基本立刻就能定位。我当年做操作系统课设就是因为赶时间跳过这步结果平均等待时间公式写错了答辩时被老师一眼看出。后来我把这个习惯带进每一个模块再也没有在细节上翻过车。希望这篇实操笔记能帮你在湖南科技大学的操作系统课设里少走这些弯路把时间花在真正需要验证的问题上。本文还有配套的精品资源点击获取
返回列表