news 2026/9/17 12:35:30

操作系统课后习题验证:PV操作、银行家算法与页面置换复算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
操作系统课后习题验证:PV操作、银行家算法与页面置换复算

简介:这份文档是《计算机操作系统教程》左万利、王英第四版课后习题的配套答案资料,面向高校计算机专业学生及考研、期末备考人群,用于核对章节作业、梳理考点与巩固解题思路。压缩包共1个文件,为doc格式,大小约4.21MB,可直接用Word或WPS打开检索,按章节顺序编排,便于对照教材逐题查阅。内容覆盖操作系统基本概念、进程管理、处理机调度、死锁避免、银行家算法与读者写者同步等核心章节:第三章给出EDF与RMS算法可调度性判断及Gantt图绘制,并完整推演FCFS、SJB、HRN三种调度策略下的平均周转时间与带权周转时间;第四章提供读者写者问题的信号量解法及写者优先方案;第五章结合银行家算法与死锁检测,逐步演算安全状态判断、资源请求分配与死锁进程识别。目前该答案已有6331人学习下载,适合作为章节练习后的自检参照与考前重点题型复盘使用。

1. 从一份课后答案说起:操作系统习题的验证式做法

搜「计算机操作系统教程左万利王英第四版课后习题答案.doc」的人,多半手里已经有一份答案文档,真正卡住的不是找不到答案,而是答案对不上自己的手算过程:银行家算法给出的安全序列和自己推的不一致,FIFO 的缺页次数差一次,PV 操作的 P 顺序换个位置结果就死锁。计算机操作系统的习题有个特点——结论对参数极度敏感,缓冲区容量从 4 改成 3、引用串多一个页号、磁头初始方向反过来,整道题都要重算。一份静态的 doc 只能给出某组参数下的一个结果,给不了中间状态。

可行的做法是把课后习题按题型拆成四类:同步与 PV 操作、死锁与银行家算法、存储管理与页面置换、磁盘调度与文件系统。每类写一个几十行的小脚本,把题目参数当输入,把答案结论当断言。这样一旦某道题对不上,能立刻定位到哪一步的中间状态算错了,而不是反复重抄。

下面按这个顺序展开,代码都能直接跑,参数位置留成变量,方便换成自己卷子上的数字。

2. 进程同步习题的推演:PV 操作、管程与协程别混着用

2.1 信号量题为什么「看得懂答案、自己写就死锁」

信号量的语义只有两条:P 操作把信号量减 1,若结果小于 0 则调用者阻塞;V 操作把信号量加 1,若结果小于等于 0 则唤醒一个等待者。题目给的是并发进程的代码骨架,要求你补 P、V 的位置。绝大多数人失分不在语义,而在P 操作的顺序

以生产者-消费者为例,三个信号量:empty表示空槽数量,full表示满槽数量,mutex保护缓冲区本身。生产者必须先申请空槽、再申请互斥锁;如果反过来,生产者拿着mutex等空槽,而消费者要先拿mutex才能取数据释放空槽,双方互等,直接死锁。这个顺序错误在习题答案里通常只标注「错误」,不会展开到底错在哪,自己跑一遍才看得见。

2.2 生产者-消费者:用 Python 信号量复现并校验缓冲区不变量

import threading, random, time BUF_SIZE = 4 empty = threading.Semaphore(BUF_SIZE) # 空槽计数,初值 = 缓冲区容量 full = threading.Semaphore(0) # 满槽计数,初值 0,必须先 V 才能 P 到 mutex = threading.Semaphore(1) # 互斥量,二值信号量 buf = [] violations = [] def producer(tid): for i in range(5): item = f"P{tid}-{i}" empty.acquire() # P(empty):先确认有空槽,再抢锁 mutex.acquire() # P(mutex) buf.append(item) # 临界区:只放一个元素 if len(buf) > BUF_SIZE: violations.append(("overflow", len(buf))) mutex.release() # V(mutex) full.release() # V(full):满槽 +1,唤醒消费者 def consumer(tid): for _ in range(5): full.acquire() # P(full):先确认有数据,再抢锁 mutex.acquire() # P(mutex) if not buf: violations.append(("underflow", 0)) item = buf.pop(0) mutex.release() # V(mutex) empty.release() # V(empty):空槽 +1,唤醒生产者 ts = [threading.Thread(target=producer, args=(i,)) for i in range(3)] ts += [threading.Thread(target=consumer, args=(i,)) for i in range(3)] [t.start() for t in ts] [t.join() for t in ts] print("violations:", violations, "left:", buf)

