Python模拟假脱机:输入输出井与多线程缓冲队列实现
发布时间:2026/10/15 17:31:11 锦皓数字建站

简介这是一份面向操作系统课程学习与实验的Spooling假脱机输入输出模拟技术资源包。其核心设计包含两个请求输出进程与一个Spooling输出进程通过输入井、输出井和后台处理程序将数据暂存到磁盘并调度输出实现多进程对慢速I/O设备的共享有助于理解多进程并发、缓冲机制与虚拟设备原理。资源共11个文件包括Java源程序及对应class字节码、4张运行界面截图和1份设计报告压缩包整体仅279KB适合对照代码与截图还原实验过程。目前已有441人学习下载。学习时可结合源码阅读进程创建与缓冲区管理逻辑借助截图比对实际运行界面再通过设计报告梳理系统架构与实现步骤报告中的性能测试与细节说明也能帮助深入掌握spooling技术在打印机共享等场景下的应用对操作系统课程设计或原理实验有直接参考价值。1. 假脱机模拟技术把慢外设从关键路径上摘下来的那套机制在一次共享打印场景里同事连续提交了两个打印任务后一个任务并不会真的等前一个任务完全打完才进入队列它会先落在一个缓冲区里让前台操作马上返回。这套机制就是 spooling 假脱机输入输出模拟技术要讲的核心用磁盘上的输入井、输出井和缓冲队列把慢速设备与快速 CPU 之间的速度差抹平。模拟技术则把这些实体搬进代码用线程、队列和延迟参数复现整个过程。适合课程设计、设备驱动调试以及需要设计嵌入式打印缓冲队列的一线开发者。2. 假脱机的实体与数据流井、缓冲区、共享队列是如何协同工作的假脱机系统里最核心的三个实体是输入井、输出井和缓冲区。输入井和输出井是磁盘上的两块编号存储区缓冲区是内存里的一小块暂存区。输入设备的数据先进入输入缓冲区再由一个输入进程把数据成块写入输入井CPU 需要数据时直接从输入井中读取而不是向外部设备发起等待。CPU 处理完毕把结果写入输出井输出进程再把输出井中的数据送到输出缓冲区最后交给输出设备。整个链路看起来像一条流水线每个环节的阻塞都不会拖垮上游操作。2.1 为什么用“井”而不是普通队列可寻址与共享是关键模拟假脱机时最容易犯的错误是简单用一个 FIFO 队列替掉输入井。队列能表达顺序却表达不了井的一个重要特性可寻址。输入井里保存的是按块编号的数据CPU 可以从任意块号开始读取不同进程可以共享同一批输入数据设备写满一块后通知等待者。这种“写端连续编号、读端按需取块”的模型只有在代码里显式维护块号和占用位才能还原。我做一个模拟器时会把井设计成两层一层是存储区列表或字典一层是分配状态表记录每块是否被占用。分配状态表决定井的容量存储区决定数据实体。队列只承担“等待者排队”的功能不承担“数据存储”的功能。这样划分后井的高水位、块占用率、等待时长都能单独统计这是直接用一个队列模块模拟所做不到的。其次井的容量是有限且共享的多个输入进程会同时写入不同块。真实系统中井是磁盘的一块物理区域所有设备进程都能访问。模拟里如果把井做成一个独占的数据结构并发写入时就会丢失“多个源同时灌入”的语义。因此我在框架里让输入端通过锁保护写位置读取端通过锁保护读位置分开加锁而不是锁整个井这样更贴近真实磁盘的读写行为。2.2 输入井与输出井的工作步骤从设备到井再到 CPU输入井的写入流程可以拆成三步。第一步设备把一条数据送入输入缓冲区缓冲区小了就等第二步输入进程把缓冲区里的数据按块复制到输入井并登记块号和大小第三步如果输入井已满就暂停设备直到 CPU 或输出进程读出数据释放块。这里的关键点是输入缓冲区只做短暂停留它的存在意义是把设备的中断频率降下来让设备按块突发写入磁盘。输出井的路径反过来。CPU 把计算或加工后的结果写入输出井登记到可用块列表上输出进程发现井里有数据先把它们搬到输出缓冲区再由缓冲区缓慢交给输出设备。打印场景里最典型的就是“假脱机打印”应用层调用打印函数后只负责把数据提交到输出井提交完成立即返回真正的打印动作由后台进程慢慢执行用户不需要盯着打印机发呆。模拟时我会把这两个流程做成两条独立线程一条线程负责“设备到输入井”另一条负责“输出井到输出设备”。中间再加若干条 CPU 工作线程处理井与井之间的转换。这样四个角色互相独立任意一条线程运行慢都不会阻塞其他线程的启动这正是假脱机技术名字里“同时联机操作”的直接体现。2.3 模拟时必须保留的三个边界条件第一磁盘访问延迟必须保留。假脱机的“井”在磁盘上读写井与读写内存缓冲区的代价完全不同。模拟里如果直接读写内存列表而不加任何延迟计算出的吞吐量会比真实系统乐观很多。第二井容量接近上限时的背压行为必须保留。井写满后输入设备要停等这个停等时间如果没有模拟就看不到假脱机系统在实际过载时如何反压设备。第三断电或异常重启后的数据恢复可以忽略。绝大多数教学和原型验证不涉及崩溃一致性没必要在这个层面增加复杂度否则模拟器会淹没在日志和恢复逻辑里。3. 搭建假脱机模拟框架井、缓冲与三条处理线程的 Python 骨架模拟器用 Python 标准库就能搭起来不依赖第三方包。我采用 threading 加 queue 两个模块原因是假脱机的并发特征正好落在生产者消费者模型上输入设备是生产者输出设备是消费者CPU 工作线程夹在中间。下面这个骨架是我常用的起点核心思路是把井封装成带延迟和容量的类所有访问统一走 write 和 read 方法方便日后替换成真实磁盘接口。3.1 核心类井的容量、访问延迟与块状态import threading import time from dataclasses import dataclass from queue import Queue dataclass class SpoolPage: job_id: int page_no: int payload: bytes class SpoolWell: 模拟磁盘上划分出的一个井容量用 maxsize 限制读取和写入带磁盘延迟。 def __init__(self, capacity, access_ms5): self.q Queue(maxsizecapacity) self.access_ms access_ms def write(self, page): time.sleep(self.access_ms / 1000) self.q.put(page, blockTrue) return page.page_no def read(self): time.sleep(self.access_ms / 1000) return self.q.get(blockTrue)井类里我保留了三个要素容量、延迟、访问方式。容量通过 Queue 的 maxsize 参数实现maxsize 等于井能容纳的块数延迟用 access_ms 模拟一次磁盘读写耗时访问方式统一走 put 和 get。blockTrue 表示井满时写入方阻塞、井空时读取方阻塞这两个阻塞行为正好对应现实中设备停等和 CPU 空转。SpoolPage 的 payload 用 bytes 类型方便后续扩展为文件内容或日志分片。这个类的设计意图是让调用方感知不到底层是队列还是真的磁盘文件。实际落地时如果要把井落到磁盘只需要把 write 改成按编号写文件、read 改成按编号读文件接口不变上层线程全部不用改。这是我的习惯先定义接口语义再决定存储实现避免模拟器后期重构时动到线程调度代码。3.2 输入设备线程间歇性生产页并写入输入井def input_device(well, job_count, pages_per_job, gap_ms80): for job_id in range(job_count): for page_no in range(pages_per_job): page SpoolPage( job_idjob_id, page_nopage_no, payload(fjob{job_id} page{page_no}).encode() ) well.write(page) time.sleep(gap_ms / 1000) print([input] 全部页已进入输入井)输入设备线程按固定时间间隔生成页。gap_ms 表示设备产生两页数据之间的间隔调大模拟慢速输入设备调小模拟高速扫描仪。每个作业的页数由 pages_per_job 控制job_count 控制作业总数。页的 payload 在这里只是占位数据真实项目中可以是图像块、日志行或数据库记录。这个线程执行完所有写入后打印结束标记用于后续验证输入井的内容是否完整。参数设置的一个经验是 gap_ms 不要小于 access_ms否则输入设备的生产速度高于井的磁盘写入速度井会一直处于满水位观察不到正常的空闲周期。想要制造压力场景时把 gap_ms 调低到 access_ms 的一半就能看到 input_device 线程卡在 write 上这就是背压现象是后续分析们需要的第一手数据。3.3 CPU 工作线程与输出设备线程两段消费逻辑的分工def cpu_worker(in_well, out_well, process_ms20): while True: try: page in_well.read() except Exception: break time.sleep(process_ms / 1000) out_well.write(page) def output_device(out_well, total_pages, print_ms100, stop_eventNone): printed 0 while printed total_pages: page out_well.read() time.sleep(print_ms / 1000) printed 1 print([output] 打印完成共, printed, 页)cpu_worker 扮演真实系统里处理数据的核心它从输入井取页、模拟处理耗时、把结果写入输出井。process_ms 越大说明数据加工越重真实场景里对应渲染、编解码、格式转换这些环节。output_device 是输出端的慢设备print_ms 表示打印一页所需时间这个值通常比 process_ms 大很多才能体现输出设备是瓶颈。这里需要特别注意 CPU 工作线程的退出条件。代码里我用 try except 包住 read是为了处理线程停止时 input 端不再产生数据的情况。实际项目中更稳妥的做法是传递一个 threading.Event 作为停止信号井读取方在事件触发时抛出一个自定义异常退出循环。输出设备线程则通过 printed 计数控制退出计数达到总页数时打印完成信息并返回避免线程泄漏。4. 跑通最小假脱机模拟任务编排、参数调节与结果验证骨架搭建好以后下一步是把所有线程组装起来加上主线程调度形成一次完整的模拟运行。最小化的编排包括创建两个井、启动一个输入设备线程、一条或两条 CPU 工作线程、一条输出设备线程然后等待若干秒收集结果。下面是完整的main函数组织方式以及我用来验证模拟器行为是否正确的检查点。4.1 主流程清单线程启动、睡眠等待与结果输出def main(): in_well SpoolWell(capacity16, access_ms5) out_well SpoolWell(capacity16, access_ms5) t_input threading.Thread( targetinput_device, args(in_well, 4, 3, 80) ) cpus [ threading.Thread(targetcpu_worker, args(in_well, out_well, 20)), threading.Thread(targetcpu_worker, args(in_well, out_well, 20)), ] t_output threading.Thread( targetoutput_device, args(out_well, 12, 100) ) t_input.start() for t in cpus: t.start() t_output.start() t_input.join() for t in cpus: t.join() t_output.join() if __name__ __main__: main()主流程里输入设备线程生成 4 个作业、每个作业 3 页总共 12 页输入井和输出井容量都设为 16page 粒度下都够用。两条 CPU 工作线程同时消费输入井模拟多核并行处理。输出设备线程固定消费 12 页后退出。join 的顺序是输入线程先结束、CPU 线程随后、输出线程最后对应数据从源头流到终点的自然顺序。运行前可以估算理想耗时输入设备 12 页乘以 80 毫秒约 0.96 秒CPU 侧 12 页分两条线程处理每页 20 毫秒约 0.12 秒输出设备 12 页乘以 100 毫秒约 1.2 秒。整个流程大约 1.5 到 2 秒结束。如果实际运行时间远远小于这个估算说明 access_ms 的磁盘延迟被线程并发掩盖了如果远远大于说明井的读写锁或队列阻塞出现了问题。这个粗略估算是我验证模拟器是否正常的第一步。4.2 三个必调参数井容量、磁盘延迟与设备速率参数设置位置作用调参方向井容量SpoolWell(capacity...)控制井内最多存放的页数调小可观察背压和阻塞磁盘延迟SpoolWell(access_ms...)模拟每次井读写耗时调大后 CPU 空转时间上升设备速率gap_ms / print_ms控制生产与消费节奏print_ms 远大于 gap_ms 时形成输出瓶颈这三个参数里最容易忽略的是井容量与总页数的关系。井容量只要大于瞬时在途页数整个模拟就退化成普通队列背压现象根本不出现。想验证假脱机系统在高负载下的表现要把井容量调到小于页面生成峰值比如容量设为 3、输入设备瞬间生成 12 页这时输入线程会卡在 write 上井空余块数为 0CPU 端才能体现出“排队等待磁盘空间”的过程。print_ms 是另一个关键参数。它决定输出设备是否成为系统瓶颈。当 print_ms 为 100、CPU process_ms 为 20、输入 gap_ms 为 80 时输出端吞吐量约每秒 10 页输入端约每秒 12.5 页输出端略慢系统最终会表现为输出设备限制了整体完成时间。把 print_ms 调到 200 时输出端吞吐量降到每秒 5 页CPU 线程处理完后会在输出井处排队输出井很快被占满这是最常见的假脱机瓶颈状态。4.3 验证模拟器的三个观察点完成页数、线程退出与日志顺序运行一次后先看两个指标输出线程打印的完成页数是否为 12以及所有线程是否正常退出。完成页数不对说明井的写入或读取发生了丢数据常见的元凶是 CPU 工作线程提前退出或输出设备线程的 total_pages 与实际不一致。线程是否正常退出可以通过在 main 末尾打印一行标记来确认没有标记说明有线程阻塞在井的读写上。日志顺序也能暴露问题。正确的顺序是输入线程先结束并打印“全部页已进入输入井”之后输出线程打印“打印完成”。如果输出线程打印完成早于输入线程结束说明输出设备消费的数据并没有严格经过输入井的完整写入流程很可能是 CPU 工作线程偷跑了尚未生成的页或者输入页数计算错了。我通常会在每条线程的结束点打印时间戳用时间差验证链路每一段是否按预期工作。5. 假脱机模拟的四个常见坑容量设置、线程退出与背压失真模拟器翻车通常不是逻辑复杂而是参数和边界条件设置不当。我总结了四个反复踩过的坑每条都按现象、原因、解决三个环节记录方便直接对照排查。5.1 井容量设得过大背压现象完全消失把井容量设为 100而总页数只有 5结果输入线程单方面写完所有数据CPU 和输出设备慢慢消费整个模拟看起来一切正常但假脱机系统最重要的“井满时设备停等”机制完全没有被触发。根本原因是容量大于在途数据峰值井永远不会满背压逻辑成了死代码。解决方法是把井容量降到与单批任务量级一致甚至小于峰值。比如制造 4 个作业同时提交的瞬间并发时容量设为 3就能稳定观察到写入阻塞还能统计设备停等时间。5.2 CPU 工作线程无法退出程序卡在 joinCPU 工作线程的 while True 里如果只靠 try except 捕获读取异常来退出当输入井所有数据被读完且输入线程已经结束时井为空read 方法会一直阻塞在 get 上主线程 join 永远等不到 CPU 线程结束。原因是井空时的阻塞语义与线程退出条件冲突。解决方式是引入一个终止事件输入线程结束后设置事件CPU 工作线程在每次循环开头检查事件状态事件被设置且井为空时主动 break。代码改为在 read 之前判断stop_event.is_set()能稳定退出。5.3 磁盘访问延迟被完全掩盖测出的吞吐量虚高井容量足够大、生产与消费节奏都不紧张时access_ms 的 5 毫秒延迟会被其他线程的处理时间完全覆盖统计结果中磁盘耗时占比为 0模拟器退化成纯内存队列。原因是队列访问的延迟与业务处理延迟在同一量级时随机波动会互相抵消。解决方法是把 access_ms 放大到业务处理时间的 2 到 3 倍或者统计每个井的读写次数和总耗时单独打印出“井读写耗时占总运行时间的百分比”只有这个占比大于 10% 时磁盘模拟才有意义。我一般会在井类里维护一个累计访问次数在结束时输出平均延迟。5.4 输出井写满后 CPU 线程空转模拟器看起来卡死当输出设备很慢、输出井容量又小的时候CPU 工作线程处理完数据后全部阻塞在输出井的 write 上输入井的数据不再被消费输入设备也堵在输入井的 write 上三条线程全部阻塞运行时间无限拉长看起来像死锁。原因是输出设备速率低于 CPU 处理速率时全系统的瓶颈集中在输出井背压反向传导到了输入端。解决方法是先做容量规划输出井容量要大于 CPU 线程数乘以单次输出页数否则多条 CPU 线程同时写满井后互相等待。另外可在输出设备线程中加大 print_ms并观察 cpu_worker 阻塞在哪个井用日志定位反向背压的具体位置。6. 进阶给假脱机模拟器加优先级调度实测高优任务的等待时间变化在基础框架上增加优先级是性价比最高的扩展。真实打印系统里紧急作业往往要插队模拟时可以在井的存储层区分普通页与紧急页而不是直接改队列顺序。我的做法是把 SpoolWell 内部实现从单队列升级成双队列结构普通队列与优先队列。写入时按页携带的 priority 字段选择进哪个队列读取时优先从高优队列取高优队列为空再取普通队列。这个改动只影响井类内部线程调用方式不变。class PrioWell: def __init__(self, capacity_high, capacity_normal): self.high Queue(maxsizecapacity_high) self.normal Queue(maxsizecapacity_normal) def write(self, page, priority0): if priority 0: self.high.put(page, blockTrue) else: self.normal.put(page, blockTrue) def read(self): try: return self.high.get_nowait() except Exception: return self.normal.get(blockTrue)用这个实现跑同一组参数分别统计高优作业与普通作业从输入设备生成到输出完成的端到端时间高优任务在输出设备繁忙时会明显缩短等待时间。进阶验证指标是“高优任务完成时长占比”理想状态下高优任务的完成时间不应随普通任务数量线性增长。我在做类似模拟器时一般会跑三组对照实验无优先级、高优比例 10%、高优比例 50%把三组完成时间画成对比记录就能定量看到调度策略的影响。这个方向比堆并发线程更有价值也是从“能跑”走向“能说明问题”的关键一步。希望这一套拆解能帮你在假脱机模拟上少走弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。