简介:华南理工大学操作系统(含课程设计)随堂练习PDF聚焦操作系统引论章节,适合正在学习操作系统基础课程的本科生、备考者及需要梳理核心概念的读者。资源以单份PDF文件形式提供,压缩包内含1个PDF,整体大小仅38KB,轻量便携,便于在手机或电脑上随时查看。当前已有117人学习浏览,内容覆盖操作系统的基本概念、类型、历史发展、组成、功能、设计实现及应用趋势等摘要信息,并收录第1章引论的13道选择题,涉及实时操作系统处理外部事件的时限、操作系统的管理对象、虚拟计算机的定义、多道程序设计提高CPU与外部设备利用率、并发性的时间特征等高频考点,每题均附参考答案,方便自测和对照纠错。整体而言,这份材料精炼浓缩了操作系统入门阶段的重要知识点,可作为课堂笔记的补充或考前冲刺的速刷题库。
1. 华南理工大学操作系统(含课程设计)随堂练习.pdf:不是刷题册,是浓缩考纲
拿到「华南理工大学操作系统(含课程设计)随堂练习.pdf」这份文件的人,大概分两种:离考试还有两周、想靠它圈重点的在校生;准备课程设计、想从里面刨项目思路的动手派。我的看法很直接:这份随堂练习的价值不在刷完就稳,而在它把操作系统最核心的五大模块——进程管理、内存管理、文件系统、设备管理和死锁——压缩成了可以逐题验证的题目形态。跟着它过一遍,等于把六百页教材读薄成一张能动手的考纲。操作系统的任务说到底就是管好 CPU、内存和 I/O,而这份练习恰好用计算题和设计题把「怎么管」问了一遍。下面这套路径,就是我带学生复习和做课设时实际走的那套,从拆题到写代码,再到答辩前怎么用它自检。
2. 拿到随堂练习先做三件事:题型拆解、考点映射和知识盲区清单
随堂练习和课后习题最大的区别是,它是老师讲完马上要你反应的题,答案往往就在最近一次课的板书里。所以它比课后题更接近考点,也更零散。我一般不会拿到就从头刷到尾,而是先干三件事:归类、映射、立习惯。做完这三步,这份 PDF 才真正变成你自己的操作系统笔记。
2.1 先按五大模块给练习题归类,别按页码刷
把一份操作系统随堂练习拆开看,题目其实就落在五个筐里:进程与调度、同步与死锁、内存与虚拟存储、文件与磁盘、设备与 I/O。我见过不少同学拿到 PDF 直接按顺序做,做到第四章发现第二章的进程状态又忘了,再回头翻,效率极低。正确做法是先花半小时通读,给每道题标上一到两个模块标签,比如「进程状态+调度」「内存+页表」「文件+目录结构」,之后按模块集中刷。
这里有个容易被忽略的规律:进程管理和内存管理通常占练习总量的六成以上,而文件系统和设备管理多是概念题加少量计算题(磁盘调度、位示图)。你要是时间紧,按这个比例分配精力,比平均用力划算得多。归类的时候把题目序号写在草稿纸上,同时标出「会做」「半懂」「完全没思路」三档。半懂和没思路的题,才是你真正要补的盲区,而不是那些已经会做的。做完归类,你会发现自己对整门课的结构一下子清楚了,这份归类过程本身,就是一份比课本目录更好用的操作系统笔记。
2.2 考点映射表:一道练习对应一个可答辩的课程设计
归类之后,第二步是做一张考点映射表。随堂练习的价值在于:每一道计算题,本质上是课程设计里一个算法的手算样例。课程设计验收的时候,老师最常问的一句话就是「你怎么证明你的程序是对的」。如果你能把练习里的标准答案变成程序的测试用例,这个问题就迎刃而解。下面这张表是我按常见题型整理的映射关系,你可以照着它把自己的练习标进去。
| 练习题型 | 对应考点 | 可延伸的课程设计方向 | 最容易丢分的位置 |
|---|---|---|---|
| 进程状态转换与调度计算(FCFS/SJF/RR) | 进程生命周期、调度算法评价指标 | 调度模拟器:输出周转、带权周转、等待时间 | 状态转换条件漏写「等待→就绪」 |
| PV 操作题(生产者消费者、读者写者) | 信号量互斥与同步 | 多线程同步程序、哲学家就餐模拟 | P/V 顺序颠倒、信号量初始值设错 |
| 银行家算法安全序列判断 | 死锁避免 | 银行家算法可视化工具 | 可用资源向量更新错、Need 矩阵算错 |
| 逻辑地址换算与页表查询 | 分页存储、快表、有效访问时间 | 地址转换命令行工具 | 十六进制换算翻车、把块号当页号 |
| 页面置换(OPT/FIFO/LRU/Clock) | 缺页率、Belady 异常 | 置换算法对比程序 | 初始是否计数约定不清、LRU 找错方向 |
| 磁盘调度(FCFS/SCAN/C-SCAN) | 寻道时间优化 | 磁盘臂调度模拟器 | 磁头方向不回绕、起始位置忘标注 |
这张表最关键的作用,是把「会做题」和「能答辩」两件事打通。比如你练习里做对了一道 FCFS 调度题,那你课程设计里调度器的输入输出格式,就应该按这道题来定义:进程号、到达时间、服务时间进去,完成时间、周转时间、等待时间出来。这样练习里的每一道题,都能变成一个可复现的验收点。答辩时老师随便指一道练习,你现场把参数敲进去,结果和标准答案一致,这比任何口头解释都有说服力。
2.3 动笔前先立三个习惯:状态图、条件表、时间轴
很多同学做计算题喜欢直接套公式,结果一到变体题就懵。我一般会让学生动笔前先立三个习惯,这三个习惯后期写代码时直接变成变量和数据结构,一举两得。
习惯一:凡进程题先画状态图。不管题目问的是调度还是同步,先把三态或五态图画出来,标注每个转换事件的触发条件。比如「运行→等待」只能由 I/O 请求或事件等待触发,「等待→就绪」只能由 I/O 完成或事件发生触发。这道工序看起来多花三十秒,但能防止你漏掉转换边,尤其防止把「等待→就绪」这条最容易漏的边丢掉。
习惯二:凡死锁题先列四个必要条件。互斥、请求保持、不可剥夺、循环等待,逐条对照题目场景,再判断题目考的是预防、避免还是检测。银行家算法属于避免,破坏循环等待属于预防,这两类题目的解题入口完全不同,先列条件能帮你快速定位题型。
习惯三:凡调度题写时间轴。就是甘特图,每完成一个进程就更新一次当前时间。FCFS 的时间轴是一条直线,SJF 非抢占的时间轴要标出每次就绪队列变化的位置。这个时间轴写熟练了,对应到代码里就是一个累加的time变量,后面写调度模拟器时,你会发现代码几乎就是时间轴的翻译。
3. 从随堂练习到可运行代码:调度、银行家、信号量的落地写法
随堂练习里的计算题,本质是手算一个算法;课程设计要做的是把这些算法写成能跑的程序。常见做法是把每个核心算法做成一个小模拟器,输入练习里的数据,输出练习要求的指标。我一般用 Python 做原型,因为数据结构直观、调试快,答辩时还能现场改参数给老师看。下面三个例子覆盖了最常考、也最常被选作课程设计的三个方向。
3.1 把调度计算题变成调度模拟器:FCFS 与 SJF 一起写
调度题是随堂练习的必考项,也是课程设计里最好出效果的方向。这里给一个同时支持 FCFS 和非抢占 SJF 的调度模拟器,输入是进程列表,输出是每个进程的完成时间、周转时间和等待时间。
def schedule(processes, mode="fcfs"): # processes: [(pid, arrival, burst), ...] # pid=进程号, arrival=到达时间, burst=服务时间 procs = sorted(processes, key=lambda p: p[1]) # 先按到达时间排 time = 0 # 当前系统时间,就是手算时的甘特图游标 done = [] # 已完成的进程 ready = [] # 就绪队列 idx = 0 n = len(procs) while len(done) < n: # 把所有已到达的进程放进就绪队列 while idx < n and procs[idx][1] <= time: ready.append(procs[idx]) idx += 1 if not ready: # CPU 空闲,直接跳到下一个进程的到达时刻 time = procs[idx][1] continue if mode == "sjf": ready.sort(key=lambda p: p[2]) # SJF:从就绪队列挑最短服务时间 pid, arrival, burst = ready.pop(0) time += burst done.append((pid, arrival, burst, time, time - arrival, # 周转时间 time - arrival - burst)) # 等待时间 return done # 示例:三道随堂练习风格的进程,手算 FCFS 后核对输出 procs = [(1, 0, 7), (2, 2, 4), (3, 4, 1)] for row in schedule(procs, "fcfs"): print(row)这段代码的核心逻辑有三处要说明。第一,time就是你在草稿纸上画的时间轴,每完成一个进程就累加一次burst,CPU 空闲时直接跳到下一个到达时刻,对应你手算时空闲段跳过不等的习惯。第二,ready队列是 FCFS 和 SJF 的分水岭:FCFS 按到达顺序弹出,SJF 每次从就绪队列里挑服务时间最短的,这就是非抢占 SJF 的手算过程。第三,输出里的周转时间time - arrival和等待时间time - arrival - burst是课程设计验收的标准指标,练习里的标准答案就是这两个数加一个平均带权周转时间。
参数上要注意:如果题目要求的是抢占式 SJF(也叫 SRTF),上面的代码就不够用了,需要在每个新进程到达时比较剩余服务时间,这个版本留给你的课设当扩展点。FCFS 模式下如果所有进程同时到达,那按到达时间排序后就是按进程号顺序执行,和手算结果一致。验证方法很简单,把练习里手算好的甘特图拿出来,逐行对照这段程序的输出。
3.2 银行家算法:安全序列从手算到程序
银行家算法是死锁章节的压轴题,手算时要反复试分配,写程序时最怕的就是把试分配的资源状态改乱了。下面这个实现把安全检查单独做成一个函数,注意看它在work上做临时累加,而不是直接改available。
def is_safe(available, allocation, need): # available: 当前可用资源向量,例如 [3, 3, 2] # allocation[i]: 进程 i 已分配的资源 # need[i]: 进程 i 还需要的资源 work = available[:] # 安全检查的临时工作副本,不能改原始数据 finish = [False] * len(allocation) safe_seq = [] while len(safe_seq) < len(allocation): found = False for i in range(len(allocation)): if not finish[i] and all(need[i][j] <= work[j] for j in range(len(work))): # 进程 i 可以完成,回收它占用的全部资源 for j in range(len(work)): work[j] += allocation[i][j] finish[i] = True safe_seq.append(i) found = True if not found: # 一轮扫描找不到可完成的进程,说明系统将进入不安全状态 return False, [] return True, safe_seq # 经典练习数据:5 个进程,3 类资源 available = [3, 3, 2] allocation = [[0, 1, 0], [2, 0, 0], [3, 0, 2], [2, 1, 1], [0, 0, 2]] need = [[7, 4, 3], [1, 2, 2], [6, 0, 0], [0, 1, 1], [4, 3, 1]] ok, seq = is_safe(available, allocation, need) print("safe" if ok else "unsafe", seq)这里最值得讲的是work = available[:]这一行。手算安全序列时,你是在草稿纸上复制一份可用资源来做试分配,算完扔掉,不会影响题目原条件。程序里如果不复制,直接在available上累加,第一次尝试成功后原始数据就变了,后续判断全部失真,这就是「手算安全、程序报错」最常见的根源。need[i][j] <= work[j]这个判断对应手算时那句「检查某进程的剩余需求是否都被当前可用资源满足」,all函数把多类资源的一次性检查压缩成一行。
如果课程设计要做到「请求资源」这一层,流程是:先检查Request[i] <= Need[i],再检查Request[i] <= Available,然后做一次试分配(把 Available、Allocation、Need 都更新),最后调用is_safe判断试分配后是否仍安全。安全才正式分配,否则回滚。这个回滚逻辑,对应手算题里那句「若找不到安全序列,则本次请求不被批准」。
3.3 生产者消费者:把 PV 练习变成能跑的多线程程序
同步问题是随堂练习里最容易让人怀疑人生的部分,因为 PV 操作光靠手算很难验证,死锁不跑起来看不见。把练习里的信号量题改造成多线程程序,是理解同步最快的路径。
import threading import time buffer = [] BUFFER_SIZE = 5 empty = threading.Semaphore(BUFFER_SIZE) # 空位个数,初始等于缓冲区大小 full = threading.Semaphore(0) # 已占用个数,初始为 0 mutex = threading.Lock() # 互斥锁,保护 buffer 本身 def producer(item): empty.acquire() # 先申请空位,对应 P(empty) mutex.acquire() # 再拿互斥锁,对应 P(mutex) buffer.append(item) print("produce", item, buffer) mutex.release() # 先释放锁,对应 V(mutex) full.release() # 增加已占用个数,对应 V(full) def consumer(): full.acquire() # 先确认有数据,对应 P(full) mutex.acquire() # 再拿互斥锁 item = buffer.pop(0) print("consume", item, buffer) mutex.release() empty.release() # 释放一个空位,对应 V(empty) return item if __name__ == "__main__": for i in range(10): threading.Thread(target=producer, args=(i,)).start() threading.Thread(target=consumer).start()对照课本的 PV 写法,你会发现代码就是P(empty); P(mutex); 写缓冲; V(mutex); V(full)的直译。两个信号量的初始值就是练习里要求的「设 empty=n, full=0」,这是整个程序正确性的前提。顺序上务必记住:先资源信号量,后互斥锁。empty和full管的是「缓冲区还有没有空位/数据」这个资源条件,mutex管的是「同一时刻只能一个线程碰 buffer」。如果把mutex.acquire()提到empty.acquire()前面,缓冲区满时生产者会拿着锁等空位,消费者又进不去取数据,直接死锁。
这套模板可以扩展成读者写者问题、哲学家就餐问题,只要把信号量个数和获取顺序按题目改就行。如果课程设计用 C 语言写,Linux 下编译记得加-pthread链接选项,Windows 下用 Win32 的CreateSemaphore也是同一个思路。跑起来后在终端观察打印顺序,你会发现所谓「同步」,就是让不同线程的打印要么都在临界区里,要么被资源信号量卡在门外。
4. 避坑:随堂练习反复翻车的 5 类问题与排查思路
下面五条是我辅导课设和批改作业时出现频率最高的翻车点,每条按「现象 → 原因 → 解决」的顺序写。你可以直接对照自己的练习册和代码找问题。
4.1 状态图漏了「等待→就绪」这条边
现象:画进程五态图每次都不完整,做调度题时默认进程 I/O 完成后直接进入运行态,导致时间轴算错。
原因:把「等待」当成了终点,忘了进程从 I/O 或事件中醒来后要先回到就绪队列排队,不能插队直接上 CPU。这里丢的不是一条边,而是整个就绪队列存在的意义。
解决:把五态转换背成两进两出——就绪→运行由调度触发,运行→就绪由时间片到触发,运行→等待由 I/O 请求触发,等待→就绪由 I/O 完成触发。每次画完图数一遍四条边,少了哪条一眼就能看出来。这个习惯延伸到代码里,就是就绪队列的入队操作必须发生在进程状态变成就绪之后,而不是在 I/O 完成的那一刻直接让出 CPU。
4.2 银行家算法手算安全、程序却报不安全
现象:随堂练习手算得出安全序列,比如 (P1, P3, P2, P4, P0),把同样的数据写进代码,程序却返回 unsafe。
原因:最常见的是在安全检查里直接修改了available,第一轮 P1 试分配成功后原始数据就变了,后面的判断条件不再反映真实系统状态。另一种是把allocation和need搞混,need是「还需要的」,allocation是「已经占着的」,用错一个矩阵整个算法就废了。
解决:检查代码里是否对available做了副本再开始试分配,就像上一章代码里的work = available[:]。再打印出need矩阵和草稿纸逐格对比。我一般会在is_safe里临时加两行print,把每轮选中的进程和当时的work向量打出来,和手算草稿上的每次「可用资源更新」对齐,能对上就说明逻辑没问题,对不上就回头查资源回收那几行。
4.3 地址换算题:页号对了偏移量却错了
现象:逻辑地址0x2A3F,页面大小 1KB,算出页号是 10,但偏移量一个版本算 575,一个版本算 543,对不上答案。还有一种翻车是把十六进制整体转十进制再去除页面大小,除出来的页号总是差一位。
原因:分页地址换算的本质是位运算,页面大小是2^10时,页内偏移就是逻辑地址的低 10 位,页号是高剩余位。很多同学把它当普通除法做,忘了十六进制数里每一位对应 4 个二进制位,低 10 位并不等于低两位十六进制,要从二进制位边界去切。
解决:页面大小为 1KB 时,先把逻辑地址写成十六进制,低两位就是偏移量,高位就是页号,这个规则对所有 2 的幂次页面大小都成立。再用 Python 的int("2A3F", 16)转成十进制算一遍交叉验证。两道工序结果一致再写答案,不一致就回头检查是哪一位切错了。这道题是随堂练习里靠粗心丢分最狠的地方,没有之一。
4.4 PV 题把 P 的顺序写反,程序直接卡死
现象:生产者消费者多缓冲区程序跑起来,输出一两行后就卡住,终端既不往下打印也不报错,像死机了一样。
原因:生产者的mutex.acquire()写在了empty.acquire()前面。缓冲区满时生产者拿着互斥锁等在empty上,消费者想进临界区拿数据却被同一把锁挡住,两边互相等,这就是教科书式的死锁。单缓冲区题目里运气好可能不翻车,缓冲区一多必现。
解决:严格按「先资源信号量、后互斥锁」的顺序写。排查时在每个acquire前后加一行打印信号量当前值,卡住的位置一定在某个acquire上,看它卡在哪个信号量,就能定位是资源条件没满足还是锁被人占着。这个排查手段在课程设计答辩现场特别好用,因为你能现场演示「卡住 → 定位 → 改顺序 → 跑通」的完整过程,比空讲同步原理有说服力得多。
4.5 页面置换答案总差一次缺页
现象:FIFO 或 LRU 手算的缺页次数,和标准答案总是差 1 次,有时多有时少。
原因:两个默认约定没对齐——初始页框是否算缺页,以及访问串里重复访问的页是否计命中。大多数练习的默认规则是「页框初始为空,第一次装入算缺页,重复访问算命中」,但题目不写的时候,你按「已预先装满」算就会差一次。
解决:做题前先在草稿纸顶部写两行约定:「初始空 = 算缺页;页已在内存 = 命中不计数」。算完如果还是差 1 次,别从头重算,直接检查最后一次置换——最后一次访问的页如果已经在页框里,却被你重复装入了一次,缺页数就会多 1。这类题目对了约定,正确率能立刻拉满。对应到课程设计代码里,就是模拟器要暴露一个「初始状态」参数,让用户明确选「空页框」还是「预填充」,这样无论题目怎么出,你都能对齐答案。
5. 把随堂练习变成课程设计答辩的素材库:一个值得坚持的验证习惯
做完整份随堂练习,最该做的不是把答案背下来,而是把这些题变成课程设计代码的回归测试集。我这几年带课设最深的体会是:老师答辩时问的不是「你会不会原理」,而是「你怎么证明你写的东西是对的」。随堂练习恰好就是现成的测试数据,因为每道题都有标准答案,而你的代码输入这些数据,输出必须和答案一致。
具体做法很朴素:为每个算法建一个tests目录,把练习里的题目输入和标准答案存成断言。调度器就断言平均周转时间,银行家算法就断言安全序列,页面置换就断言缺页次数。下面这个片段是调度器的测试骨架,你可以照着扩展。
# tests/test_scheduler.py cases = [ # 输入: (进程列表, 调度方式) 输出: 期望的平均周转时间 ([(1, 0, 7), (2, 2, 4), (3, 4, 1)], "fcfs", 6.0), ([(1, 0, 7), (2, 2, 4), (3, 4, 1)], "sjf", 5.0), ] def run_case(processes, mode, expected): result = schedule(processes, mode) avg = sum(r[4] for r in result) / len(result) # r[4] 是周转时间 assert abs(avg - expected) < 1e-6, f"{mode}: {avg} != {expected}" for processes, mode, expected in cases: run_case(processes, mode, expected)这样做的直接收益是,课程设计做到最后,你手里有一个「任何修改都不会破坏已验证结论」的保障。改调度逻辑、加新算法,跑一遍测试集,哪里坏了立刻知道。答辩前我还会专门测三类边界条件:单进程且带较长空闲段、所有进程同一时刻到达、银行家算法里请求大于 Need,这三种情况是老师最常随手考的场景。
我自己当年做页面置换课设时,把教材和练习里能找到的二十道置换题全做成了断言,答辩时老师随手点了一道 LRU 的变体题,我现场把参数输进程序,一行输出就给出了和标准答案一致的缺页次数。那个瞬间我意识到,随堂练习最好的用法不是考前突击,而是平时就把每一道题喂给代码当裁判。这个习惯后来无论做操作系统还是做别的项目,我都一直保留着,希望帮到你。
本文还有配套的精品资源,点击获取