逻辑说明:emptyfull的和恒等于BUF_SIZE,这是这类题最该检查的不变量。代码里每次进入临界区都做一次越界检查,跑完violations为空且buf为空,说明同步逻辑自洽。参数说明:BUF_SIZE改成题面给的容量;生产/消费轮次改成题目要求的值;把empty.acquire()mutex.acquire()互换,就能亲手复现死锁——程序会在几十秒后卡住不动,这比看答案上那句「会死锁」直观得多。

注意:CPython 有 GIL,这里不会真的并行执行字节码,但信号量的阻塞唤醒顺序与真实并发语义一致,用来验证 PV 顺序足够。想更接近裸机行为可以换成多进程,代价是要把buf换成multiprocessing.Queue

2.3 管程、协程和信号量的边界

热搜里常把「管程和协程」放一起,这两者根本不在一个层面。管程是把互斥和同步封装进模块的语言级同步构造,进入管程自动加锁,条件变量上的 wait/signal 对应 P/V;协程是用户态调度的执行体,切换靠显式 yield/await,它解决的是并发任务的切换开销,不是互斥问题。

构造谁负责切换解决的问题习题里的典型问法
信号量内核调度器阻塞/唤醒互斥与同步写出 P、V 操作序列,判断是否死锁
管程语言运行时 + 条件变量把同步封装进模块,避免散落的 P/V用管程改写生产者-消费者
协程用户态显式让出高并发任务的轻量调度与同步原语的区别,能否替代互斥量

答题时如果题目明确说「用管程实现」,就不要再写semaphore.acquire(),而要写wait(c)/signal(c)形式,条件判断用 while 而不是 if——被唤醒后要重新检查条件,这是管程题最常见的扣分点。

2.4 习题里最容易扣分的四个写法

  • if代替while检查条件:虚假唤醒或条件被别的进程抢先改变时直接出错。
  • V(mutex)写在V(full)之后:多数情况下能跑,但临界区被拉长,题目若问「能否减少临界区」会失分。
  • 读者-写者题里让读者持有mutex去读:读操作本可并行,会退化成串行。
  • 哲学家进餐题不做奇偶区分或一次性申请两把叉子:五个哲学家同时拿起左叉,死锁必然发生。

3. 银行家算法与死锁习题:手算表格怎么变成可执行校验

3.1 安全序列判定:Need 与 Work 的逐分量比较

银行家算法的核心只有一句话:找一个 Need 的每一个分量都不超过当前 Work 的进程,让它执行完并归还资源,重复到所有进程都完成,则状态安全。手算步骤固定为四步:算 Need = Max − Allocation;复制 Available 到 Work;按顺序扫描找满足条件的进程;回收该进程的 Allocation 加到 Work 上。

下面这组数据是这类题的经典结构,三种资源、五个进程:

进程AllocationMaxNeed(Max−Allocation)
P00 1 07 5 37 4 3
P12 0 03 2 21 2 2
P23 0 29 0 26 0 0
P32 1 12 2 20 1 1
P40 0 24 3 34 3 1

Available = (3,3,2)。手算过程:P1 的 Need(1,2,2) ≤ Work(3,3,2),执行完 Work 变 (5,3,2);P3 的 (0,1,1) ≤ (5,3,2),Work 变 (7,4,3);P4 的 (4,3,1) ≤ (7,4,3),Work 变 (7,4,5);P0 的 (7,4,3) ≤ (7,4,5),Work 变 (7,5,5);P2 的 (6,0,0) ≤ (7,5,5),全部完成。安全序列 P1→P3→P4→P0→P2。

3.2 用一段代码复算安全序列并输出一个可行解

