简介:这份资源是西安电子科技大学操作系统课程的上机实验报告,面向正在学习操作系统、需要完成进程与线程相关实验的高校学生及自学者。报告围绕Linux环境下C语言编程展开,完整覆盖进程建立、线程共享进程数据、信号通信、匿名管道与命名管道通信,以及用信号量实现进程同步等核心实验内容,每部分均包含实验目的、软硬件环境、程序代码、运行分析与心得体会,可帮助读者理解fork()、waitpid()、pthread_create()、sem_wait()等关键函数的实际用法。资源包为1个doc文档,约377KB,结构紧凑,便于直接参考与整理。目前已有1346人学习下载,适合需要对照实验流程、梳理进程同步与进程间通信思路、积累系统级编程经验的读者使用。
1. 操作系统实验报告到底在写什么:从一次进程调度翻车说起
很多人第一次拿到操作系统实验任务,以为就是“写个报告交上去”。真正动手才发现,实验报告背后是一整套从内核机制理解到代码验证的闭环。标题里的“操作系统实验报告”不是一份文档模板,而是一个信号:你需要把进程调度、内存管理、文件系统、并发控制这些抽象概念,用可运行的代码和可复现的数据讲清楚。我带过几届学生的实验课,最常见的翻车场景是:代码跑通了,但报告里说不清为什么这么设计,参数为什么取这个值,换个输入会怎样。这份笔记面向两类人:正在做操作系统实验、需要把过程整理成可交付报告的学生,以及想通过实验反补理论短板的开发者。接下来我会按“先立住理论、再动手复现、最后避坑”的顺序,把一份能站得住脚的操作系统实验报告拆开讲。
2. 进程调度实验:从FCFS到时间片轮转的代码复现
2.1 为什么先做调度实验:它把“并发”变成可观测数据
操作系统实验里,进程调度是最适合入门的模块。原因很直接:它不需要你改内核源码,也不需要特殊硬件,用一门通用语言就能模拟出调度器的核心逻辑。你写一个进程队列,给每个进程标注到达时间、服务时间,然后按不同策略选下一个执行的进程,统计周转时间、带权周转时间、等待时间。这些数字就是报告里最硬的内容。
我一般会让学生先实现FCFS(先来先服务),再实现SJF(短作业优先),最后做时间片轮转。顺序不能反。FCFS的代码最短,但能暴露一个关键问题:长作业先到会把短作业堵死,带权周转时间急剧恶化。SJF能改善平均周转时间,但需要预知服务时间,现实中往往只能估算。时间片轮转则引入时钟中断的概念,时间片大小的选择直接决定上下文切换开销和响应速度的平衡。
实验报告里必须有一张对比表,列出三种策略下每个进程的完成时间、周转时间、带权周转时间,以及平均值。没有这张表,报告就只是代码堆砌。
2.2 用Python模拟调度器:核心数据结构与调度循环
下面这段代码实现了一个通用的调度框架,支持FCFS、SJF和时间片轮转三种策略。你可以直接复制运行,然后按自己的实验要求调整进程列表。
import collections class Process: def __init__(self, pid, arrive, burst): self.pid = pid # 进程标识 self.arrive = arrive # 到达时间 self.burst = burst # 服务时间 self.remain = burst # 剩余服务时间(用于时间片轮转) self.start = None # 首次开始执行时间 self.finish = None # 完成时间 def fcfs(processes): """先来先服务:按到达时间排序,依次执行""" procs = sorted(processes, key=lambda p: p.arrive) current_time = 0 for p in procs: if current_time < p.arrive: current_time = p.arrive # CPU空闲,跳到下一个到达时间 p.start = current_time current_time += p.burst p.finish = current_time return procs def sjf(processes): """短作业优先(非抢占):每次从已到达的进程里选服务时间最短的""" procs = sorted(processes, key=lambda p: p.arrive) current_time = 0 completed = [] ready = [] i = 0 while len(completed) < len(procs): # 把所有已到达的进程加入就绪队列 while i < len(procs) and procs[i].arrive <= current_time: ready.append(procs[i]) i += 1 if not ready: current_time = procs[i].arrive continue # 选服务时间最短的 ready.sort(key=lambda p: p.burst) p = ready.pop(0) p.start = current_time current_time += p.burst p.finish = current_time completed.append(p) return completed def rr(processes, quantum): """时间片轮转:每个进程最多执行quantum时间,未完成则回到队尾""" procs = sorted(processes, key=lambda p: p.arrive) current_time = 0 queue = collections.deque() completed = [] i = 0 # 先把第一个到达的进程入队 while i < len(procs) and procs[i].arrive <= current_time: queue.append(procs[i]) i += 1 while queue or i < len(procs): if not queue: current_time = procs[i].arrive while i < len(procs) and procs[i].arrive <= current_time: queue.append(procs[i]) i += 1 p = queue.popleft() if p.start is None: p.start = current_time run_time = min(quantum, p.remain) current_time += run_time p.remain -= run_time # 在进程执行期间到达的新进程入队 while i < len(procs) and procs[i].arrive <= current_time: queue.append(procs[i]) i += 1 if p.remain == 0: p.finish = current_time completed.append(p) else: queue.append(p) return completed def print_stats(procs, name): print(f"--- {name} ---") total_turnaround = 0 total_weighted = 0 for p in procs: turnaround = p.finish - p.arrive weighted = turnaround / p.burst total_turnaround += turnaround total_weighted += weighted print(f"P{p.pid}: 到达={p.arrive}, 服务={p.burst}, 完成={p.finish}, " f"周转={turnaround}, 带权周转={weighted:.2f}") n = len(procs) print(f"平均周转时间={total_turnaround/n:.2f}, " f"平均带权周转时间={total_weighted/n:.2f}\n") if __name__ == "__main__": # 构造一组测试进程:到达时间和服务时间 test_procs = [ Process(1, 0, 7), Process(2, 2, 4), Process(3, 4, 1), Process(4, 5, 4), ] # 每次实验前重新构造,避免状态污染 print_stats(fcfs([Process(p.pid, p.arrive, p.burst) for p in test_procs]), "FCFS") print_stats(sjf([Process(p.pid, p.arrive, p.burst) for p in test_procs]), "SJF") print_stats(rr([Process(p.pid, p.arrive, p.burst) for p in test_procs], quantum=2), "RR(q=2)")这段代码的关键点有三个。第一,Process类里用remain字段记录剩余服务时间,时间片轮转靠它判断进程是否完成。第二,SJF的就绪队列每次重新排序,选burst最小的进程,这模拟了非抢占式短作业优先。第三,时间片轮转里,进程执行期间新到达的进程必须及时入队,否则统计结果会偏。quantum参数就是时间片大小,你可以改成1、2、4,观察平均带权周转时间的变化。一般来说,时间片越小,响应越快,但上下文切换次数增加;时间片越大,退化成FCFS的趋势越明显。
2.3 实验报告里必须写清的三个参数
第一个参数是进程数量。少于3个进程,调度策略的差异不明显;多于8个,手工分析变得困难。我建议用4到6个进程,到达时间错开,服务时间有长有短。
第二个参数是时间片大小。在时间片轮转实验里,时间片取所有进程服务时间的最大公约数附近,或者取平均服务时间的一半。比如服务时间是7、4、1、4,时间片取2比较合适。报告里要写清楚为什么取这个值,以及取1和取4时结果怎么变。
第三个参数是到达时间的分布。如果所有进程同时到达,FCFS和SJF的差异只体现在服务时间排序上;如果到达时间分散,才能观察到CPU空闲和就绪队列动态变化。报告里至少给一组“到达时间递增”的输入,再给一组“短作业后到达”的输入,对比调度策略的适应性。
3. 内存管理实验:页面置换算法的命中率对比与实现
3.1 为什么页面置换是内存管理实验的核心
内存管理实验通常围绕分页机制展开,而页面置换算法是其中最能量化评估的部分。你给一个页面访问序列,比如7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1,然后模拟OPT、FIFO、LRU三种算法,统计缺页次数和缺页率。这个实验的价值在于:它让你直观看到“理想算法”和“实际可实现算法”之间的差距。
OPT(最优置换)需要预知未来访问序列,现实中无法实现,但它是理论下界。FIFO(先进先出)实现最简单,但会出现Belady异常——增加物理块数,缺页率反而上升。LRU(最近最少使用)用栈或链表维护访问顺序,性能接近OPT,但硬件支持成本高。实验报告里必须包含这三者的对比,并解释为什么LRU在实际系统中更常用。
3.2 用Python实现三种置换算法并统计缺页率
def opt(pages, frames): """最优置换:淘汰未来最长时间不被访问的页面""" memory = [] faults = 0 for i, page in enumerate(pages): if page in memory: continue faults += 1 if len(memory) < frames: memory.append(page) else: # 找未来最远才被访问的页面 farthest = -1 victim = None for m in memory: try: next_use = pages[i+1:].index(m) except ValueError: next_use = float('inf') # 之后不再使用 if next_use > farthest: farthest = next_use victim = m memory.remove(victim) memory.append(page) return faults def fifo(pages, frames): """先进先出:淘汰最早进入内存的页面""" memory = [] faults = 0 for page in pages: if page in memory: continue faults += 1 if len(memory) < frames: memory.append(page) else: memory.pop(0) # 淘汰队首 memory.append(page) return faults def lru(pages, frames): """最近最少使用:淘汰最久未被访问的页面""" memory = [] faults = 0 for page in pages: if page in memory: memory.remove(page) # 命中时移到末尾表示最近使用 memory.append(page) continue faults += 1 if len(memory) < frames: memory.append(page) else: memory.pop(0) # 淘汰最久未使用的 memory.append(page) return faults if __name__ == "__main__": access_sequence = [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] for frames in [3, 4]: print(f"物理块数={frames}") print(f"OPT 缺页次数={opt(access_sequence, frames)}") print(f"FIFO 缺页次数={fifo(access_sequence, frames)}") print(f"LRU 缺页次数={lru(access_sequence, frames)}") print()代码逻辑很直白。OPT在需要淘汰时,遍历当前内存中的每个页面,计算它在未来第一次出现的位置,选位置最远的淘汰。FIFO用列表模拟队列,pop(0)淘汰最早进入的。LRU在命中时把页面移到列表末尾,淘汰时取列表头部。注意LRU的memory.remove(page)和append(page)操作,这保证了列表顺序始终是“从最久未使用到最近使用”。
运行结果会显示:物理块数为3时,OPT缺页9次,FIFO缺页15次,LRU缺页12次。物理块数增加到4时,FIFO缺页次数可能反而增加,这就是Belady异常。报告里要专门用一段解释这个现象:FIFO的淘汰策略与访问序列的局部性无关,增加块数可能打乱原有的淘汰节奏。
3.3 缺页率对比表怎么写才有说服力
表格不能只列缺页次数。我建议列五列:算法名称、物理块数、缺页次数、缺页率、命中率。缺页率等于缺页次数除以总访问次数。命中率等于1减去缺页率。然后加一行“相对OPT的差距”,用百分比表示。比如LRU在3块时缺页12次,OPT缺页9次,差距是33.3%。这个数字能说明LRU离理论最优有多远。
另外,报告里要给出至少两组不同的访问序列。一组是“局部性明显”的序列,比如反复访问少数几个页面;另一组是“循环扫描”的序列,比如0,1,2,3,0,1,2,3,...。局部性强的序列,LRU表现接近OPT;循环扫描序列,FIFO和LRU都可能表现很差。这种对比能体现你对算法适用场景的理解。
4. 文件系统实验:用系统调用模拟目录树与文件读写
4.1 文件系统实验的两种做法:模拟实现与真实调用
文件系统实验通常有两种路径。一种是纯模拟:用结构体或类定义目录项、索引节点、超级块,在内存里构建一棵目录树,实现创建、删除、读写、列目录等操作。另一种是调用真实操作系统的API,比如Linux下的open、read、write、mkdir、opendir,观察文件在磁盘上的组织方式。两种做法各有价值:模拟实现帮你理解文件系统的内部数据结构,真实调用帮你掌握系统编程接口。
我一般建议先做模拟实现,因为可控性强,不需要特殊权限。模拟实现的核心是设计一个目录项结构,包含文件名、文件类型(普通文件或目录)、指向数据块的指针或索引节点编号。然后实现路径解析函数,把/home/user/test.txt这样的路径拆成各级目录名,逐级查找。
4.2 用Python构建内存目录树:核心类与路径解析
class INode: """索引节点:存储文件元数据和数据""" def __init__(self, name, is_dir=False): self.name = name self.is_dir = is_dir self.data = "" if not is_dir else None self.children = {} if is_dir else None # 目录项:名字到INode的映射 class FileSystem: def __init__(self): self.root = INode("/", is_dir=True) self.current = self.root def _resolve(self, path): """解析路径,返回目标INode。支持绝对路径和相对路径""" if path.startswith("/"): node = self.root parts = path.strip("/").split("/") if path.strip("/") else [] else: node = self.current parts = path.split("/") if path else [] for part in parts: if part == "" or part == ".": continue if part == "..": # 简化处理:不实现父目录指针,仅示意 continue if not node.is_dir or part not in node.children: return None node = node.children[part] return node def mkdir(self, path): """创建目录""" parent_path = "/".join(path.strip("/").split("/")[:-1]) dir_name = path.strip("/").split("/")[-1] parent = self._resolve(parent_path) if parent_path else self.current if parent is None or not parent.is_dir: print(f"mkdir: 无法创建 {path},父目录不存在") return if dir_name in parent.children: print(f"mkdir: {path} 已存在") return parent.children[dir_name] = INode(dir_name, is_dir=True) print(f"目录 {path} 创建成功") def create(self, path): """创建空文件""" parent_path = "/".join(path.strip("/").split("/")[:-1]) file_name = path.strip("/").split("/")[-1] parent = self._resolve(parent_path) if parent_path else self.current if parent is None or not parent.is_dir: print(f"create: 无法创建 {path},父目录不存在") return if file_name in parent.children: print(f"create: {path} 已存在") return parent.children[file_name] = INode(file_name, is_dir=False) print(f"文件 {path} 创建成功") def write(self, path, content): """写入文件内容(覆盖)""" node = self._resolve(path) if node is None or node.is_dir: print(f"write: {path} 不是有效文件") return node.data = content print(f"写入 {path}: {len(content)} 字节") def read(self, path): """读取文件内容""" node = self._resolve(path) if node is None or node.is_dir: print(f"read: {path} 不是有效文件") return None return node.data def ls(self, path="."): """列出目录内容""" node = self._resolve(path) if node is None or not node.is_dir: print(f"ls: {path} 不是目录") return for name, child in node.children.items(): suffix = "/" if child.is_dir else "" print(f" {name}{suffix}") if __name__ == "__main__": fs = FileSystem() fs.mkdir("/home") fs.mkdir("/home/user") fs.create("/home/user/test.txt") fs.write("/home/user/test.txt", "hello os experiment") print("读取内容:", fs.read("/home/user/test.txt")) print("列出 /home/user:") fs.ls("/home/user")这段代码模拟了一个极简的树形文件系统。INode类区分目录和文件:目录的children是字典,文件的data是字符串。_resolve方法负责路径解析,支持绝对路径和相对路径,遇到不存在的路径返回None。mkdir和create先解析父目录,再在父目录的children里添加新节点。write和read直接操作data字段。ls遍历children并打印。
报告里要写清楚这个模拟系统与真实文件系统的差距:没有磁盘块分配、没有索引节点编号、没有权限控制、没有硬链接和软链接。但核心的“目录树+路径解析+文件数据存储”已经体现出来了。你可以在此基础上增加删除操作、重命名操作,或者把data改成按块存储,模拟磁盘空间管理。
4.3 真实系统调用版:用os模块操作实际文件
如果你需要在Linux环境下做真实调用实验,下面这段代码展示了如何用Python的os模块创建目录、写入文件、读取文件、遍历目录。
import os base = "/tmp/os_exp_demo" # 实验用临时目录,避免污染用户目录 # 创建多级目录 os.makedirs(os.path.join(base, "home", "user"), exist_ok=True) # 写入文件 file_path = os.path.join(base, "home", "user", "data.txt") with open(file_path, "w") as f: f.write("operating system experiment\n") # 读取文件 with open(file_path, "r") as f: content = f.read() print("文件内容:", content.strip()) # 遍历目录树 for root, dirs, files in os.walk(base): level = root.replace(base, "").count(os.sep) indent = " " * level print(f"{indent}{os.path.basename(root)}/") for file in files: print(f"{indent} {file}") # 获取文件状态 stat_info = os.stat(file_path) print(f"文件大小: {stat_info.st_size} 字节") print(f"索引节点号: {stat_info.st_ino}")这段代码的关键是os.makedirs的exist_ok=True参数,避免目录已存在时报错。os.walk递归遍历目录树,root是当前目录路径,dirs是子目录列表,files是文件列表。os.stat返回文件的元数据,其中st_ino是索引节点号,st_size是文件大小。报告里可以把st_ino和模拟实现里的“索引节点”概念对应起来,说明真实文件系统确实用索引节点管理文件。
5. 并发与同步实验:生产者消费者问题的信号量实现与死锁排查
5.1 为什么生产者消费者是并发实验的必做项
并发与同步是操作系统实验里最容易出玄学问题的部分。生产者消费者问题把“共享缓冲区、互斥访问、条件等待”三个要素集中在一起,用信号量或管程实现。你写一个固定大小的缓冲区,生产者线程往里放数据,消费者线程从里取数据,用信号量控制空槽位和满槽位数量,再用一个互斥信号量保护缓冲区的访问。
这个实验的翻车点非常多:信号量初值设错、P操作和V操作顺序颠倒、忘记释放互斥锁、缓冲区索引越界。每一个都能导致死锁、数据竞争或程序挂起。报告里必须记录你遇到的至少一个并发bug,以及怎么定位和修复的。
5.2 用Python线程和信号量实现生产者消费者
import threading import time import random BUFFER_SIZE = 5 buffer = [] mutex = threading.Semaphore(1) # 互斥访问缓冲区 empty = threading.Semaphore(BUFFER_SIZE) # 空槽位数量 full = threading.Semaphore(0) # 满槽位数量 def producer(pid): for i in range(5): item = f"产品-{pid}-{i}" time.sleep(random.uniform(0.1, 0.3)) # 模拟生产耗时 empty.acquire() # 等待空槽位 mutex.acquire() # 进入临界区 buffer.append(item) print(f"生产者{pid} 放入 {item},缓冲区: {buffer}") mutex.release() # 离开临界区 full.release() # 增加满槽位 def consumer(cid): for i in range(5): time.sleep(random.uniform(0.1, 0.3)) # 模拟消费耗时 full.acquire() # 等待满槽位 mutex.acquire() # 进入临界区 item = buffer.pop(0) print(f"消费者{cid} 取出 {item},缓冲区: {buffer}") mutex.release() # 离开临界区 empty.release() # 增加空槽位 if __name__ == "__main__": threads = [] for i in range(2): t = threading.Thread(target=producer, args=(i,)) threads.append(t) for i in range(2): t = threading.Thread(target=consumer, args=(i,)) threads.append(t) for t in threads: t.start() for t in threads: t.join() print("所有线程结束")这段代码里,empty信号量初值是缓冲区大小,表示初始有5个空槽位。full初值是0,表示没有产品。生产者先empty.acquire()等待空槽位,再mutex.acquire()进入临界区,放入产品后mutex.release(),最后full.release()通知消费者。消费者顺序相反:先full.acquire()等待产品,再mutex.acquire(),取出后mutex.release(),最后empty.release()。
如果交换empty.acquire()和mutex.acquire()的顺序,比如先拿互斥锁再等空槽位,当缓冲区满时,生产者持有互斥锁等待空槽位,消费者无法进入临界区取产品,也无法释放空槽位,直接死锁。这就是经典的“锁顺序错误导致死锁”。报告里要画出这个死锁的等待关系:生产者持有mutex,等待empty;消费者等待mutex,无法释放empty。
5.3 死锁排查:用线程转储和日志定位阻塞点
Python线程死锁不会自动报错,程序只是挂起。排查方法是:在关键位置加日志,打印每个信号量acquire前后的状态。或者用threading.enumerate()打印所有活跃线程,再用sys._current_frames()获取每个线程的调用栈。
import threading import sys import traceback def dump_threads(): """打印所有线程的调用栈,用于死锁排查""" frames = sys._current_frames() for thread in threading.enumerate(): print(f"线程: {thread.name}, 状态: {thread.is_alive()}") if thread.ident in frames: traceback.print_stack(frames[thread.ident])在程序挂起后,从另一个终端发送信号触发dump_threads,或者直接在代码里加一个定时器,每隔几秒打印一次。调用栈会显示线程阻塞在哪个acquire调用上。如果两个线程互相等待对方持有的信号量,就是死锁。
报告里要写清楚:你用了什么方法复现死锁,日志里看到了什么,修改了哪一行代码,修改后程序是否正常结束。这种“现象→定位→修复→验证”的闭环,是操作系统实验报告最有价值的部分。
6. 实验报告避坑:从数据造假到环境差异的五个血泪教训
6.1 坑一:进程调度结果对不上,因为忽略了CPU空闲时间
现象:FCFS模拟结果里,第一个进程完成时间比预期早,后续进程的周转时间全部偏小。原因:代码在第一个进程到达前就把current_time从0开始累加,没有判断CPU是否空闲。如果第一个进程到达时间是3,CPU在前3个时间单位是空闲的,current_time应该跳到3再开始执行。解决:在调度循环里加一个判断,if current_time < p.arrive: current_time = p.arrive。这个坑在SJF和时间片轮转里同样存在,每次从就绪队列取进程前都要检查。
6.2 坑二:页面置换缺页率算错,因为没区分“命中但未更新顺序”
现象:LRU的缺页次数比预期高,明明页面在内存里却算成了缺页。原因:LRU在命中时也需要更新访问顺序,把命中的页面移到“最近使用”的位置。如果只在缺页时才操作内存列表,命中时什么都不做,LRU就退化成了FIFO。解决:在if page in memory分支里,先memory.remove(page)再memory.append(page),确保列表末尾始终是最近使用的页面。
6.3 坑三:文件系统路径解析失败,因为没处理尾部斜杠和空路径
现象:mkdir("/home/user/")报错“父目录不存在”。原因:路径按/分割后,末尾多了一个空字符串,导致父目录解析时多了一层不存在的目录。解决:在分割后过滤掉空字符串和.,或者用path.strip("/")去掉首尾斜杠再分割。另外,空路径""应该解析为当前目录,而不是根目录。报告里要列出你测试过的边界路径:/、/home、/home/、home/user、../home。
6.4 坑四:生产者消费者死锁,因为信号量P操作顺序反了
现象:程序运行几秒后挂起,缓冲区满时生产者和消费者都不动。原因:生产者先mutex.acquire()再empty.acquire(),消费者先mutex.acquire()再full.acquire()。当缓冲区满时,生产者持有mutex等待empty,消费者等待mutex无法释放full,循环等待。解决:资源信号量的P操作必须在互斥信号量之前。生产者先等空槽位,再拿互斥锁;消费者先等满槽位,再拿互斥锁。这个顺序不能颠倒。
6.5 坑五:实验数据在不同机器上不一致,因为线程调度和时钟精度不同
现象:同一份生产者消费者代码,在A机器上输出顺序是“生产-消费-生产-消费”,在B机器上变成“生产-生产-消费-消费”。原因:线程调度由操作系统决定,time.sleep的精度也受系统负载影响。解决:报告里不要写“输出顺序必须如何”,而是写“在多次运行中观察到两种顺序,说明线程调度的不确定性”。如果要可复现的结果,用信号量或事件强制同步,比如生产者放入产品后full.release(),消费者取出后empty.release(),但不要依赖print的顺序。另外,统计平均周转时间时,多跑几次取平均值,减少单次波动的影响。
7. 把实验报告写成可复现的技术笔记:我的三个习惯
第一个习惯:每个实验先写“最小可运行版本”,再逐步加功能。比如进程调度,先写FCFS,只统计完成时间;跑通后再加周转时间和带权周转时间;再加SJF和时间片轮转。这样每一步都有可验证的输出,不会一次性堆几百行代码然后面对一堆报错。报告里可以按这个递进过程组织,读者能跟着你的思路走。
第二个习惯:所有参数都记录在代码开头的配置区,不散落在各处。比如进程列表、时间片大小、物理块数、缓冲区大小,全部用变量定义在文件顶部。报告里附上配置区的截图或代码块,读者改一个数字就能复现你的实验。我见过太多报告,参数藏在循环里,别人根本没法复现。
第三个习惯:每次实验至少跑三组输入,一组正常、一组边界、一组异常。正常输入验证基本功能,边界输入测试空队列、满缓冲区、单进程,异常输入测试路径不存在、信号量初值为0。报告里用表格列出三组输入下的输出差异,这比只给一组“完美结果”有说服力得多。
最后一个技巧:用time.perf_counter()替代time.time()做性能统计。perf_counter返回高精度计时器,适合测量短时间间隔。在调度实验里,如果你要比较不同策略的实际执行时间,用perf_counter能减少误差。但注意,模拟实验的“执行时间”主要是CPU计算时间,和真实操作系统的调度开销不是一回事,报告里要区分清楚。
我在整理这些实验时,最大的教训是:不要为了报告好看而修改数据。有一次我为了让LRU的缺页率看起来更接近OPT,手动调整了访问序列,结果答辩时被问“为什么序列和题目要求的不一样”,当场翻车。后来我养成习惯,所有数据从代码输出直接复制,原始日志和最终报告一起保存。希望帮到你。
本文还有配套的精品资源,点击获取