操作系统复习(九)

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

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