def need_matrix(maxm, alloc): return [[maxm[i][j] - alloc[i][j] for j in range(len(maxm[0]))] for i in range(len(maxm))] def is_safe(avail, maxm, alloc, n, m): need = need_matrix(maxm, alloc) work = avail[:] # 复制一份,不能改原 Available finish = [False] * n seq = [] while len(seq) < n: picked = -1 for i in range(n): if not finish[i] and all(need[i][j] <= work[j] for j in range(m)): picked = i # 只要求 Need <= Work,不是 Max <= Work break if picked == -1: return False, [] # 存在进程无法完成,状态不安全 for j in range(m): work[j] += alloc[picked][j] # 进程归还全部已分配资源 finish[picked] = True seq.append(picked) return True, seq avail = [3, 3, 2] maxm = [[7,5,3], [3,2,2], [9,0,2], [2,2,2], [4,3,3]] alloc = [[0,1,0], [2,0,0], [3,0,2], [2,1,1], [0,0,2]] print(is_safe(avail, maxm, alloc, 5, 3))

逻辑说明:外层while保证每个进程最多被选中一次;内层for每次从下标 0 开始扫描,所以输出的是「按进程号优先」的那个安全序列,而安全序列通常不唯一,答案里给的是另一种顺序不代表你错。参数说明:avail是当前可用向量,maxm是各进程的最大需求矩阵,alloc是已分配矩阵,n/m分别是进程数和资源类数。把all(...)里的need错写成maxm是手算时最高频的错误,代码里刻意保留了注释提醒。

3.3 Request 请求的三个前置条件与试探分配

题目给的第二个问法通常是「P1 发出 Request(1,0,2),能否分配」。判定顺序不能乱:先查Request ≤ Need,超过说明进程请求超出自己声明的最大需求,是出错;再查Request ≤ Available,超过说明资源暂时不够,阻塞等待;最后做试探分配,把 Allocation、Available 改掉之后重跑一次安全性检查,不安全就撤销试验并让进程等待

def request(avail, maxm, alloc, pid, req): need = need_matrix(maxm, alloc) m = len(avail) if any(req[j] > need[pid][j] for j in range(m)): return "出错:请求超过最大需求" if any(req[j] > avail[j] for j in range(m)): return "阻塞:当前可用资源不足" trial_alloc = [row[:] for row in alloc] # 深拷贝,避免污染原矩阵 trial_avail = avail[:] for j in range(m): trial_alloc[pid][j] += req[j] trial_avail[j] -= req[j] ok, seq = is_safe(trial_avail, maxm, trial_alloc, len(alloc), m) return f"可以分配,安全序列 {seq}" if ok else "暂不分配:试探后状态不安全"

逻辑说明:三个检查必须按「是否合法 → 是否够用 → 是否安全」的次序执行,顺序颠倒会得到语义不同的结论。参数说明:req是长度为 m 的请求向量;trial_alloc用列表推导逐行复制,直接trial_alloc = alloc会改到原数据,导致同一道题第二次调用结果错乱。

3.4 手算常见错误对照表

错误写法后果正确做法
用 Max ≤ Work 筛选进程永远找不到可执行进程,误判为不安全用 Need ≤ Work
修改 Available 而不深拷贝多次判定互相污染每次复制 Work / Available
只找到一个安全序列就收工题目要求「所有安全序列」时漏答案用回溯枚举或接受任一序列
把不安全等同于死锁概念混淆不安全只是「可能」进入死锁
死锁检测与银行家算法混用检测针对已发生死锁,避免针对分配前看清题干动词

4. 页面置换与内存管理计算题:FIFO、LRU、OPT 的表格化复算

4.1 引用串、驻留集与缺页次数:三个必须先固定的假设

页面置换题丢分往往不是算法错,而是假设没对齐。三个必须先在草稿纸角上写清楚的量:引用串是否含首次装入阶段、可用页框数是固定分配还是可变分配、缺页次数统计时首次装入空页框算不算缺页。教材间的约定并不统一——常见的汤小丹版、慕课版讲义与左万利王英这本的记号方式各有差异,同一道题在两本书里答案差 1 到 2 次缺页很正常。做题时把这三个假设写进答案开头,比算出正确数字更重要。

置换了哪一页,取决于算法选择的淘汰对象:FIFO 淘汰内存中驻留最久的页,LRU 淘汰最久未被访问的页,OPT 淘汰未来最长时间不会被访问的页。前两者可实现,OPT 只是理论下界,用来衡量其他算法的差距。

