操作系统复习(九)

操作系统复习(九)

IO软件的四个层次

I/O软件通常自下而上分为以下四个层次:

1. 中断处理程序(底层)

  • 功能:位于最底层,当I/O操作完成或出错时,负责响应和捕获中断,保存被中断进程的CPU现场,唤醒等待该I/O完成的驱动程序,并恢复现场。

2. 设备驱动程序

  • 功能:直接与硬件设备控制器交互。将上层发出的抽象I/O请求(如“读数据”)翻译成具体的设备寄存器操作命令(如向控制寄存器写指令、向数据寄存器读写数据)。

3. 设备独立性软件(设备无关I/O软件)

  • 功能:向上层提供统一的标准接口,屏蔽底层硬件差异。主要负责设备命名、缓冲管理(如设置缓冲区)、错误报告,以及对独占设备的逻辑分配(如SPOOLing技术的实现)

4. 用户层I/O软件(最高层)

  • 功能:运行在用户态,是连接应用程序与内核的接口。通常以**库函数(如C语言的printfscanffread)**形式存在,负责将用户数据格式化后,通过系统调用陷入内核,触发后续的I/O操作。

补充记忆口诀(自底向上)

中(中断) 驱(驱动) 独(独立) 用(用户)—— 硬件最近的是中断,离用户最近的是库函数。
现代操作系统(如Linux)严格遵循此分层架构,以实现I/O功能的高内聚和低耦合。

通道程序

通道程序I/O通道(一种专用于控制输入/输出的小型处理器)所执行的指令序列。它本质上是“写给通道看的程序”,用来精确控制数据的传输路径、内存位置和传输长度。

你可以把它理解为CPU 给 I/O 通道下发的“详细操作清单”。CPU 只需把这份清单的起始地址告诉通道,通道就会独立地、自动地按清单逐条执行,直到所有数据传输完毕才中断通知 CPU。

1. 通道程序的组成

在经典的 IBM 大型机或现代操作系统原理教材中,通道指令通常被称为CCW(Channel Command Word,通道命令字)。一条典型的通道指令包含以下核心字段:

  • 命令码(Command):指明要做什么(如“读”、“写”、“控制”、“转移”)。
  • 数据主存地址(Data Address):指明本次传输的数据在内存中的起始位置。
  • 传输字节数(Count):指明本次传输的数据长度。
  • 标志位(Flags):指明指令执行完毕后的动作(如“结束中断”、“链式执行”等)。

多条这样的 CCW 排列在一起,就组成了通道程序

2. 通道程序的执行流程

  1. 编写与提交:操作系统在内存中构建好通道程序(即一连串 CCW)。
  2. 启动通道:CPU 执行“启动 I/O”指令,将通道程序的首地址告诉通道控制器。
  3. 并行运作(关键):通道从内存取出第一条 CCW,开始控制硬件传输数据。此时,CPU 与 I/O 通道并行工作——CPU 去执行其他计算任务,通道负责搬运数据。
  4. 结束处理:当通道执行完最后一条指令(或遇到错误),会通过中断信号通知 CPU:“数据传输完毕,请来验收”。

3. 通道程序 vs CPU 程序

这是理解它的关键,避免混淆:

  • 运行主体不同:CPU 程序由 CPU 执行,通道程序由I/O 通道(专门的外设处理器)执行。
  • 指令集不同:CPU 指令负责加减乘除、逻辑判断;通道指令只负责“把某段内存的数据搬到某设备”或“从设备搬回内存”,它没有运算、跳转等复杂逻辑(通常仅支持顺序执行或简单的链式转移)。
  • 存在位置:通道程序存储在内存中,但由通道直接去内存取指执行(类似 DMA,但拥有更强的指令解析能力)。

4. 为什么要引入它?

这是为了解决“程序控制 I/O”“中断驱动 I/O”的缺陷:

  • 如果每次传输一个字节都要 CPU 干预,CPU 会被拖死。
  • 引入通道程序后,CPU 只需发出一次“启动”命令,剩下的成千上万个字节的搬运工作全部由通道程序自动管理。这实现了I/O 操作与 CPU 计算的高度并行,是早期大型机实现高吞吐量的核心技术。

