spooling假脱机模拟:2请求进程+1输出进程的并发控制与信号量实现

简介:这份资源围绕操作系统中的Spooling假脱机输入输出模拟技术展开,面向计算机专业学生及系统设计学习者,帮助理解如何通过磁盘缓冲将慢速I/O设备虚拟化,实现多进程共享打印机等外设。包内共11个文件,以5个class编译文件、1个java源程序、1个doc设计报告和4张jpg运行截图为主,源程序展示请求输出进程与spooling输出进程的协作逻辑,报告与截图则呈现设计思路和实际运行状态,压缩包约279KB。已有441人学习。读者可从中获得完整的进程模型实现、输入井与输出井的缓冲管理代码、设备驱动交互细节,以及可对照复现的实验报告与界面截图,适合课程设计、实验报告撰写和操作系统原理的深入理解。

1. 从一次打印队列卡死说起:spooling 假脱机到底在模拟什么

上周帮同事看一个模拟程序,现象很怪:两个请求输出的进程轮流往打印机写数据,结果第一个进程刚写完一半,第二个进程的数据就插进来了,打印出来的内容交错成一团乱码。他以为是并发锁没加对,查了半天才发现,问题出在没理解 spooling 假脱机输入输出模拟技术的核心——独占设备怎么被改造成共享设备。

这个资源就是围绕这个场景做的:用程序模拟 spooling 技术,具体配置是2 个请求输出进程 + 1 个 spooling 输出进程。它要解决的不是"怎么调用打印机 API",而是操作系统层面那个经典问题——多个进程同时要用一台独占设备时,怎么让它们不打架、不阻塞、还能按顺序输出。适合正在学操作系统原理、要做课程设计、或者想搞懂"假脱机"这三个字到底假在哪的从业者。下面我按自己拆这套东西的顺序,把原理、代码结构、参数设置和踩过的坑一条条讲清楚。

2. spooling 的三层结构:输入井、输出井和那张关键的表

2.1 为什么不能直接让进程抢打印机

先想清楚一个前提:打印机、磁带机这类设备,物理特性决定了同一时刻只能有一个进程真正操作它。如果放任 2 个请求输出进程直接去抢,要么加互斥锁让一个等另一个(进程阻塞,CPU 空转),要么不加锁导致输出交错(就是开头那个乱码现场)。

spooling 的思路是加一层中间缓冲:进程不直接碰设备,而是把要输出的数据先写到磁盘上的一个区域,再由一个专门的进程去读这个区域、真正驱动设备。这样请求输出的进程写完就走,不用等设备,设备那头也有序。这层磁盘缓冲,就是"井"。

2.2 输入井与输出井的分工

在模拟程序里,通常用两个数据结构来代表井:

结构作用模拟中的常见实现
输出井存放 2 个请求进程要打印的数据一个队列或数组,每个元素是一段待输出内容
输出进程从井里取数据、驱动"打印机"一个独立循环,按顺序消费队列
请求表记录哪个进程申请了输出、数据在哪结构体数组,含进程号、数据地址、长度、状态

输出井是共享资源,2 个请求进程都能往里写,所以对井的写入要互斥;输出进程从井里读,也要和写入互斥。但注意——互斥的是井,不是打印机。打印机只被输出进程一个人碰,天然不存在竞争。这就是 spooling 把独占设备"虚拟"成共享设备的关键:竞争被转移到了井上,而井是内存/磁盘结构,加锁成本远低于设备。

2.3 请求表:整个模拟的调度中枢

请求表是很多人写模拟时容易忽略的部分。它不只是记录,还承担状态流转:

// 请求表项结构,模拟 spooling 输出请求 typedef struct { int pid; // 请求进程号,1 或 2 int data_addr; // 数据在输出井中的起始位置 int length; // 数据长度 int state; // 0=空闲 1=已提交待输出 2=正在输出 3=已完成 } SpoolRequest; SpoolRequest req_table[MAX_REQ]; // 请求表 int out_well[WELL_SIZE]; // 输出井,模拟磁盘缓冲 int well_head, well_tail; // 井的读写指针