4.2 FIFO、LRU、OPT 的最小实现

from collections import OrderedDict, deque def fifo(ref, frames): q, faults = deque(), 0 for p in ref: if p not in q: faults += 1 if len(q) == frames: q.popleft() # 淘汰队首,即最早进入的页 q.append(p) return faults def lru(ref, frames): od, faults = OrderedDict(), 0 for p in ref: if p in od: od.move_to_end(p) # 命中,刷新为最近使用 else: faults += 1 if len(od) == frames: od.popitem(last=False) # 淘汰最久未使用 od[p] = None return faults def opt(ref, frames): mem, faults = [], 0 for i, p in enumerate(ref): if p in mem: continue faults += 1 if len(mem) < frames: mem.append(p) continue far, victim = -1, None for q in mem: nxt = next((j for j in range(i + 1, len(ref)) if ref[j] == q), float("inf")) if nxt > far: # 下次访问最晚的页被淘汰 far, victim = nxt, q mem[mem.index(victim)] = p return faults ref = [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] for name, fn in (("FIFO", fifo), ("LRU", lru), ("OPT", opt)): print(name, fn(ref, 3))

逻辑说明:三个函数都只返回缺页次数,不关心具体淘汰过程,出结果后再和答案对照。OrderedDictmove_to_endpopitem(last=False)恰好实现了 LRU 的「命中刷新、淘汰最旧」两步,比手写链表短得多。参数说明:frames是页框数,改成 4 就能观察 FIFO 的异常行为。上面这组 20 个引用的串,三页框下 FIFO 缺页 15 次、LRU 12 次、OPT 9 次;如果你手算得到 14 次,多半是某次命中被误判成缺页。

提示:OPT 计算里的float("inf")表示该页之后不再被访问,它一定是淘汰首选。手算时对这类页可以直接划掉,不用逐个比较。

4.3 Belady 异常:FIFO 加页框反而更差的一组数据

FIFO 会违反「页框越多缺页越少」的直觉,这就是 Belady 异常。用引用串1 2 3 4 1 2 5 1 2 3 4 5手推一遍:

页框数缺页次数现象
3 个页框9第 4 次引用 1 时就已装入,未额外缺页
4 个页框10淘汰顺序被打乱,后续 1、2 全部缺页

三页框时,序列走到1 2 3 4淘汰 1,之后5 1 2 3 4 5中 1 缺页一次、5 命中一次;四页框时 1、2 在早期全部命中,却因此在5 1 2 3 4 5段里被成批淘汰,反而多缺一次。LRU 和 OPT 属于栈算法,不会出现这种现象——考卷上问「哪种算法可能产生 Belady 异常」,答案只能是 FIFO。

4.4 Clock 算法是 LRU 的近似,答题时别写反置换指针

Clock 算法给每页一个访问位,需要淘汰时从指针位置开始扫描:访问位为 1 就清 0 并跳过,为 0 就淘汰。它和 LRU 的差别在于只记录「最近是否被访问」这一个 bit,代价是精度,收益是不用维护完整的访问顺序链。

手算 Clock 题最容易犯的错是扫描方向写反、清 0 后没有回头。正确的循环是「扫描 → 遇 1 清 0 → 指针后移 → 遇 0 淘汰并让指针停在下一页」。改进型 Clock 再加一个修改位,优先淘汰「未访问且未修改」的页,其次「未访问已修改」,这四类优先级顺序是固定答题模板,值得背下来。

5. 磁盘调度与答案核对:把课后习题做成可重跑的自测集

5.1 SSTF 与 SCAN/LOOK 的移臂量计算

磁盘调度题的全部工作量就是累加相邻访问磁道号之差的绝对值。设请求序列98 183 37 122 14 124 65 67,磁头初始在 53 号磁道,磁道范围 0–199:

算法访问顺序移臂总量
FCFS53→98→183→37→122→14→124→65→67640
SSTF53→65→67→37→14→98→122→124→183236
SCAN(向 0 方向)53→37→14→0→65→67→98→122→124→183236
LOOK(向 0 方向)53→37→14→65→67→98→122→124→183208

