首页 / 资讯中心 / 文章详情

基于Java的操作系统课设:FCFS作业调度、RR进程调度与内存管理联动

基于Java的操作系统课设:FCFS作业调度、RR进程调度与内存管理联动 ★ FEATURED ARTICLE
简介一份面向计算机相关专业学生的操作系统课程设计源码与报告包围绕作业调度、内存管理、进程调度与进程阻塞唤醒四个核心模块展开。项目用随机数初始化10个作业进入后备队列采用先来先服务FCFS完成作业调度内存中最多驻留5个进程处于阻塞状态的进程仍占用内存就绪和运行状态进程以时间片轮转RR算法调度首次适应FF算法负责内存分配进程结束时回收并合并空闲分区整体串成结构清晰、模块划分明确的可直接运行的控制台演示程序。压缩包共8个文件以4个Java源文件为主可直接导入IDE调试另附PDF与Word课设报告、Markdown说明和License文件便于阅读设计、整理文档。包体约1.28MB体量紧凑已有约259人学习下载适合操作系统课设参考算法组合、代码框架与报告写法。1. 基于 Java 的操作系统课设作业调度、内存管理与进程调度怎么串成一条线操作系统课设最怕的不是某个算法写不出来而是作业调度、内存管理、进程调度各写各的最后拼不到一起。这份基于 Java 的控制台课设把 FCFS 作业调度、RR 时间片轮转、FF 首次适应内存分配和进程阻塞唤醒全部串在一条主流程里10 个随机作业从后备队列一步步走进内存、上 CPU、被阻塞、再被唤醒整个过程在控制台就能看到。适合正在做操作系统课设、特别是“老师要求多个算法联动而不是单独演示”的 Java 学生。我拆完源程序和报告后发现它最大的价值不是某个算法写得漂亮而是把“作业调度 → 内存分配 → 进程调度 → 阻塞唤醒 → 内存回收”这个完整闭环讲清楚了。接下来按模块拆开讲每个部分都能直接照抄进你自己的课设。2. FCFS 作业调度与后备队列10 个作业怎么排队进内存2.1 作业调度和进程调度为什么必须分开很多同学会把作业调度和进程调度混成同一个东西这是第一个认知坎。作业调度也叫高级调度它决定的是“哪个作业能从磁盘后备队列进入内存”进程调度也叫低级调度它决定的是“内存里已经就绪的进程下一个谁上 CPU”。这份课设的流程是先用随机数生成 10 个作业放进后备队列作业调度采用 FCFS先来先服务每次从后备队列头部取一个作业但这个作业能不能进内存还要看内存里并发进程数是否达到上限。上限是 5 个。这里有个关键设定阻塞状态的进程依然占着内存也就是说就绪、运行、阻塞三种状态的进程数加起来最多是 5。我当初第一次看这个设定觉得奇怪后来想明白了——这是在模拟“内存空间被占用”的现实约束不是每个进程都能立刻进内存。所以作业调度触发时机有两个一是初始时内存为空按 FCFS 依次调入前 5 个作业二是某个进程运行结束后释放内存再从后备队列头部补一个进来。2.2 后备队列的数据结构与随机作业生成作业的数据结构直接决定后续所有模块怎么写。先看作业实体类的核心字段public class Job { private int jobId; // 作业编号 private int arrivalTime; // 到达时间 private int serviceTime; // 需要服务的总时间 private int memorySize; // 所需内存大小 private int status; // 作业状态0-后备 1-就绪 2-运行 3-阻塞 4-完成 private int remainingTime; // 剩余服务时间RR调度要用 private int memoryStart; // 在内存中的起始地址内存分配后写入 }随机生成 10 个作业的代码逻辑通常长这样Random random new Random(); ListJob backlogQueue new ArrayList(); for (int i 1; i 10; i) { Job job new Job(); job.setJobId(i); job.setArrivalTime(i); // 简化处理第i个作业第i时刻到达 job.setServiceTime(random.nextInt(20) 5); // 服务时间 5~24 job.setMemorySize(random.nextInt(80) 20); // 内存需求 20~99 job.setStatus(0); // 后备状态 backlogQueue.add(job); } // 按到达时间排序保证FCFS的队列顺序 backlogQueue.sort(Comparator.comparingInt(Job::getArrivalTime));参数说明serviceTime 范围设 5~24 是为了保证在时间片轮转下能观察到多次调度切换如果服务时间太短进程一两个时间片就跑完了轮转效果不明显。memorySize 范围 20~99 是配合内存总大小设计的如果内存总量是 512这个区间能保证产生多种分配情况偶尔出现碎片。arrivalTime 用 i 而不是随机值是为了让 FCFS 的顺序确定方便验证算法正确性——你不想在调试时分不清是调度算法错了还是随机数导致的现象不稳定。2.3 FCFS 调度主循环的写法作业调度的主循环和进程调度是嵌套关系外层循环负责从后备队列调作业进内存内层循环负责内存中进程的时间片轮转。核心逻辑while (!backlogQueue.isEmpty() || !runningProcesses.isEmpty()) { // 1. 检查内存中的进程是否结束结束则回收内存并尝试调入新作业 for (int i 0; i runningProcesses.size(); i) { Process proc runningProcesses.get(i); if (proc.getRemainingTime() 0) { memoryManager.release(proc); runningProcesses.remove(i); // 有进程结束从后备队列按FCFS调入新作业 if (!backlogQueue.isEmpty()) { Job nextJob backlogQueue.remove(0); loadJobToMemory(nextJob); } } } // 2. 时间片轮转调度内存中的进程 Process current getNextProcessByRR(runningProcesses); if (current ! null) { current.setStatus(2); // 运行 current.setRemainingTime(current.getRemainingTime() - timeSlice); memoryManager.allocate(current); // 3. 可能触发阻塞条件模拟I/O等待 if (shouldBlock(current)) { current.setStatus(3); // 阻塞 blockedQueue.add(current); } } }这里有个很关键的细节进程结束释放内存后不是立即从后备队列调入作业而是要先做内存回收、再触发调入。顺序不能反——先释放内存才能知道空闲分区有多大否则首次适应算法拿到的分区表是过期的。另外注意 loadJobToMemory 内部还要再做一次内存分配检查如果内存不够作业应该留在后备队列头部而不是丢弃这就是所谓的“重新排队”。2.4 作业调度模块的验证方法调试作业调度最容易翻车的地方是“调入时机”和“并发数控制”。我给个自查清单初始时是否正确调入了 5 个作业内存中进程结束一个后后备队列是否只补一个后备队列为空时循环是否还在跑内存里的进程作业调入时并发数是否严格不超过 5。建议在每次作业调入或调出时打印一行日志格式类似[调度] 作业3 调入内存当前并发数: 4/5这样一眼就能看出调度节奏对不对。3. 时间片轮转与进程状态流转阻塞是怎么插入调度周期的3.1 RR 调度中的就绪队列与阻塞队列划分时间片轮转算法的关键是你得维护的就绪队列顺序。这份课设里内存中的进程分成三态就绪、运行、阻塞。运行态在同一时刻只有一个就是当前拿到 CPU 时间片的进程阻塞态进程在等待 I/O 或某个事件不参与轮转就绪态排队等 CPU。我建议用两个队列分别维护就绪和阻塞而不是用一个状态字段硬扛否则轮转一圈回来你都不知道谁该被跳过。RR 调度器的核心实现public Process getNextProcessByRR(ListProcess processes) { // 按上次运行时间排序保证轮转顺序稳定 processes.sort((p1, p2) - { if (p1.getLastRunTime() ! p2.getLastRunTime()) { return p1.getLastRunTime() - p2.getLastRunTime(); } return p1.getProcessId() - p2.getProcessId(); }); for (Process p : processes) { if (p.getStatus() 1) { // 就绪 p.setLastRunTime(System.currentTimeMillis()); return p; } } return null; // 没有就绪进程时CPU空转 }注意这个排序逻辑lastRunTime 记录的是进程上一次被调度的时间用当前时间戳作为排序键能保证轮转公平性。但如果是纯控制台模拟用时间戳会让每次运行结果都不一样难以复现 bug。我实际调试时改成计数器自增——每轮调度全局计数器 1lastRunTime 记录的是第几轮这样日志可复现排查问题方便得多。这是课设里值得注意的一个细节。3.2 阻塞与唤醒的模拟策略阻塞在这份课设里是个重点它把进程调度和作业调度连接起来了。常见的模拟方式是给每个进程一个随机阻塞概率运行到某个时间点就进入阻塞阻塞若干个时间片后自动唤醒回到就绪队列private boolean shouldBlock(Process p) { // 运行了随机时长后以一定概率模拟I/O阻塞 if (p.getStatus() 2 p.getRunTicks() p.getBlockAfter()) { return random.nextInt(100) 30; // 30%概率阻塞 } return false; }唤醒逻辑我建议写在 RR 主循环的固定位置每轮调度先检查阻塞队列中是否有进程阻塞时间已到到了就唤醒并放入就绪队列尾部public void wakeUpBlockedProcesses() { IteratorProcess it blockedQueue.iterator(); while (it.hasNext()) { Process p it.next(); p.setBlockTime(p.getBlockTime() - 1); if (p.getBlockTime() 0) { p.setStatus(1); // 就绪 readyQueue.add(p); it.remove(); } } }这里有个隐藏坑唤醒的进程应该放到就绪队列尾部还是头部教材上通常说唤醒后进入就绪队列但没说队头还是队尾。按时间片轮转的公平性应该放尾部否则刚被唤醒的进程插队到老进程前面轮转就乱套了。我在这个点上栽过跟头日志里看到某个进程连续两次拿 CPU 才发现是唤醒位置写错了。3.3 总运行时间片与切换次数的统计课设报告里一般要求分析调度算法的性能指标。我在这份源码的进程类里加了一组统计字段首次运行时间、完成时间、等待时间、周转时间、带权周转时间。等待时间累加在调度主循环里for (Process p : runningProcesses) { if (p.getStatus() ! 2) { // 非运行态等待时间1 p.setWaitTime(p.getWaitTime() 1); } }等待时间的统计逻辑是每轮调度只给非运行态进程累加。注意阻塞时间算不算等待时间不同课设要求不一样——有的老师算有的不算。我建议单独统计阻塞等待时间和就绪等待时间报告里可以分开列。这样答辩时老师问“阻塞对性能的影响”你能直接拿出数据。4. 首次适应内存分配与回收空闲分区链的边界处理4.1 内存分区表的数据结构与 FF 算法实现内存管理模块要维护一个空闲分区表和一个已分配分区表。首次适应First Fit算法的核心是空闲分区按地址从小到大排序分配时从头扫描找到第一个足够大的分区就切割分配。代码实现public MemoryBlock allocate(int size) { for (int i 0; i freeBlocks.size(); i) { MemoryBlock block freeBlocks.get(i); if (block.getSize() size) { // 分配从空闲块中切出size大小 MemoryBlock allocated new MemoryBlock(block.getStart(), size); int remaining block.getSize() - size; if (remaining 0) { // 更新空闲块为剩余部分 block.setStart(block.getStart() size); block.setSize(remaining); } else { // 正好用完直接从空闲链表移除 freeBlocks.remove(i); } return allocated; } } return null; // 内存不足 }分区表用 ArrayList 保存空闲块每次分配后需要重新按起始地址排序。注意“正好用完”这个分支很多实现会忽略恰好相等的情况导致空闲链表里残留大小为 0 的空块后面合并时出现下标越界。这个边界问题你迟早会遇到不如现在就把 if/else 写完整。4.2 内存回收与相邻空闲分区合并进程结束时回收内存具体要做两件事把已分配块转换成空闲块加入空闲链表检查该块与相邻空闲块是否连续连续则合并。这里的相邻不只是物理相邻而是地址相邻具体实现public void release(MemoryBlock block) { freeBlocks.add(new MemoryBlock(block.getStart(), block.getSize())); // 按起始地址排序后检查相邻 freeBlocks.sort(Comparator.comparingInt(MemoryBlock::getStart)); for (int i 0; i freeBlocks.size() - 1; i) { MemoryBlock current freeBlocks.get(i); MemoryBlock next freeBlocks.get(i 1); if (current.getStart() current.getSize() next.getStart()) { // 合并 current.setSize(current.getSize() next.getSize()); freeBlocks.remove(i 1); i--; // 继续检查新的相邻关系 } } }合并逻辑里有个细节合并后需要把游标 i 减回去否则连续三个相邻空闲块合并时只处理了前两个第三个漏掉。这是内存回收最容易出的 bug——不是合并逻辑看不懂而是循环游标处理不到位。我调试时打印过每一轮的 freeBlocks 状态用地址连续性检查脚本对比才发现这个问题。4.3 首次适应相比最佳适应坏适应的取舍为什么这份课设选首次适应而不是最佳适应Best Fit首次适应的优点是分配速度快、偏向保留高地址大块空间缺点是低地址区域会积累小碎片。最佳适应相反分配时找最小够用块内存利用率高但速度慢且产生大量难以利用的微小碎片。对于课设场景首次适应实现简单、现象直观更不容易在答辩时被追问到死角。如果老师问“FF 的缺点是什么”你可以回答“低地址区域的碎片化”——这个回答能显示你真的理解而不是背代码。当然如果你学有余力我建议在源码里再加一个 Best Fit 的实现用 boolean 变量切换两种算法报告里对比分析碎片率。这属于加分项一般老师看到这个会说“这个同学有思考”。4.4 内存状态的可视化输出控制台程序怎么展示内存分配做一个简单的字符画内存映像图public void printMemoryStatus() { char[] mem new char[MEMORY_SIZE]; Arrays.fill(mem, .); // . 表示空闲 for (MemoryBlock block : allocatedBlocks) { for (int i block.getStart(); i block.getStart() block.getSize(); i) { mem[i] #; } } System.out.println(new String(mem)); }输出一行 512 个字符的二进制图一眼就能看出内存碎片的情况。这个输出放在每次进程调入调出之后。对照着看首次适应算法下低地址区域的碎片化累积过程非常直观——你会发现碎片越来越多新的作业越来越难找到大的连续空闲区。这也是报告里最有说服力的截图素材。5. 实操避坑从参数设置到并发控制这 7 个坑我已经替你踩过了5.1 并发进程数超过 5 个作业提前调入导致状态错乱现象控制台输出里出现了 6 个甚至更多进程同时在内存中运行几个时间片后数据开始乱跳。原因作业调入内存的检查条件写错位置。常见错误是把并发数检查写在 FCFS 调度器的开头而不是每次调入前都检查。还有一种情况是进程结束回收内存后忘了把进程从 runningProcesses 里移除导致已释放内存的进程还在占用名额。解决在 loadJobToMemory 方法入口处强制校验if (runningProcesses.size() MAX_CONCURRENCY) return;同时进程结束回收时必须同时从 runningProcesses、memoryManager 的已分配表两个地方移除。我建议把并发数的检查收口到一个方法里所有调入逻辑都走这个方法不旁路。5.2 时间片轮转出现连续两个时间片分配给同一进程现象日志显示进程 A 运行完一个时间片后没切走下一个时间片还是 A。原因调度器在选择下一个进程时把当前运行进程的状态改成了就绪但排序时 lastRunTime 没有更新导致按时间戳排序后它还是排在最前面又被打回运行态。解决进程被调度器选中时立刻更新 lastRunTime然后在每次调度前把运行态的进程改为就绪态。如果是用计数器而不是真实时间戳要保证计数器在每次选择前自增。这个坑排查起来非常隐蔽因为偶发情况下看不出来多跑几个时间片就会发现某个进程饥饿了。5.3 进程结束内存回收后空闲分区没有合并碎片越来越多现象内存中空闲空间总值足够但连续空闲空间很少新作业调入时总是“内存不足”。原因回收逻辑里只添加了新空闲块没有触发相邻合并或者合并循环里游标处理有问题连续三个相邻空闲块只合并了前两个。解决每次 release 后都强制走一遍完整的合并循环并且考虑把相邻合并提取成公共方法分配和回收时都可以调用。验证方法连续让多个进程结束后打印空闲分区表检查所有空闲块的起始地址是否严格递增、是否还存在相邻块未合并。我在 4.2 节里写的 i-- 那行就是从坑里爬出来后补上的。5.4 就绪队列为空时 CPU 空转的无限循环现象所有进程都进入阻塞态但程序还在空转控制台刷屏飞快。原因RR 调度器返回 null 时主循环没有处理这种情况还是继续执行导致空转死循环。解决调度器返回 null 时直接 continue同时每次循环至少执行一次阻塞队列的检查——因为只有阻塞进程的阻塞时间递减才能唤醒进程。更稳妥的做法是加一个“全阻塞退出”判断如果就绪队列为空且阻塞队列也即将为空说明所有进程都完成了跳出主循环。5.5 随机数种子不固定同一个 Bug 没办法复现现象第一次跑崩了第二次跑又正常没法定位问题。原因每次运行 random 生成的作业参数不同操作系统课设不像 Web 开发那样输入确定随机的数据让排查难上加难。解决给 Random 构造器传入固定种子比如new Random(42)每次跑出来的 10 个作业完全相同。调试定位完问题后再改成不传种子的版本观察多组数据的表现。这个方法只对调试有用最终提交的版本建议用系统时间做种子否则答辩老师发现每次都是同一组数据会怀疑你的代码是不是写死了。5.6 阻塞时间参数设置不合理进程永远醒不过来现象某个进程进入阻塞队列后再也没被唤醒其他进程正常轮转系统整体速度缓慢。原因阻塞时间设置得太长或者唤醒检查只在特定条件下执行阻塞进程被漏掉。常见是把阻塞时间设为固定大数或者唤醒逻辑写在“就绪队列非空”的分支里阻塞队列一旦为空就跳过检查。解决把阻塞队列的检查放到主循环的最前面无论就绪队列是否为空每轮都先检查阻塞进程的剩余时间。阻塞时间建议用random.nextInt(3) 11~3 个时间片就够了太长会导致进程长时间不能参与调度太短可能进程运行两下又醒了看不出阻塞现象。5.7 进程 ID 混乱回收后 ID 复用引发统计错误现象报告里的周转时间统计数据明显不对某个进程的等待时间变成了负数。原因进程结束后从队列移除但新的作业调入时沿用了同样的编号统计数组里的历史数据被覆盖。解决作业 ID 从后备队列生成时保持唯一进程结束只标记为完成状态不重新分配 ID。如果想把 ID 复用需要在复用时检查该 ID 的历史统计是否已归档。最省事的方式是进程对象不复用结束就丢弃新作业进内存时新建进程对象这个对象与原作业共享 jobId 但统计字段独立。这是我自己在写报告时加统计功能踩到的坑数据对不上特别耗时。6. 从模块拼装到完整复现一份可以照用的验证清单和调试套路课设拿到手先不要急着跑我建议按下面这个顺序走一遍20 分钟内能确认每个模块工作正常。第一步打开 README看清楚它定义的运行入口和参数常量——内存总大小、时间片长度、最大并发数这三个值决定了整个模拟的行为。如果时间片设得太大RR 退化成了 FCFS如果内存总大小设得太小10 个作业可能一个都调不进去。第二步把随机数种子固定跑一次完整流程记录每一步的控制台输出。第三步手动算一遍前 2~3 个作业的流转过程和控制台输出对照确认调度顺序没错。验证清单我整理成这样作业调度是否严格 FCFS即后备队列头部永远是最先到达的未调入作业并发进程数峰值是否≤5RR 轮转顺序是否按就绪队列从头到尾循环阻塞进程是否不参与轮转阻塞唤醒后是否进入就绪队列尾部内存回收后空闲分区表中无相邻可合并块所有进程结束后后备队列为空、内存全部空闲。每一条都可以用一条日志输出对一次不用猜。我在按这份课设改写成自己的版本时把上面的每条验证做成了一个checkStatus()方法跑完一步就自动检查一遍。这套思路做完之后它成了我每次操作系统实验的通用模板后来做存储管理实验时换了一组数据就继续用。复盘这套课设我感觉最重要的收获不是某个算法本身而是理解了“并发上限为 5 个进程”这个约束是如何把作业调度、内存分配、进程调度串成闭环的——没有这个上限作业调度和进程调度就是两套独立的演示代码有了它每个模块之间才真正耦合起来。从那以后我每次拿到一个仿真的课设都先画出“状态流转图 资源约束条件”再动手写代码这个习惯让我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站