逻辑说明:pid区分是哪个请求进程提交的,state让输出进程知道该处理哪条。well_head和well_tail构成一个环形缓冲,避免井无限增长。参数上,WELL_SIZE决定井能缓冲多少数据,太小会导致请求进程写不进去而等待,太大则失去"缓冲"的模拟意义,一般设成单次输出量的 3 到 5 倍比较合适。

提示:请求表的状态字段一定要在互斥区内更新,否则输出进程可能读到"半提交"的记录。

3. 2 个请求进程 + 1 个输出进程:用信号量把并发顺序管起来

3.1 信号量怎么设:三个就够

这套模拟的并发控制,核心是三个信号量:

sem_t mutex; // 保护输出井和请求表的互斥访问,初值 1 sem_t empty; // 井中空闲槽位数,初值 WELL_SIZE sem_t full; // 井中已提交待输出的记录数,初值 0

请求进程的流程是:申请一个空槽(P empty)→ 拿互斥锁(P mutex)→ 写数据进井、填请求表 → 释放锁(V mutex)→ 通知有数据了(V full)。输出进程反过来:P full → P mutex → 从井里取数据、更新请求表 → V mutex → V empty。

这样设计的好处是,2 个请求进程之间不需要直接互斥,它们通过 empty 和 full 这两个计数信号量自然排队。谁先拿到空槽谁先写,写完了输出进程按井里顺序取,天然保证输出有序。

3.2 请求进程的代码骨架