SCAN 与 LOOK 差的 28 道,正是从 14 走到 0 再折返到 65 的那段空跑。教材写法不统一,有的把 LOOK 也叫「电梯调度」,答题时要把「是否走到磁盘端点」写明,否则同一道题会算出两个数。SSTF 的隐患则是饥饿——如果新请求持续落在磁头附近,远处的 183 可能一直排不上队,这也是它不能直接用于生产调度的原因。

5.2 用 pytest 把一组课后题固化成回归用例

import pytest from os_hw.paging import fifo, lru, opt from os_hw.banker import is_safe REF20 = [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] @pytest.mark.parametrize("algo,expected", [(fifo,15), (lru,12), (opt,9)]) def test_paging_ref20(algo, expected): assert algo(REF20, 3) == expected # 三页框下的标准结论 def test_belady(): ref = [1,2,3,4,1,2,5,1,2,3,4,5] assert fifo(ref, 3) == 9 assert fifo(ref, 4) == 10 # 页框变多,缺页反而增加 def test_banker_safe(): ok, seq = is_safe([3,3,2], [[7,5,3],[3,2,2],[9,0,2],[2,2,2],[4,3,3]], [[0,1,0],[2,0,0],[3,0,2],[2,1,1],[0,0,2]], 5, 3) assert ok and len(set(seq)) == 5

逻辑说明:parametrize让一个函数同时校验三种算法,改参数比改代码快。test_belady用两个断言把异常现象本身固化成用例——如果哪天重构把 FIFO 写成了栈算法,这条测试会立刻变红。参数说明:把REF20换成你正在做的题目引用串,把期望值填成自己手算的结果,跑一次就知道是算法实现错了还是手算错了。

5.3 跨教材版本的记号差异怎么核对

同一道题在不同教材里答案不一致,通常出在三处:缺页次数是否含初始装入、SCAN 是否走到物理端点、P/V 是否写成 wait/signal。核对时不要直接比数字,而是比中间表——把每一轮的驻留页集合或 Work 向量打出来逐行比对,差异会集中在某一次置换或某一次资源回收上,那一步就是约定不同之处。

我的习惯是给每道题在脚本里写一行假设注释,例如「# 缺页含首次装入」「# SCAN 走到端点 0」,答案对不上时先看注释再怀疑答案。那份.doc里的答案大概率是对的,只是它的假设没写在纸面上;把假设显式化之后,你手里就不再是一份答案,而是一组能重跑的断言,换个数也能立刻验算。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 12:35:07

单细胞Seurat可视化进阶:DimPlot与FeaturePlot参数调优全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 12:35:04

全志V3S嵌入式Linux从零构建实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 12:34:31

Eclipse项目迁移IDEA全攻略:从导入到部署的避坑指南

上个月帮朋友把一个维护了六年的老Eclipse项目迁移到IDEA&#xff0c;本以为就是Open一下的事&#xff0c;结果整整折腾了一天。后来复盘发现&#xff0c;很多问题其实都是对IDEA和Eclipse项目模型的理解差异造成的——你拿着Eclipse的思维去用IDEA&#xff0c;第一步就会碰壁。…

作者头像 李华
网站建设 2026/9/17 12:34:24

Jira实战生存指南:从入门到高效协同的完整路径

1. 这不是一本说明书&#xff0c;而是一份Jira实战生存指南你点开这个标题&#xff0c;大概率正被三件事同时围困&#xff1a;第一&#xff0c;刚接手一个陌生项目&#xff0c;发现所有需求、Bug、任务都散落在Jira里&#xff0c;像进了迷宫&#xff1b;第二&#xff0c;团队里…

作者头像 李华
网站建设 2026/9/17 12:34:11

SQL Server分页实战:从LIMIT迁移到TOP、ROW_NUMBER与OFFSET-FETCH

第一次从MySQL迁到SQL Server&#xff0c;我差点以为装了个假数据库。SELECT ... LIMIT 10在MySQL里跑得行云流水&#xff0c;到SQL Server控制台一敲&#xff0c;直接给我一句Incorrect syntax near LIMIT。查了半天文档才反应过来&#xff0c;SQL Server压根没有把LIMIT当作保…

作者头像 李华