ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

任务调度器课程设计:基于C语言实现FCFS、SJF与RR调度算法

任务调度器课程设计:基于C语言实现FCFS、SJF与RR调度算法 简介面向大二学生数据结构课程设计基于C/C实现的任务调度器系统覆盖任务队列、调度模块与交互主程序适合需要完成同类选题、理解队列结构与任务管理流程的学生参考。压缩包共38个文件大小约40.97MB包含VS2019解决方案、C源码、任务队列与调度管理相关头文件、测试主程序、可直接运行的exe以及pdb、obj、tlog等编译调试产生的中间文件工程目录清晰解压后用Visual Studio 2019打开即可复现运行效果。已有207名学生学习下载说明该选题在课程设计场景中具备一定代表性。通过该项目可以完整走一遍任务调度器的设计实现过程掌握任务队列构建、任务状态维护、调度策略设计等核心思路也能借鉴其模块划分、工程组织方式和类图设计便于快速改写成自己的课程设计代码、报告与答辩材料。1. 任务调度器这件课设为什么值得认真做很多人拿到任务调度器这个题目的第一反应是去翻操作系统里的进程调度算法结果发现课设要求的是用数据结构去模拟调度过程而不是真的实现一个操作系统调度内核。先来先服务要排队短作业优先要快速找出最小服务时间时间片轮转要环形取任务这背后对应的是队列、优先队列和循环队列的设计与编码能力。正因为如此它才成为数据结构课程设计里最常被选中的题目之一——题目不大但能同时考到结构体、指针、堆、队列和排序稳定性。这份方案面向正在准备数据结构课程设计的学生也面向想快速理清任务调度器完整实现思路的开发者给出的是从需求分析、数据结构选型、模块划分到C语言核心实现的一整套可落地参考。适合拿来做C/C课设的蓝本也适合在答辩时应对你为什么要这样设计的追问。2. 需求分析与技术选型先确定调度规则再决定用堆还是队列2.1 课设任务书里最容易被忽略的四个需求点先别急着写代码我会把任务书拆成四个必答项。第一必须支持的调度算法。常见组合是FCFS先来先服务、SJF短作业优先、RR时间片轮转三种部分题目还会要求动态优先级调度。第二任务输入方式。多数要求从文件或控制台读入任务列表每条任务包含任务ID、到达时间和服务时间。第三结果输出。通常要求打印调度顺序、每个任务的开始时间、完成时间、等待时间、周转时间以及平均周转时间。第四边界行为。比如两个任务同时到达、服务时间相同、任务到达时间错峰这些场景决定了算法能否正确处理排序稳定性。以最常见的输入文件为例5 T1 0 8 T2 1 4 T3 2 9 T4 3 5 T5 4 2第一行是任务总数之后每行依次是任务ID、到达时间、服务时间。表面上简单但隐含了一个容易漏掉的需求任务不是一开始全部到达调度器必须在current_time小于下一个任务到达时间时等待。我一般会在需求分析里专门写一条支持任务错峰到达这样设计就完整了。需求分析还会直接影响编码边界。比如如果任务书要求输出每个任务的开始时间那Task里还需要记录start_time如果要求支持任务随机生成那么输入层就要加一个生成器函数。很多同学拿到题就开写写着发现字段不够用又回头改结构体来回浪费一个下午。我通常的做法是先在纸上画一个输入的样例、一个输出的样例把字段对齐了再动键盘。2.2 数据结构选型优先队列、普通队列和数组各司其职任务调度器的核心数据结构是就绪队列但不同算法对队列的要求彼此冲突。FCFS只需要先进先出用循环队列即可SJF每次要选出服务时间最短的任务用最小堆最合适RR要按顺序循环取任务本质上又是循环队列。如果只用数组加排序硬扛每来一个新任务就重新全排序代码难写且复杂度高更重要的是答辩时说不清楚为什么用这个结构。因此在完整方案里我会同时实现两个就绪容器一个二叉堆用于SJF和优先级调度一个循环队列用于FCFS和RR。二者都以Task指针为元素通过相同的队列接口操作。下面这个表是我放在课设报告里的选型对比数据结构适用算法核心操作时间复杂度循环队列FCFS / RR入队、出队、判空O(1)二叉堆最小堆SJF / 优先级调度插入、取最小、堆化O(log n)数组全部算法存储原始任务、按id索引O(n)有一点需要特别说明SJF里的最短是指服务时间最小当服务时间相等时按到达时间排序。这就要求堆的比较函数不是只看service_time而是先比service_time再比arrive_time否则两个服务时间相同的任务顺序可能与手算结果不一致。这是后面的避坑章节里最容易翻车的一个点。在这个选型阶段还有一个容易被当成玄学的点为什么时间片轮转也要用队列而不是链表。链表插入删除虽然方便但循环队列在固定大小任务数组的课设场景下更简单不需要频繁malloc/new。如果任务数上限已知预分配一个MAX_TASKS容量的数组用head和tail两个下标维护队列几乎没有内存管理负担。链表方案可以作为加分项写在扩展思考里但核心代码用循环队列更容易讲。2.3 用C还是C封装与调试成本的权衡如果允许自由选择语言我会优先用C而不是C。原因有三第一C的结构体和指针能直观展示任务节点、堆、队列之间的关系答辩老师很容易看出你掌握的程度第二C的STL会把priority_queue、queue这些容器都包装好代码量少了但老师一句你讲讲最小堆的堆化过程就可能答不上来第三C程序用gdb调试堆数组和队列更透明每次push/pop后都能直接查看data数组的顺序。如果你更习惯C也可以用class封装Scheduler和Task但必须自己管理析构和拷贝避免浅拷贝导致指针重复释放。这个方案里给出的代码以C语言为基准同时在注释里标注了C改进点。我这里建议课设代码控制在三个源文件main.c负责流程控制scheduler.c负责算法实现task_queue.c负责堆与队列的操作。模块清晰答辩时分头讲解很从容。选型确定后下一步需要把数据结构封装成统一接口让调度算法不感知底层是堆还是队列。比如我定义一组入队、出队、判空函数堆和循环队列都实现同一套函数名内部各自维护自己的存储方式。这样的抽象能让后续切换算法时只改动一行设置代码也为第3章的总体架构设计做了铺垫。3. 总体架构设计三层结构与五类模块的职责划分3.1 顶层分层输入层、调度核心层、输出层任务调度器整体采用三层结构。输入层负责解析任务文件、校验数据比如任务数不能为0、服务时间不能为负数、时间片必须大于0等。调度核心层维护任务状态机和三个调度算法这一层不关心数据是从文件还是控制台来的。输出层负责打印调度顺序和统计指标。这样的好处是算法切换不会影响输入输出新增算法时只需要在核心层添加一个函数。在课设报告里我会用一个模块划分表来说明模块文件职责关键结构/函数main.c解析命令行参数、组织流程Scheduler结构体task_queue.c循环队列与最小堆的操作enqueue、dequeue、heap_push、heap_popscheduler.c三种调度算法的状态推进scheduler_fcfs / sjf / rroutput.c打印调度顺序、计算统计量print_schedule、calc_avgmain函数中先用load_tasks()读取任务数组再根据命令行参数创建Scheduler调用scheduler_run()最后调用print_result()。我会特别在main函数里保留一个打印中间状态的开关调试时开启答辩演示时关闭。在三层结构的边界上我还会留一个hook点调度核心层只暴露一个scheduler_run()接口和一个scheduler_set_algorithm()设置函数。这样main.c无需关心算法内部是用了堆还是队列如果日后想支持动态优先级抢占只需在Scheduler里增加一个priority字段并在比较函数中增加一条判断即可。这种设计看似多写几行代码但在答辩中被问到可扩展性时就能拿出实际的函数接口来证明而不是只说思路。3.2 任务状态机新建、就绪、运行、完成任务不能共享同一块队列就完事每个任务都有自己的生命周期。我给Task设计一个state字段枚举值分别代表NEW新建、READY就绪、RUNNING运行、FINISHED完成。调度流程是这样的所有任务初始都是NEW当current_time推进到某个任务的到达时间时把它插入就绪队列并置为READY调度器从就绪队列取一个任务置为RUNNING并运行任务运行完成后置为FINISHED。以RR举例任务T1第一次被调度时state由1变为2运行一个时间片未完成state回到1并插入队尾等第二次出队再次变为2运行完毕才变为3。如果没有这中间一次READY回缩任务就可能在第二次被取出时还被当作RUNNING导致调度器认定它还在运行而不重新分配。状态机把这个问题转化成了简单的枚举判断代码里一眼能看清。这个状态机最大的价值是避免重复入队。RR算法里一个任务执行完一个时间片但没结束会被放回队尾此时它的状态应该是READY而不是RUNNING而一个已经FINISHED的任务如果因为下标判断失误再次被扫描到就会重复入队。有了state每次入队前检查一下不是READY和RUNNING就能提前拦住错误。3.3 调度核心模块统一接口与三个算法的策略选择调度核心层我不会写成三个相互独立的大函数而是定义一个Scheduler结构体来统一管理typedef struct { int algorithm; /* 0-FCFS 1-SJF 2-RR */ int time_slice; /* RR使用 */ int current_time; Task *tasks; int task_count; int finished_count; } Scheduler;然后提供一个scheduler_run()入口内部通过switch调用不同的调度函数。每个调度函数只负责把task的finish_time写好不直接打印结果。这样统计模块永远从finish_time字段取数不会因为全局变量被改而算错。使用统一接口还有一个微观好处统计模块在调度完成后遍历任务数组不需要知道刚才跑的是哪个算法。算法的选择只影响每个任务的finish_time不影响后续计算。这也让main.c的代码量大幅减少从开始到读入参数再到输出结果不超过60行。三种算法的推进逻辑分别是FCFS按到达顺序入队任务执行完才让出CPUSJF每步从最小堆堆顶取任务同样不可抢占RR每个任务最多运行一个时间片没做完就重新入队尾部。对比来看SJF和FCFS都是运行到结束只是选择顺序不同RR才会在运行中途打断。答辩时我会主动说出这个区别表明算法并不是简单换排序。3.4 统计指标模块周转时间、等待时间与平均数的计算课程设计通常要求输出平均周转时间所以统计模块要独立于调度模块。每个任务调度完成后只记录finish_time统计时再统一计算周转时间 finish_time - arrive_time等待时间 周转时间 - service_time带权周转时间 周转时间 / service_time。平均周转时间则是所有任务周转时间之和除以任务数。这里有一个容易踩坑的细节打印带权周转时间时如果用整数除法9/8会变成1而不是1.12。我会用浮点数保存结果printf保留两位小数。另外当任务服务时间为0时带权周转时间会做除零运算我在输入校验里直接禁止服务时间小于等于0从根上规避掉这个黑匣子问题。输出格式我建议用表格对齐方式比如每行打印任务ID、到达时间、服务时间、完成时间、周转时间、带权周转时间。最后单独一行打印平均周转时间和平均带权周转时间。这样的打印结果可以直接贴进课程设计报告不需要再手工整理。4. 关键实现堆、队列与三种调度算法的落地代码4.1 任务结构体与最小堆的实现代码从Task定义开始typedef struct Task { int id; /* 任务ID从1开始 */ int arrive_time; /* 到达时间 */ int service_time; /* 服务时间 */ int done_time; /* 已经运行的时间RR用 */ int finish_time; /* 完成时间 */ int state; /* 0新建 1就绪 2运行 3完成 */ } Task;就绪容器我采用两个独立结构体的方案先看最小堆typedef struct PQueue { Task *data[MAX_TASKS]; /* 存储任务指针 */ int size; /* 堆中元素个数 */ } PQueue; void heap_push(PQueue *h, Task *t) { int i h-size; h-data[i] t; while (i 0) { int p (i - 1) / 2; if (compare(h-data[i], h-data[p]) 0) { Task *tmp h-data[i]; h-data[i] h-data[p]; h-data[p] tmp; i p; } else { break; } } } Task *heap_pop(PQueue *h) { Task *top h-data[0]; h-data[0] h-data[--h-size]; int i 0; while (1) { int l 2 * i 1, r 2 * i 2, smallest i; if (l h-size compare(h-data[l], h-data[smallest]) 0) smallest l; if (r h-size compare(h-data[r], h-data[smallest]) 0) smallest r; if (smallest i) break; Task *tmp h-data[i]; h-data[i] h-data[smallest]; h-data[smallest] tmp; i smallest; } return top; }这段代码的逻辑是heap_push从数组末尾上浮heap_pop先将堆顶与末尾元素交换并下沉正好对应堆的两个核心操作。MAX_TASKS采用宏定义如#define MAX_TASKS 100避免动态扩容引出的指针失效问题。compare函数是优先级判定的核心int compare(Task *a, Task *b) { if (a-service_time ! b-service_time) { return a-service_time - b-service_time; } return a-arrive_time - b-arrive_time; }返回值小于0表示a应排在b前面。这与普通排序的cmp函数保持一致。很多同学在这里会犯方向性错误写成b-service_time - a-service_time导致最小堆变成最大堆这是最典型的翻车点。4.2 FCFS与SJF调度实现FCFS采用循环队列这里给出调度主体的最简版void scheduler_fcfs(Scheduler *sched) { CQueue q; init_queue(q); int next 0; while (sched-finished_count sched-task_count) { while (next sched-task_count sched-tasks[next].arrive_time sched-current_time) { sched-tasks[next].state 1; enqueue(q, sched-tasks[next]); next; } if (is_empty(q)) { sched-current_time; continue; } Task *t dequeue(q); t-state 2; t-finish_time sched-current_time t-service_time; sched-current_time t-finish_time; t-state 3; sched-finished_count; } }注意两个细节第一个while负责把到点的新任务放入队列第二个if用于处理队列为空且任务尚未全部到达的情况这时current_time逐秒推进会稍慢但逻辑最简单。如果追求效率可以直接跳到下一个任务的到达时间但仍要处理下一个任务也没到齐的边界。SJF与FCFS的结构非常像只是把循环队列换成最小堆取任务时从堆顶取void scheduler_sjf(Scheduler *sched) { PQueue h; h.size 0; int next 0; while (sched-finished_count sched-task_count) { while (next sched-task_count sched-tasks[next].arrive_time sched-current_time) { sched-tasks[next].state 1; heap_push(h, sched-tasks[next]); next; } if (h.size 0) { sched-current_time sched-tasks[next].arrive_time; continue; } Task *t heap_pop(h); t-state 2; t-finish_time sched-current_time t-service_time; sched-current_time t-finish_time; t-state 3; sched-finished_count; } }这段代码与FCFS最大的区别在于两处一是就绪容器由队列变成堆二是队列为空时不再用current_time而是直接跳跃到下一个任务的到达时间。跳跃写法在SJF里更合理因为堆为空代表此刻没有可执行任务继续逐秒加一不会改变堆中的内容。4.3 时间片轮转RR的实现与队尾回插RR需要维护每个任务已经运行的时间done_time时间片time_slice由命令行传入。核心代码void scheduler_rr(Scheduler *sched, int time_slice) { CQueue q; init_queue(q); int next 0; while (sched-finished_count sched-task_count) { while (next sched-task_count sched-tasks[next].arrive_time sched-current_time) { sched-tasks[next].state 1; enqueue(q, sched-tasks[next]); next; } if (is_empty(q)) { sched-current_time; continue; } Task *t dequeue(q); if (t-state 3) continue; /* 防御已完成任务不入队 */ t-state 2; int need t-service_time - t-done_time; int run_time need time_slice ? time_slice : need; t-done_time run_time; sched-current_time run_time; if (t-done_time t-service_time) { t-state 1; enqueue(q, t); /* 没做完放回队尾 */ } else { t-finish_time sched-current_time; t-state 3; sched-finished_count; } } }这段代码里最关键的是回插动作任务每轮只能运行一个时间片如果还没完成done_time加满当前时间片后重新入队。需要特别注意的是回插的任务不能被重复计算到达时间所以next指针只负责新到达任务回插直接走enqueue队列尾部。防御性的state3判断是血泪经验曾经有同学在任务完成瞬间又被调度器取出就是因为状态没有置为完成。4.4 命令行参数与运行配置为了让课设更完整我会让程序支持命令行参数./scheduler -a sjf -t input.txt -o result.txt -q 2各参数含义如下表参数说明默认值-a调度算法fcfs/sjf/rrfcfs-t任务文件路径input.txt-o结果输出文件stdout-q时间片大小RR2main函数中用getopt解析参数并用switch判断算法。文件不存在或参数非法时打印输入文件不存在不直接崩溃。这样设计的好处是演示时可以在答辩现场用同一份任务文件分别跑三种算法对比输出效果比只看代码好得多。参数表可以放在设计文档中代码里只需要一个结构体SchedConfig来保存。5. 常见问题与避坑指南五个最容易翻车的地方任务调度器这个课设代码量不大但翻车点很集中。我见过的模拟项目里十个同学有七八个都卡在下面这几类问题上。这里按现象→原因→解决的顺序整理方便你在答辩前快速自查。5.1 比较函数方向写反最短任务反而最后执行现象SJF调度输出里服务时间最短的任务出现在序列最后运行时间反而像最长作业优先。 原因最小堆的compare返回值语义与预期相反。我见过很多初学者写compare时参考qsort的写法结果在堆的siftDown里又套了一层balance 0的符号翻转负负得正。 解决统一约定compare(a,b) 0 表示 a 优先级高于 b堆顶永远存放compare值最小的元素。写完compare后先把三个任务服务时间6、2、4按顺序插入堆再全部弹出检查是否得到2、4、6。这个测试只需10秒能避免后面所有算法结果错乱。我一般会在单元测试代码里直接写上这个用例作为回归检查。5.2 时间片轮转里任务重复入队程序死循环现象RR运行到某个任务后发现它被调度了两次或者printf刷屏不断执行同一个任务。 原因任务已经完成但状态没有置为FINISHED或者调度器在新到达任务扫描时重复处理了已经入队的任务。 解决强制使用任务状态机入队前检查state是否为NEW或FINISHED。更直接的防御是在调度器入口加一个断言if (t-state 3) continue;同时维护一个next下标只扫描尚未入队的新任务禁止每轮都从头遍历整个任务数组。这两种手段叠加后重复入队基本被根除。5.3 任务错峰到达时程序卡在空队列等待现象用全是0时刻到达的测试数据一切正常换成错峰到达后程序要么长时间无输出要么直接跳过任务。 原因调度器在队列为空时没有处理下一个任务还没到达的情况。FCFS里用current_time能侥幸跑通但SJF里如果也这么做遇到下一个任务到达时间跨度很大程序会做大量无效循环观感上像死机。 解决队列为空时直接把current_time跳到下一个待入队任务的到达时间。SJF代码里的sched-current_time sched-tasks[next].arrive_time;就是这个用途。需要注意的是跳跃前要确保next下标有效也就是next task_count。否则说明所有任务都已经处理完这本来也是退出循环的条件。5.4 动态分配产生的野指针和内存泄漏现象程序测小数据正常加到50个任务后偶发崩溃或者使用valgrind检查时发现一堆invalid read。 原因Task数组用malloc动态分配而队列data数组里保存的是Task指针当任务数组被realloc扩容时旧指针全部失效。这是最典型的野指针来源。 解决课程设计场景下直接在栈上定义固定大小数组Task tasks[MAX_TASKS];并先读入任务数再检查是否超过MAX_TASKS。如果一定要动态分配就一次性malloc最大任务数不要做realloc。对于队列中的指针只要任务数组地址不变指针始终有效。检查泄漏时在gdb或valgrind下跑一遍完整测试会大大降低翻车概率。5.5 输出结果与手算不一致平均周转时间算错现象调度顺序看起来对但平均周转时间比手算大很多或者带权周转时间出现1.00这种明显不合理的数值。 原因统计时用全局current_time而不是任务自身的finish_time。由于RR会反复切换任务某个任务完成时current_time可能已经被后续任务推进过直接用当前时间减到达时间必然出错。 解决在任务完成时就把t-finish_time记录好统计模块只从finish_time字段取值。打印前先逐个输出每个任务的arrive_time、finish_time、周转时间与手算表逐项比对。这里我踩过一个坑带权周转时间用整数除法9/8算成1后来全部改成double输出加上%.2f结果才正常。6. 验收与答辩把课设做到能演示、能讲透6.1 功能验收清单在提交前我会用一个包含5个任务、3种算法的测试文件跑完整流程并对照手算结果。下表是最低验收标准检查项通过标准输入解析能正确读取文件非法参数给出提示不崩溃FCFS调度顺序与手算完全一致SJF最短任务最先执行服务时间相同时按到达顺序RR每个任务每个时间片只运行一次回插正确统计平均周转时间、平均带权周转时间保留两位小数鲁棒性任务数0、时间片0、文件不存在均能友好退出6.2 答辩演示流程答辩时只讲三条主线需求、选型、验证。先花半分钟说这个课设解决的问题是让一组任务在三种调度策略下得到不同的执行序列并量化对比再用同一组数据分别跑FCFS、SJF、RR指着输出说SJF的平均周转时间比FCFS低因为短任务优先完成。最后结合代码解释为什么要用堆——SJF每次取任务需要O(log n)如果改用顺序查找整体复杂度就到O(n²)。老师如果要你展示代码优先展示compare函数和RR的回插逻辑这两个点最能体现出对数据结构的理解。演示前准备一页打印纸左侧输入、右侧三个算法的输出老师提问时不会因为现场敲命令而冷场。如果时间充裕可以点亮一个debug模式开关打印每个时间片的就绪队列内容这会让演示显得很扎实。这套完整的课程设计资源包含可编译运行的C源码、任务生成与测试用例、设计文档框架和答辩PPT提纲拿到后可以直接对照修改。建议在动笔写报告前先跑通功能再回头整理文档效率会高很多。我最初做模拟项目X时也曾在RR里把任务重复入队调试到半夜翻车后来养成的习惯是每改一次算法先拿三个任务做手算再到程序里打印current_time和队列内容比对。从那以后我每次做调度类的课程设计都会强制自己走一遍手算结果 → 程序输出 → 差异分析的流程。希望这份方案能帮到你少踩几个坑。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表