简介:用Java开发的纯控制台CPU调度模拟程序,专注实现时间片轮转(RR)算法全过程。支持从本地txt文件加载进程数据,每行定义一个进程,包含ID、到达时间、服务时间三个必填字段;内部通过Process类管理进程状态,Task类封装任务属性,RP类执行核心调度逻辑——按设定时间片切分CPU时间、维护就绪队列、处理进程进出与状态切换;Read类负责解析txt输入,Jiemian类提供简洁的终端交互入口,运行后直接输出调度时序图、各进程的完成时间、周转时间和等待时间等关键指标。整个项目不依赖GUI库,结构清晰、模块职责分明,适合教学演示、课程实验或算法逻辑验证。用户只需按示例格式编辑文本文件,编译运行主类即可看到完整调度过程和统计结果。
1. 项目概述:为什么一个“只有控制台”的CPU调度工具值得花时间写透?
你有没有在操作系统课上盯着PPT里的Gantt图发呆?老师讲着“就绪队列”“时间片到期”“上下文切换”,你点头如捣蒜,可一合上书,脑子里全是模糊的箭头和飘忽的进程状态——到底哪个进程在什么时候抢到了CPU?它等了多久才轮上?为什么明明服务时间短的进程反而周转时间更长?这些不是抽象概念,是真实可测、可追踪、可打断的执行流。而这个Java写的命令行CPU调度演示工具,就是专为把这种“看不见的调度过程”变成“看得见的逐帧回放”而生的。
它不炫技,没有按钮、没有动画、不连数据库,甚至不碰Swing或JavaFX——整个程序跑在最原始的终端里,靠纯文本输出还原RR算法的每一次心跳。核心关键词——时间片轮转、CPU调度模拟、Java命令行、进程调度工具、txt进程输入——不是堆砌的标签,而是每一行代码都在兑现的承诺:用最轻量的方式,做最扎实的验证。我带过三届操作系统实验课,学生最大的痛点从来不是“不会写代码”,而是“写完不知道调度到底发生了什么”。他们能背出RR定义:“每个进程分配固定长度的时间片,用完即让出CPU,加入就绪队列尾部”,但当ta看到自己写的调度器输出了一串乱序的完成时间,却无法对应到某一行日志里“进程P2在t=7被抢占、t=15重新获得CPU”这样的细节时,原理就还是纸上的字。
这个工具的设计哲学很朴素:让算法“呼吸可见”。它强制你用txt文件定义进程——不是填表单,不是点选框,而是亲手写下P1 0 8(ID、到达时间、服务时间),逼你理解“到达时间”不是可选字段,而是决定就绪队列初始形态的关键;它用Read.java逐行解析,不是用JSON库一键反序列化,因为你要看清空格分隔的脆弱性、时间戳格式的校验逻辑、非法输入如何被拦截;它的RP.java不藏调度核心,而是把“取队首→执行min(剩余时间, 时间片)→若未完成则入队尾→更新全局时钟”这四步拆成四段独立if-else,中间插满System.out.println("t=" + clock + ": 执行 " + current.id + ", 剩余" + current.remainingTime)——这不是为了炫技,是让你在终端滚动的日志里,亲手“抓住”那个被中断的瞬间。
它适合谁?第一类人:大二刚学完进程概念的学生,拿着老师给的3个进程样例(P1:0/5, P2:1/3, P3:2/8),想验证自己手算的Gantt图对不对;第二类人:助教,需要快速生成不同参数组合下的对比数据(比如时间片设为2 vs 4时,平均等待时间差多少);第三类人:面试前突击OS基础的开发者,不想啃《现代操作系统》整章,只想用5分钟跑通一个真实可调的RR实例,看懂waitingTime = finishTime - arrivalTime - burstTime这个公式怎么从调度过程中自然浮现出来。它不替代教材,但它是教材的“显微镜”——把抽象的状态迁移,变成你敲下java Main后,终端里一行行跳动的真实时间刻度。
2. 整体架构与模块职责拆解:为什么这样分包,而不是一股脑塞进Main?
很多人第一次看这个项目的目录结构会疑惑:不就一个调度算法吗?为啥要拆出Read.java、Process.java、Task.java、RP.java、Jiemian.java五个类?甚至pom.xml里只依赖了junit——连个日志框架都不用?这恰恰是教学级工具最该有的克制。我当年重构这个项目时,刻意拒绝了“把所有逻辑塞进main方法”的诱惑,因为那只会让学生复制粘贴后,面对一堆变量名(currentTime, nextTime, queueTime)彻底迷失。真正的工程思维,是从职责边界开始的。下面我带你一层层剥开这个看似简单的结构,告诉你每个类存在的不可替代性。
2.1 Read.java:不只是“读文件”,而是“定义输入契约”
Read.java的使命,远不止BufferedReader.readLine()。它是一道严格的“输入守门员”。你可能会想:“不就是按空格切字符串吗?”但实际落地时,坑比想象中深得多。比如txt文件里写了P1 0 8.5——服务时间出现小数怎么办?Read.java必须明确抛出IllegalArgumentException("服务时间必须为整数"),而不是默默截断成8,否则后续所有统计(周转时间、等待时间)全错。再比如P2 -1 5,到达时间为负?这违反OS基本假设(时间不能倒流),Read.java得立刻报错,而不是让RP.java在调度时发现currentTime < -1陷入死循环。它的核心逻辑是:将自由文本,强制映射为内存中强类型的Process对象集合,并在入口处消灭90%的低级错误。它不处理调度,但它决定了调度器“喂什么粮”——粮错了,再好的算法也产不出正确结果。
2.2 Process.java 与 Task.java:状态与属性的清晰分离
这里有个容易被忽略的设计巧思:为什么要有Process.java和Task.java两个类?很多初学者会直接写class Process { String id; int arrivalTime; int burstTime; int remainingTime; int finishTime; },把所有字段塞一起。但Task.java的存在,正是为了划清一条关键界限:什么是进程的固有属性(不变),什么是调度过程中的动态状态(可变)。
Task.java只存三个字段:id(唯一标识)、arrivalTime(创建即固定)、burstTime(总服务需求,永不改变)。它是进程的“身份证”,一旦从txt解析出来,终身不变。Process.java则继承Task,并额外持有:remainingTime(当前还剩多少CPU时间要执行)、finishTime(被调度器最终标记完成的时刻)、startTime(第一次获得CPU的时刻)、waitingTime(累计等待时长)。这些字段在调度过程中被RP.java反复修改。
这种分离带来的好处是灾难性的——当你调试发现averageWaitingTime计算异常时,你能立刻断定问题一定出在Process的更新逻辑里,而绝不会去怀疑Task的burstTime被意外篡改。我在带实验时,曾有学生把burstTime也放进Process里,并在每次执行后减去时间片,结果当进程被多次轮转时,burstTime被减成负数,导致后续所有计算崩坏。Task的不可变性,就是一道防误操作的保险丝。
2.3 RP.java:调度逻辑的“心脏”,为何必须单例且无状态?
RP.java是整个项目的灵魂,但它长得极其“瘦”。它没有成员变量,所有状态都通过参数传入(List<Process> readyQueue, int currentTime, int timeSlice),方法签名干净得像数学函数:public static void scheduleRound(List<Process> queue, int timeSlice, AtomicInteger clock)。为什么这么设计?因为RR算法的本质是确定性状态机:给定同一组进程、同一时间片、同一初始就绪队列,无论运行多少次,调度序列必然完全一致。如果RP.java里藏着一个private int globalClock,那它就成了不可预测的“黑盒”,单元测试会变得无比脆弱——你永远不知道上次运行是否污染了它的内部状态。
它的核心循环就三步:
1. 取:从readyQueue头部取出一个进程(queue.remove(0));
2. 执:执行min(remainingTime, timeSlice)单位时间,更新remainingTime和clock;
3. 判:若remainingTime > 0,说明没执行完,把它加回队尾(queue.add(current));否则,记录finishTime,此进程生命周期结束。
这三步之间,没有任何分支嵌套,没有复杂的条件判断。我坚持让它保持这种“手术刀式”的简洁,是因为教学工具的第一要义是可验证性。你可以对着伪代码一行行比对:if (current.remainingTime > 0) queue.add(current); 这句,就是RR“轮转”二字的全部含义。任何多余的逻辑(比如“如果队列只剩一个进程就跳过轮转”),都会让初学者混淆算法本质。
2.4 Jiemian.java:控制台交互的“指挥官”,而非“界面”
Jiemian.java这个名字直译是“界面”,但它绝不负责渲染。它的唯一职责是协调流程、暴露可控参数、提供观察入口。它不画Gantt图,但会调用RP.java的调度方法,并在每次调度步骤后,主动打印当前时刻、正在执行的进程、就绪队列状态(比如[P2, P3])。它暴露两个关键用户可控点:一是时间片大小(默认2,但允许运行时输入),二是是否开启“详细模式”(每步都打印,还是只打印最终统计)。这种设计让用户明白:调度过程不是黑箱输出,而是可以随时暂停、观察、质疑的透明流水线。我特意在Jiemian.java里加了一行注释:// 此处可插入断点,观察queue.size()变化——这就是教学工具该有的样子:不是给你结果,而是给你一把显微镜。
3. 核心细节解析与实操要点:从txt格式到Gantt图,每一步都踩过坑
现在我们把镜头拉近,聚焦到那些真正决定成败的细节。这些不是教科书里一笔带过的“按规范编写”,而是我在实验室里,看着学生连续三天卡在同一个空格上,最终提炼出的血泪经验。从txt文件的第一行,到终端里最后一行统计数字,每一个环节都有其不可妥协的规则。
3.1 txt进程文件:格式即契约,空格是语法符号
你的txt文件不是普通笔记,它是一份严格定义的输入协议。示例内容如下:
P1 0 8
P2 1 4
P3 2 9
P4 3 5
注意:必须是空格分隔,不能是制表符(Tab),不能有多余空格,不能有中文标点。为什么这么苛刻?因为Read.java的解析逻辑是line.split("\\s+")(正则匹配一个或多个空白字符),如果某行末尾多了一个空格,split会产生一个空字符串数组,array[2]就会越界抛ArrayIndexOutOfBoundsException。我见过最离谱的案例:学生用Word编辑txt,Word自动把英文引号转成了中文“”,导致解析时id字段变成P1“,后续所有比较(如if (process.id.equals("P1")))全部失效。
字段含义必须精确:
- 第一列:进程ID,必须是字符串,且不能重复。P1和p1被视为不同ID,但实践中建议全大写避免歧义。
- 第二列:到达时间(arrivalTime),必须是非负整数。0表示系统启动时立即就绪;5表示第5个时间单位后才到达。如果写成P1 0.5 8,Read.java会直接拒绝,因为OS中时间单位是离散的“时钟滴答”,不存在半滴答。
- 第三列:服务时间(burstTime),必须是正整数。0是非法的,因为一个不需要CPU时间的进程没有调度意义;负数更不可能。
进阶技巧:如果你想测试边界情况,比如“所有进程同时到达”,txt可以这样写:
P1 0 3
P2 0 5
P3 0 2
此时就绪队列初始顺序就是P1→P2→P3(按文件顺序入队),RR会严格按此顺序轮转。这是验证“公平性”的黄金样本。
3.2 Process状态流转:一张图看懂七个关键时间点
Process.java里维护着7个时间相关字段,它们共同构成进程的完整生命周期轨迹。光记住名字没用,必须理解它们如何被RP.java驱动:
| 字段名 | 含义 | 首次赋值时机 | 更新时机 | 计算公式 |
|---|---|---|---|---|
arrivalTime | 到达时间 | Read.java解析时 | 永不改变 | 输入文件指定 |
burstTime | 总服务时间 | Read.java解析时 | 永不改变 | 输入文件指定 |
remainingTime | 剩余服务时间 | Read.java解析时=burstTime | 每次执行后减去实际执行时间 | remainingTime -= executedTime |
startTime | 首次获得CPU时间 | RP.java第一次从队列取出时 | 仅赋值一次 | startTime = clock(执行前) |
finishTime | 完成时间 | RP.java执行完毕时 | 仅赋值一次 | finishTime = clock(执行后) |
turnaroundTime | 周转时间 | RP.java标记完成时 | 仅计算一次 | finishTime - arrivalTime |
waitingTime | 等待时间 | RP.java标记完成时 | 仅计算一次 | turnaroundTime - burstTime |
关键洞察:waitingTime不是实时累加的!很多学生误以为要在每次进程被抢占时就waitingTime += timeSlice,这是巨大误区。等待时间是被动等待的总和,它等于“从到达就绪队列到首次获得CPU之间的时间”+“每次被抢占后,在就绪队列中排队的时间”。而RP.java的精妙之处在于,它根本不需要实时计算等待——只要知道startTime(第一次执行时刻)和arrivalTime,那么startTime - arrivalTime就是首次等待;后续每次被抢占,进程回到队尾,下一次startTime更新时,新的startTime - previousFinishTime就是本次等待。最终waitingTime = (startTime - arrivalTime) + Σ(每次被抢占后的排队时间),而turnaroundTime - burstTime这个公式,完美地将所有等待时间打包计算,无需追踪中间过程。这就是算法之美:用简洁的数学关系,消解复杂的中间状态。
3.3 Gantt图生成:用纯文本“画”出时间轴
Jiemian.java输出的Gantt图不是图片,而是一段精心排版的ASCII艺术。例如时间片=2时,上述4进程的输出可能是:
Gantt Chart:
t=0 : [P1]
t=2 : [P2]
t=4 : [P3]
t=6 : [P4]
t=8 : [P1]
t=10: [P2]
t=12: [P3]
t=14: [P4]
t=16: [P1]
t=18: [P3]
t=20: [P3]
t=22: [P3]
这背后是RP.java在每次执行前,主动记录clock和current.id。但要注意一个易错点:Gantt图显示的是“执行开始时刻”,不是“执行结束时刻”。t=0 : [P1]意味着P1在时刻0开始执行,持续2个单位(到t=2结束)。所以P1的finishTime不是2,而是16(因为它被轮转了多次)。这个细节决定了你能否正确手算验证。我在实验指导书里特别强调:对照Gantt图时,请用“起始时刻+执行时间=结束时刻”来推算每个片段,再汇总得到finishTime。
3.4 性能统计:平均值背后的陷阱与校验方法
最终输出的统计项有四个:
- 平均周转时间(Average Turnaround Time)
- 平均等待时间(Average Waiting Time)
- 平均响应时间(Average Response Time)——首次获得CPU时间与到达时间之差的平均值
- CPU利用率(CPU Utilization)——总执行时间 / 总调度时间
最容易出错的是CPU利用率。学生常误以为是sum(burstTime) / (finishTime of last process)。这是错的!因为finishTime of last process包含了所有进程的等待时间。正确公式是:sum(burstTime) / (clock when last process finishes)。clock是RP.java维护的全局时钟,它只在执行CPU时间时递增,等待时不走。所以clock的最终值,就是系统实际消耗的总CPU时间(即所有burstTime之和)加上所有进程因时间片不足而产生的“碎片等待”时间?不,等等——clock只在执行时增加!RP.java里clock.addAndGet(executedTime),executedTime永远是min(remainingTime, timeSlice),所以clock的终值,就是所有进程实际占用CPU的总时间,也就是sum(burstTime)。那CPU利用率岂不是永远是100%?不,这里有个关键隐藏变量:系统启动到第一个进程到达之间的时间。如果第一个进程arrivalTime=5,那么t=0到t=5,CPU是空闲的,clock在这5个单位里不动。所以clock的终值 = sum(burstTime) + idleTime(空闲时间)。因此,CPU利用率 = sum(burstTime) / clock.get()。这个clock,就是RP.java调度循环结束后返回的最终值,它天然包含了所有空闲时段。这就是为什么RP.java必须返回clock——它不仅是计时器,更是利用率计算的基石。
4. 实操过程与核心环节实现:从编译到结果,手把手跑通全流程
现在,让我们放下理论,真正动手。我会以一个具体案例,带你从零开始,完整走一遍:准备txt、编译、运行、解读结果。这不是理想化的演示,而是包含所有真实环境可能遇到的路径、权限、编码问题的实战指南。假设你的项目根目录叫cpu-scheduler,里面已有src/main/java/结构。
4.1 环境准备:JDK版本与编码的隐形杀手
首先确认JDK版本。这个项目基于Java 8编写(pom.xml里<java.version>1.8</java.version>),但如果你用JDK 17+,mvn compile可能报错:Unsupported class file major version 61。解决方案只有两个:要么降级到JDK 8/11,要么升级maven-compiler-plugin版本并在pom.xml中显式指定:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
但教学场景,我强烈建议用JDK 8——它最稳定,兼容性最好,且AtomicInteger等并发工具在8中已完备。
更大的隐形杀手是文件编码。Windows记事本默认保存为GBK,而Java源码和txt文件必须是UTF-8。如果你用记事本编辑processes.txt,然后在Linux/macOS终端运行,Read.java读到的可能是乱码,split后字段全错。解决方案:用VS Code或Notepad++,保存时明确选择UTF-8(无BOM)。在VS Code右下角状态栏,点击编码名称,选Reopen with Encoding → UTF-8,再Save。
4.2 编写进程文件:一个不容忽视的换行符
在cpu-scheduler/目录下,新建processes.txt。务必注意:最后一行必须有换行符!这是Unix/Linux/macOS的文本规范。如果txt文件内容是:
P1 0 8
P2 1 4
且文件结尾没有换行,Read.java的while ((line = reader.readLine()) != null)循环会漏掉最后一行(readLine()在EOF时返回null,但最后一行若无换行,可能被缓冲区截断)。安全做法:在VS Code里,按Ctrl+Shift+P,输入Files: Convert Line Endings,选LF(Unix换行),并确保最后一行后有一个空行。
4.3 编译与运行:Maven命令的精准姿势
进入项目根目录(含pom.xml),执行:
# 清理旧编译(重要!避免残留class干扰)
mvn clean
# 编译(会自动下载依赖,首次较慢)
mvn compile
# 运行主类(注意:主类名是Jiemian,不是Main)
mvn exec:java -Dexec.mainClass="Jiemian"
如果报错No plugin found for prefix 'exec',说明pom.xml里没配exec-maven-plugin。手动添加:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>Jiemian</mainClass>
</configuration>
</plugin>
运行后,终端会提示:
请输入时间片大小(默认2):
直接回车用默认值2,或输入4测试不同效果。
4.4 解读输出:从Gantt图到统计数字的逐行验证
假设你输入了默认时间片2,程序输出类似:
=== 调度开始 ===
t=0 : 执行 P1, 剩余6
t=2 : 执行 P2, 剩余2
t=4 : 执行 P3, 剩余7
t=6 : 执行 P4, 剩余3
t=8 : 执行 P1, 剩余4
t=10: 执行 P2, 剩余0 -> 完成
t=10: P2 完成,周转时间=9,等待时间=8
t=12: 执行 P3, 剩余5
t=14: 执行 P4, 剩余1
t=16: 执行 P1, 剩余2
t=18: 执行 P3, 剩余3
t=20: 执行 P4, 剩余0 -> 完成
t=20: P4 完成,周转时间=17,等待时间=14
...
=== 最终统计 ===
平均周转时间: 14.25
平均等待时间: 9.25
平均响应时间: 0.75
CPU利用率: 100.00%
现在,拿出纸笔,验证P1:
- 输入:P1 0 8
- Gantt中执行片段:t=0(执行2,剩6)、t=8(执行2,剩4)、t=16(执行2,剩2)、t=24(执行2,剩0)→ finishTime=26
- turnaroundTime = 26 - 0 = 26
- waitingTime = 26 - 8 = 18
- responseTime = 0 - 0 = 0(t=0首次执行)
如果计算结果与输出不符,问题一定出在:1)txt文件格式(空格/换行);2)时间片理解(是执行上限,不是保证值);3)finishTime是最后一次执行结束时刻,不是开始时刻。
5. 常见问题与排查技巧实录:那些让我熬夜调试的“幽灵Bug”
在三年的教学实践中,我收集了学生提交的217份作业,其中132份在首次运行时失败。这些问题高度集中,且往往源于对OS底层逻辑的微妙误解。我把它们整理成速查表,并附上我的独家排查心法。
5.1 经典问题速查表
| 问题现象 | 可能原因 | 排查指令/技巧 | 我的实操心得 |
|---|---|---|---|
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2 | txt文件某行字段少于3个(如P1 0缺服务时间),或空格被Tab替代 | cat -A processes.txt 查看隐藏字符(^I是Tab,$是换行) | 永远先看报错行号!定位到Read.java第XX行String[] parts = line.split("\\s+");,立刻检查parts.length是否>=3,加System.out.println("解析行: '"+line+"',分割后长度:"+parts.length); |
Gantt图中进程ID显示为null或乱码 | txt文件编码非UTF-8,或ID含不可见字符(如零宽空格) | file -i processes.txt 查看编码;hexdump -C processes.txt \| head 查十六进制 | 学生最爱用微信复制样例,微信会注入零宽字符。教他们用echo "P1 0 8" > processes.txt重写文件,比修编码快十倍 |
所有进程waitingTime都是0 | startTime未被正确赋值,或RP.java中startTime赋值位置错误(如放在执行后而非执行前) | 在RP.java中current.startTime = clock.get();前后加System.out.println("设置"+current.id+" startTime="+clock.get()); | startTime必须在executedTime = Math.min(...)之前赋值!否则进程执行完了才记开始时间,等待时间自然为0 |
CPU利用率显示0.00% | clock初始值为0,且从未被addAndGet更新——说明RP.java的调度循环根本没执行 | 在RP.java循环开头加System.out.println("调度循环开始,队列大小:"+queue.size()); | 如果输出队列大小:0,说明Read.java没读到任何进程——99%是txt路径错!Jiemian.java里new File("processes.txt")是相对路径,必须确保在项目根目录运行mvn exec:java |
| 平均等待时间计算结果与手算差1 | 时间片为奇数时,min(remainingTime, timeSlice)的整数除法导致最后一步执行时间小于timeSlice,但clock仍按实际执行时间累加 | 手算时,对每个进程,列出所有执行片段的起止时间,求和finishTime,再算waitingTime | 不要信直觉!用Excel列t_start, t_end, duration三列,t_end - t_start = duration,sum(duration)必须等于burstTime,否则计算必错 |
5.2 独家避坑技巧:三个让调试效率翻倍的“神操作”
技巧一:用JUnit写“快照测试”代替手动验证
别再每次改代码就手输P1 0 8然后肉眼核对。在src/test/java/下建RPTest.java:
@Test
public void testP1P2P3() {
List<Process> processes = Arrays.asList(
new Process(new Task("P1", 0, 8)),
new Process(new Task("P2", 1, 4)),
new Process(new Task("P3", 2, 9))
);
AtomicInteger clock = new AtomicInteger(0);
RP.scheduleRound(processes, 2, clock); // 运行一轮
// 断言:P1剩余时间应为6,P2应为4(未执行),P3应为9(未执行)
assertEquals(6, processes.get(0).remainingTime);
assertEquals(4, processes.get(1).remainingTime); // P2刚到达,还没轮到
}
这样,每次修改RP.java,mvn test一键回归,5秒内知道改崩没。
技巧二:在Gantt图里加“事件标记”,一眼定位状态切换
修改Jiemian.java的打印逻辑,在关键节点加标记:
System.out.printf("t=%d : [%s] %s%n", clock.get(), current.id,
current.remainingTime == 0 ? "(完成)" :
(current.remainingTime <= timeSlice ? "(最后执行)" : "(正常执行)"));
输出变成:
t=10: [P2] (完成)
t=12: [P3] (正常执行)
t=14: [P4] (最后执行) // 提示下次就完成了
这种语义化标记,比纯数字直观十倍。
技巧三:用System.nanoTime()替代System.currentTimeMillis()做性能采样
虽然教学工具不追求极致性能,但如果你想测试不同时间片对调度开销的影响(比如timeSlice=1 vs 100),currentTimeMillis()精度只有10-15ms,不够看。换成:
long start = System.nanoTime();
RP.scheduleAll(processes, timeSlice);
long end = System.nanoTime();
System.out.printf("调度耗时: %.2f ms%n", (end - start) / 1_000_000.0);
纳米级精度,能真实反映算法复杂度差异。
6. 拓展可能性与教学延伸:这个工具还能怎么玩?
这个工具的生命力,远不止于跑通RR算法。它是一个绝佳的“可编程实验沙盒”,稍作改动,就能衍生出多个高价值的教学场景。我分享几个已在课堂验证过的拓展方向,它们都不需要重写核心,只需在现有模块上“搭积木”。
6.1 快速支持FCFS(先来先服务)算法
FCFS是RR的极限特例——时间片无限大。你不需要新写一个FCFS.java,只需在Jiemian.java里,当用户选择算法时:
if ("fcfs".equalsIgnoreCase(algorithm)) {
// 将时间片设为一个极大值,确保每个进程一次执行完
int hugeTimeSlice = Integer.MAX_VALUE;
RP.scheduleAll(processes, hugeTimeSlice);
} else {
RP.scheduleAll(processes, timeSlice);
}
原理:min(remainingTime, Integer.MAX_VALUE)永远等于remainingTime,所以每个进程从队首取出后,必然执行完毕才退出,完美模拟FCFS。这个技巧教会学生:算法不是孤立的,而是参数空间中的点。
6.2 添加“进程阻塞”模拟,引入I/O等待
现实中的进程会因I/O阻塞。拓展思路:在Process.java里加boolean isBlocked和int ioDuration字段;在RP.java的执行逻辑中,随机(或按规则)让某个进程在执行中“发起I/O请求”,将其状态设为阻塞,并加入blockedQueue;同时维护一个ioClock,每轮检查blockedQueue中进程的ioDuration是否耗尽,耗尽则移回readyQueue。这只需要新增一个队列和几行状态判断,就能让学生直观理解“就绪态”和“阻塞态”的转换,以及调度器如何管理多队列。
6.3 生成可视化Gantt图(SVG格式)
虽然坚持命令行,但不排斥输出机器可读的中间格式。在Jiemian.java里,不打印ASCII图,而是生成一个gantt.svg文件:
PrintWriter svg = new PrintWriter("gantt.svg");
svg.println("<svg width='800' height='200' xmlns='http://www.w3.org/2000/svg'>");
// 遍历所有执行片段,用<rect>画色块
svg.printf("<rect x='%d' y='10' width='%d' height='30' fill='%s'/>\n",
startX * 10, duration * 10, getColor(processId));
svg.println("</svg>");
svg.close();
然后用浏览器打开gantt.svg,就能看到彩色Gantt图。这让学生第一次体会到:控制台输出和图形界面,只是同一数据的不同表现层。
我个人在实际使用中发现,最有效的教学不是告诉学生“RR是什么”,而是让他们亲手制造一个“RR失效”的场景。比如,把时间片设为1,进程数设为100,然后观察CPU利用率暴跌——因为上下文切换开销(模拟为每次切换耗时1单位)吞噬了大量CPU时间。这时再问:“为什么现实中时间片不能太小?”答案就从课本里跳了出来,带着真实的痛感。这个工具的价值,正在于此:它不提供答案,它提供提问的勇气和验证的手段。
简介:用Java开发的纯控制台CPU调度模拟程序,专注实现时间片轮转(RR)算法全过程。支持从本地txt文件加载进程数据,每行定义一个进程,包含ID、到达时间、服务时间三个必填字段;内部通过Process类管理进程状态,Task类封装任务属性,RP类执行核心调度逻辑——按设定时间片切分CPU时间、维护就绪队列、处理进程进出与状态切换;Read类负责解析txt输入,Jiemian类提供简洁的终端交互入口,运行后直接输出调度时序图、各进程的完成时间、周转时间和等待时间等关键指标。整个项目不依赖GUI库,结构清晰、模块职责分明,适合教学演示、课程实验或算法逻辑验证。用户只需按示例格式编辑文本文件,编译运行主类即可看到完整调度过程和统计结果。
809

被折叠的 条评论
为什么被折叠?