void *request_process(void *arg) { int pid = *(int *)arg; for (int i = 0; i < REQ_TIMES; i++) { sem_wait(&empty); // 等一个空槽 sem_wait(&mutex); // 进临界区 // 把本进程第 i 条数据写入输出井 out_well[well_tail] = pid * 100 + i; well_tail = (well_tail + 1) % WELL_SIZE; // 填请求表 req_table[req_idx].pid = pid; req_table[req_idx].state = 1; req_idx++; sem_post(&mutex); // 出临界区 sem_post(&full); // 通知输出进程 usleep(rand() % 100000); // 模拟请求间隔 } return NULL; }

逻辑说明:sem_wait(&empty)保证井满时请求进程会阻塞等待,不会覆盖未输出的数据。usleep那行模拟真实场景里请求不是连续不断的,加随机间隔能暴露调度顺序问题。参数REQ_TIMES控制每个请求进程提交多少次,设成 5 到 10 次既能看出交错效果,又不至于跑太久。

3.3 输出进程的消费逻辑

void *spool_output_process(void *arg) { while (output_count < TOTAL_REQ) { sem_wait(&full); // 等有数据可输出 sem_wait(&mutex); // 从井头取数据 int data = out_well[well_head]; well_head = (well_head + 1) % WELL_SIZE; // 更新请求表状态为"正在输出" req_table[out_idx].state = 2; sem_post(&mutex); sem_post(&empty); // 释放一个空槽 printf("输出进程打印: %d\n", data); // 模拟驱动打印机 req_table[out_idx].state = 3; out_idx++; output_count++; usleep(200000); // 模拟打印耗时 } return NULL; }

逻辑说明:输出进程是唯一的消费者,full为 0 时它会阻塞,不会空转。usleep(200000)模拟打印机的物理速度,这个值要比请求进程的间隔大,才能看出"井"起到的缓冲作用——请求进程可以快速写满井然后继续干别的,输出进程慢慢打。

注意:output_count和out_idx如果被多个线程访问,也要纳入互斥保护,别只锁了井忘了计数器。

4. 避坑排查:模拟跑不通时先看这五条

4.1 输出内容交错、顺序全乱

现象:打印出来的数据是1 2 1 2这种交错,而不是先来先出。 原因:请求进程写井时没加互斥,两个进程同时移动了well_tail,导致数据覆盖或指针错位。 解决:确认sem_wait(&mutex)包住了写井和更新请求表的全部操作,一个都不能漏。

4.2 程序卡死不动,谁也不输出

现象:跑起来就挂住,没有任何打印。 原因:信号量初值设错,最常见的是full初值给了 1 或者empty初值给了 0。 解决:empty必须等于井容量,full必须从 0 开始。检查sem_init的第三个参数。

4.3 输出进程只打了一条就退出

现象:只输出第一条数据,循环就结束了。 原因:循环终止条件写成了while(1)里判断full的值,或者output_count没累加。 解决:终止条件应该基于总请求数,而不是井的当前状态。把TOTAL_REQ设成两个请求进程提交次数之和。

4.4 井满了请求进程还在写,数据丢失

现象:输出条数少于提交条数。 原因:empty信号量没在输出进程里正确V操作,空槽没被释放。 解决:输出进程每消费一条,必须sem_post(&empty),位置放在取完数据之后、打印之前都行,但不能忘。

4.5 加了锁反而更慢甚至死锁

现象:程序偶尔卡在某个sem_wait上。 原因:P 操作顺序不一致,比如请求进程先 P mutex 再 P empty,输出进程先 P full 再 P mutex,形成环路等待。 解决:所有进程统一先 P 资源信号量(empty/full),再 P 互斥信号量(mutex),顺序不能乱。

5. 进阶玩法:把井换成真实文件、加优先级、看调度效果

把基础版跑通之后,我一般会做三件事来加深理解。

第一,把输出井从内存数组换成磁盘文件。用fopen打开一个临时文件当井,请求进程fwrite,输出进程fread。这样能直观看到"假脱机"里"脱机"的含义——数据先落到磁盘,设备操作和请求操作在时间上彻底解耦。文件指针的移动要配合fseek,注意每次写完要fflush,否则输出进程可能读到空。

第二,给请求表加优先级字段。让 pid 为 1 的进程优先级高于 pid 为 2,输出进程每次从井里取数据时,先扫描请求表里状态为"待输出"且优先级最高的记录。这能模拟真实 spooling 系统里"按优先级调度"的行为。改法是在输出进程取数据前加一段扫描:

int pick_next() { int best = -1, best_prio = 999; for (int i = 0; i < MAX_REQ; i++) { if (req_table[i].state == 1 && req_table[i].prio < best_prio) { best_prio = req_table[i].prio; best = i; } } return best; // 返回优先级最高的请求下标 }

逻辑说明:state == 1过滤出还没输出的记录,prio越小优先级越高。这个函数要在 mutex 保护下调用,返回 -1 表示当前没有待输出请求。

第三,用time命令或者自己打时间戳,对比"有 spooling"和"没 spooling"两种模式下 2 个请求进程的总耗时。没 spooling 时,两个进程串行等打印机,总时间约等于两者之和;有 spooling 时,请求进程几乎不等待,总时间接近输出进程单独打印的时间。这个对比数据放在课程设计报告里,比任何文字描述都有说服力。

从那以后我每次做并发模拟,都强制先把信号量的 P/V 顺序在纸上画一遍再写代码,画不清楚就不动手。希望帮到你。

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

相关新闻

原生应用接入AI的伦理与合规:从开源许可到隐私边界

原生应用接入AI的伦理与合规:从开源许可到隐私边界

1. 为什么原生应用开发者绕不开“AI伦理”这道门槛“AI伦理”这四个字放在前几年&#xff0c;很多做原生应用开发的同行会觉得这是大厂法务和研究院才需要操心的事。但这两年情况完全不一样了——几乎每个原生应用都在往里面塞AI能力&#xff0c;从刷脸登录、智能客服&#xff…

2026/10/21 2:52:02 阅读更多 →
PowerBuilder 11.5 环境搭建与数据窗口连库实战指南

PowerBuilder 11.5 环境搭建与数据窗口连库实战指南

简介&#xff1a;PowerBuilder 11.5 安装文件资源包&#xff0c;面向需要搭建经典 PB 开发环境的学习者与维护老项目的开发者。PowerBuilder 是 Sybase 推出的快速应用开发工具&#xff0c;11.5 版本在数据窗口、数据库连接与客户端应用开发方面较为成熟&#xff0c;适合用于课…

2026/10/21 2:52:02 阅读更多 →
JSP+Servlet学生管理系统毕设实战指南

JSP+Servlet学生管理系统毕设实战指南

简介&#xff1a;这是一份面向Java Web初学者与高校课程实践者的完整学生信息管理系统教学资源&#xff0c;基于JSP与Servlet技术栈实现MVC分层架构&#xff0c;覆盖动态网页开发、HTTP请求处理、JDBC数据库操作及Tomcat容器部署等核心知识点。资源包共576个文件&#xff0c;包…

2026/10/21 2:52:02 阅读更多 →

最新新闻

MALA幅度感知线性注意力如何高效改造YOLO26

MALA幅度感知线性注意力如何高效改造YOLO26

最近在忙目标检测模型的落地优化&#xff0c;把精力主要放在了YOLO26的注意力机制改造上。试过不少主流的注意力模块&#xff0c;要么计算量上去了推理变慢&#xff0c;要么就是涨点不明显&#xff0c;白折腾。后来读到一篇当时挂在学术预印平台上的新工作&#xff0c;核心是提…

2026/10/21 3:33:25 阅读更多 →
Kettle ETL实战:为什么老工具在混乱数据场景中依然不可替代

Kettle ETL实战:为什么老工具在混乱数据场景中依然不可替代

1. 为什么今天还在用Kettle做ETL&#xff1f;一个被低估的“老派”工具的真实价值很多人看到“Kettle”这个词&#xff0c;第一反应是&#xff1a;这玩意儿不是十年前就该淘汰了吗&#xff1f;现在不都上Airflow、Dagster、dbt&#xff0c;或者直接云厂商的DataFlow、Glue、Dat…

2026/10/21 3:33:25 阅读更多 →
用pgzrun零基础实现克隆人游戏:Python游戏开发入门实践

用pgzrun零基础实现克隆人游戏:Python游戏开发入门实践

1. 项目概述&#xff1a;为什么一个“克隆人”游戏值得用pgzrun重做&#xff1f;最近在某高校的编程入门工作坊里&#xff0c;我带学生做了一个小项目——用Python复刻经典街机游戏《阿肯色克隆人》&#xff08;Arkansas Clone&#xff09;&#xff0c;但没碰Pygame原生API&…

2026/10/21 3:33:25 阅读更多 →
MapLibre Native 地图样式切换指南:3 种方式让地图不重启应用就换外观

MapLibre Native 地图样式切换指南:3 种方式让地图不重启应用就换外观

MapLibre Native 地图样式切换指南&#xff1a;3 种方式让地图不重启应用就换外观 【免费下载链接】maplibre-native MapLibre Native - Interactive vector tile maps for iOS, Android and other platforms. 项目地址: https://gitcode.com/GitHub_Trending/ma/maplibre-na…

2026/10/21 3:33:25 阅读更多 →
REA 逆向工程快速上手:把 REA 装进 AI 编码助手,跑通第一次二进制分析

REA 逆向工程快速上手:把 REA 装进 AI 编码助手,跑通第一次二进制分析

REA 逆向工程快速上手&#xff1a;把 REA 装进 AI 编码助手&#xff0c;跑通第一次二进制分析 【免费下载链接】rea Reverse engineer anything with agents, from app behavior down to native binaries. 项目地址: https://gitcode.com/GitHub_Trending/rea2/rea REA&…

2026/10/21 3:33:25 阅读更多 →
51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

51单片机贪吃蛇实战:12864驱动、EEPROM掉电保存与嵌入式分层设计

简介&#xff1a;本资源是一套面向大学本科课程设计的51单片机贪吃蛇游戏完整开发方案&#xff0c;适用于单片机原理与应用、嵌入式系统实践等课程学习与项目实训。资源包含Proteus仿真工程&#xff08;.pdsprj&#xff09;、Keil C51源码&#xff08;.c/.a51&#xff09;、编译…

2026/10/21 3:32:25 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/20 9:50:45 阅读更多 →