现代 PC 中还有吗?

在 PC 体系中,这种“通道程序”的概念被高度简化为DMA(直接内存访问)控制器描述符链(Descriptor Chain)。现代 NVMe 固态硬盘或网卡使用的**环形缓冲区(Ring Buffer)PRP(物理区域页)**列表,本质上就是“通道程序”的变种——依然是 CPU 告诉硬件“去这里读、传到哪里、长度多少”的指令清单。

进程间通信机制

进程间的通信机制(IPC,Inter-Process Communication)是操作系统为隔离的进程之间提供的数据交换通道。由于进程拥有独立的地址空间,它们无法直接访问对方的内存,必须借助操作系统提供的特定机制。

根据通信效率数据量,IPC 机制可以分为以下几大类:

1. 管道通信(Pipe)

管道本质是内核维护的环形缓冲区,以无格式字节流方式传输数据。

  • 匿名管道(Pipe):仅在父子进程或兄弟进程(有亲缘关系)间使用。通过pipe()系统调用创建,一端写、一端读,数据被消费后即从缓冲区消失。
  • 命名管道(FIFO):通过磁盘上的特殊文件(mkfifo)存在,允许任意两个进程(无亲缘关系)通过打开该文件进行通信。缺点是单向传输,如需双向需建立两个管道。

2. 信号(Signal)

信号是软件中断,用于通知进程发生了某个异步事件(如按 Ctrl+C、非法内存访问)。

  • 特点:信息量极小(仅传递一个预定义的整数编号),且不携带用户数据。它主要用于进程间控制(如终止进程SIGKILL、暂停SIGSTOP),而非大规模数据传输。

3. 消息队列(Message Queue)

消息缓冲机制的正式名称。进程通过系统调用将数据打包成有格式的消息块(带有类型标识),发送到内核维护的消息队列中。

  • 优点有边界(读端可以根据消息类型直接提取特定消息,无需按顺序读取);克服了管道的字节流模糊性。
  • 缺点:数据需要在用户态和内核态之间拷贝两次,传输长度受限,效率中等。

4. 共享内存(Shared Memory)

操作系统将同一块物理内存区域映射到不同进程的虚拟地址空间中,使它们可以直接读写这片区域。

  • 优点最快的通信方式。数据只需在写入时拷贝一次到共享区,其他进程立即可见,无需内核介入拷贝。
  • 致命缺点缺乏同步机制。如果两个进程同时修改同一块数据,会发生数据错乱。因此,必须配合信号量(Semaphore)来保证互斥访问。

5. 信号量(Semaphore)

严格来说,信号量不负责传输数据,它属于同步工具(PV操作)。它用于控制多个进程对共享资源(如共享内存)的访问权限。

  • 它是一个整数计数器,P操作(申请)减1,V操作(释放)加1。值大于0表示可用资源数,值等于0表示资源被占满,进程需阻塞等待。

6. 套接字(Socket)

套接字允许通信双方跨越物理网络边界。

  • 类型:流式套接字(TCP,面向连接,可靠)和数据报套接字(UDP,无连接,不可靠)。
  • 本地使用:在 Linux 中,Unix Domain Socket(UDS)常用于本机高性能进程通信,它不走网络协议栈,效率介于管道和共享内存之间,且支持双向、带边界的数据传输。

效率与选择建议(核心总结)

通信机制数据拷贝次数数据边界使用场景
管道(Pipe)2次(内存→内核→内存)字节流(无边界)Shell命令拼接、简单父子进程传输
消息队列2次(内存→内核→内存)有边界(按类型取)需要格式化的短消息、任务分发
共享内存1次(写入共享区即可)无(裸内存块)大数据量、高频读写(视频帧、数据库缓存)
Socket(本地UDS)2次(有协议栈开销)有边界(数据报)或流(TCP)跨语言服务调用(如 Nginx 与 PHP-FPM)

现代高性能系统(如 Redis、Kafka)的典型搭配是:共享内存(存数据) + 信号量(锁保护)达到极限吞吐,而跨机器的分布式通信则依赖Socket + 序列化协议(如 Protobuf)

索引节点(inode)多级指针表示文件

每个inode表示一个文件:

【文件偏移量 → 逻辑块号 L(每块 8KB)】 | 【判断 L 落在哪个区间】 | ┌─────────────┼──────────────────────────────┐ ↓ ↓ ↓ 【直接区】 【间接区】 【二次/三次间接区】 (L 0 ~ 14) (L 15 ~ 2062) (L >= 2063 ...) ================================================================================ ★ 寻址总结构图(INODE 与索引块的层级关系) ================================================================================ ┌────────────────────────────────────────────┐ │ INODE (128 字节) │ │ ┌──────────────────────────────────────┐ │ │ │ 状态信息 (68 字节) │ │ │ └──────────────────────────────────────┘ │ │ ┌──────────────────────────────────────┐ │ │ │ 直接指针 0 ──────→ (指向数据块) │ │ │ │ 直接指针 1 ──────→ (指向数据块) │ │ │ │ ... │ │ │ │ 直接指针 14 ──────→ (指向数据块) │ │ ← 共 15 个 │ ├──────────────────────────────────────┤ │ │ │ 指针[15] 一次间接 ─→ [一级索引块] │ │ │ │ 指针[16] 二次间接 ─→ [二级索引块] │ │ │ │ 指针[17] 三次间接 ─→ [三级索引块] │ │ │ └──────────────────────────────────────┘ │ └────────────────────────────────────────────┘ │ │ 每个索引块大小为 8KB │ 每个指针 4 字节 → 每块可存 2048 个指针 ↓ ================================================================================ ① 直接指针区 (0 ~ 14) ================================================================================ INODE ┌──────┐ │ 直接0 │────→ ┌──────────┐ │ 直接1 │────→ │ 数据块 │ │ ... │ │ (物理号) │ │ 直接14│────→ └──────────┘ └──────┘ 特点:1 次 I/O 就能读到数据,适合小文件。 ================================================================================ ② 一次间接指针 (第 15 号槽) ================================================================================ INODE 一级索引块 (8KB) 数据块 ┌──────┐ ┌─────────────────────────────┐ │指针[15]│──────────→│ 指针[0] ──────────────────→│ 数据块 0 │ └──────┘ │ 指针[1] ──────────────────→│ 数据块 1 │ │ 指针[2] ──────────────────→│ 数据块 2 │ │ ... │ ... │ │ 指针[2047]─────────────────→│ 数据块2047│ └─────────────────────────────┘ 覆盖逻辑块号:15 ~ 2062 (共 2048 块) → 大小 16 MB 需要 2 次 I/O:读索引块 + 读数据块 ================================================================================ ③ 二次间接指针 (第 16 号槽) ================================================================================ INODE 二级索引块 (8KB) 一级索引块 (8KB) 数据块 ┌──────┐ ┌────────────────────┐ ┌─────────────────┐ │指针[16]│─────→│ 指针[0] ──────────→│ │ 指针[0] ────────→│ 数据块 │ └──────┘ │ 指针[1] ──────────→│ │ 指针[1] ────────→│ 数据块 │ │ ... │ │ ... │ ... │ │ 指针[2047]────────→│ │ 指针[2047]──────→│ 数据块 │ └────────────────────┘ └─────────────────┘ 覆盖逻辑块号:2063 ~ 2063+2048²-1 (共 2048² = 4,194,304 块) → 大小 32 GB 需要 3 次 I/O:读二级块 + 读一级块 + 读数据块 ================================================================================ ④ 三次间接指针 (第 17 号槽) ================================================================================ INODE 三级索引块 二级索引块 一级索引块 数据块 ┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │指针[17]│─────→│指针[0] ──→│ │指针[0] ──→│ │指针[0] ──→│ 数据块 │ └──────┘ │指针[1] ──→│ │指针[1] ──→│ │指针[1] ──→│ 数据块 │ │ ... │ │ ... │ │ ... │ ... │ │指针[2047]→│ │指针[2047]→│ │指针[2047]→│ 数据块 │ └──────────┘ └──────────┘ └──────────┘ 覆盖大小:2048³ = 8,589,934,592 块 → 64 TB 需要 4 次 I/O:三级块 + 二级块 + 一级块 + 数据块

一些题目

1

14、最短作业优先调度算法其作业平均周转时间最短。 ( 对 )

2