编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 计算机原理

    • README
    • 计算机组成结构原理
    • 计算器存储器的原理
    • 计算机基础CPU设计
    • 系统CPU缓存的设计
    • 计算机输入输出设备
      • 01.工作案例引入
        • 1.1 CPU闲传不动文件
        • 1.2 追问清单
      • 02.I/O设备概述
        • 2.1 什么是I/O设备
        • 2.2 I/O设备分类
        • 2.3 为什么I/O很重要
      • 03.I/O设备接口
        • 3.1 什么是设备接口
        • 3.2 接口的核心组成
        • 3.3 I/O端口编址方式
        • 3.4 主流接口标准演进
      • 04.I/O控制方式
        • 4.1 程序查询方式
        • 4.2 中断驱动方式
        • 4.3 DMA方式
        • 4.4 通道方式
        • 4.5 四种方式对比
      • 05.中断系统设计
        • 5.1 为什么需要中断
        • 5.2 中断的基本概念
        • 5.3 中断处理流程
        • 5.4 中断优先级
        • 5.5 软件中断硬件中断
      • 06.DMA深入原理
        • 6.1 DMA的设计背景
        • 6.2 DMA工作原理
        • 6.3 DMA传输流程
        • 6.4 DMA与中断的配合
      • 07.常见I/O设备原理
        • 7.1 键盘的工作原理
        • 7.2 显示器的工作原理
        • 7.3 磁盘的工作原理
        • 7.4 网卡的工作原理
      • 08.I/O设备与编程
        • 8.1 设备驱动程序
        • 8.2 IO对软件性能影响
        • 8.3 零拷贝技术
      • 09.综合案例Nginx文件
        • 9.1 场景背景
        • 9.2 readwrite慢版本
        • 9.3 sendfile快版本
        • 9.4 串起本章所有知识
      • 10.思考题与作业
        • 10.1 基础思考题
        • 10.2 进阶思考题
        • 10.3 动手作业
    • 计算机总线系统设计
    • 计算机指令编程原理
    • 计算机程序如何执行
    • 计算机内存设计原理
    • 计算机二进制和字节
    • 计算机异常处理机制
    • 计算机IO操作和原理
  • 网络协议

  • 操作系统

  • 数据库原理

  • 计算机
  • 计算机原理
杨充
2020-06-14
目录

计算机输入输出设备

# 05.计算机输入输出设备

# 目录介绍

  • 01.工作案例引入
    • 1.1 CPU闲传不动文件
    • 1.2 追问清单
  • 02.I/O设备概述
    • 2.1 什么是I/O设备
    • 2.2 I/O设备分类
    • 2.3 为什么I/O很重要
  • 03.I/O设备接口
    • 3.1 什么是设备接口
    • 3.2 接口的核心组成
    • 3.3 I/O端口编址方式
    • 3.4 主流接口标准演进
  • 04.I/O控制方式
    • 4.1 程序查询方式
    • 4.2 中断驱动方式
    • 4.3 DMA方式
    • 4.4 通道方式
    • 4.5 四种方式对比
  • 05.中断系统设计
    • 5.1 为什么需要中断
    • 5.2 中断的基本概念
    • 5.3 中断处理流程
    • 5.4 中断优先级
    • 5.5 软件中断硬件中断
  • 06.DMA深入原理
    • 6.1 DMA的设计背景
    • 6.2 DMA工作原理
    • 6.3 DMA传输流程
    • 6.4 DMA与中断的配合
  • 07.常见I/O设备原理
    • 7.1 键盘的工作原理
    • 7.2 显示器的工作原理
    • 7.3 磁盘的工作原理
    • 7.4 网卡的工作原理
  • 08.I/O设备与编程
    • 8.1 设备驱动程序
    • 8.2 IO对软件性能影响
    • 8.3 零拷贝技术
  • 09.综合案例Nginx文件
    • 9.1 场景背景
    • 9.2 readwrite慢版本
    • 9.3 sendfile快版本
    • 9.4 串起本章所有知识
  • 10.思考题与作业
    • 10.1 基础思考题
    • 10.2 进阶思考题
    • 10.3 动手作业

# 01.工作案例引入

# 1.1 CPU闲传不动文件

场景:小赵维护一个内部文件下载服务,用的是 Java FileInputStream + OutputStream 手写的传输逻辑。某天业务反馈:下载 500MB 的大文件平均要 40 秒,机器明明是千兆网(理论上 5 秒就够)。

小赵上线观察:

%CPU   : 12%    ← 很闲
%MEM   : 30%    ← 够用
网络出口: 120Mb/s  ← 居然没跑满!千兆网应该能 900Mb+
磁盘读  : 180MB/s  ← SSD 没问题
1
2
3
4

最诡异的是:所有组件都不是瓶颈,但速度就是上不去。代码看着也没毛病:

byte[] buf = new byte[4096];
while ((n = fis.read(buf)) > 0) {
    sos.write(buf, 0, n);       // 标准的"读一段写一段"
}
1
2
3
4

资深同事看了一眼说:换成 FileChannel.transferTo() 试试。小赵改了一行,下载时间从 40 秒降到 5 秒。CPU 占用率从 12% 降到 3%。

# 1.2 追问清单

小赵这次踩的坑,涉及本章所有知识:

  • "为什么 CPU 不忙但传输慢?" → 因为瓶颈在 I/O 的上下文切换和数据拷贝,不在计算
  • "read + write 到底慢在哪里?" → 数据要在"磁盘→内核→用户→内核→网卡"跳 4 次拷贝 + 4 次上下文切换
  • "transferTo 为什么快?" → 底层是 sendfile 系统调用,走的是"磁盘→内核→网卡"的 2 次 DMA 拷贝,CPU 零参与
  • "为什么 CPU 越少参与反而越快?" → 因为 DMA 是硬件级别的并行,CPU 在此期间可以干别的
  • "这跟中断、DMA、零拷贝这些原理有什么关系?" → 这是三者配合的经典演出

本章就是带你把这条"磁盘到网卡"的路径一层一层拆开:I/O 接口的作用、CPU 和设备怎么沟通、中断和 DMA 的分工、零拷贝凭什么快。读完后你会理解为什么 Kafka 号称"磁盘比内存还快",Nginx 为什么能轻松顶几万 QPS——都是这条路径上的不同优化姿势。

# 02.I/O设备概述

# 2.1 什么是I/O设备

I/O 是 Input/Output 的缩写,即输入/输出。I/O设备是计算机与外界交互的桥梁——没有I/O设备,计算机就是一个"孤岛",算再多也没有意义。

疑惑:键盘鼠标显示器我理解,但硬盘、网卡也算I/O设备吗?

答疑:是的。在计算机体系结构中,除了 CPU 和内存之外的所有设备都统称为 I/O 设备(外部设备/外设)。

CPU 和 内存 = 计算机的"大脑"
I/O设备      = 计算机的"五官和四肢"

  输入设备:获取外界信息(眼睛、耳朵)
  输出设备:向外界表达结果(嘴巴、手)
  存储设备:长期记忆(笔记本)
  网络设备:远程通信(电话)
1
2
3
4
5
6
7

# 2.2 I/O设备分类

分类方式 类型 示例
按方向 输入设备 键盘、鼠标、摄像头、麦克风、扫描仪
输出设备 显示器、打印机、扬声器
输入输出设备 触摸屏、网卡、硬盘
按速度 低速设备 键盘(~10字节/秒)、鼠标
中速设备 打印机(~数KB/秒)
高速设备 硬盘(~GB/秒)、网卡、显卡
按信息组织 块设备 硬盘、SSD(以数据块为单位传输)
字符设备 键盘、鼠标(以字符/字节为单位传输)

# 2.3 为什么I/O很重要

疑惑:I/O不就是接个设备读写数据吗,为什么要专门学?

答疑:I/O是计算机系统的性能瓶颈所在。现代计算机的CPU速度已经非常快,大多数程序的性能瓶颈不在于"算不过来",而在于"数据搬运不过来"。

一个Web服务器的典型时间分配:
  CPU计算:   5%  的时间
  等待I/O:  95%  的时间(等待网络数据、等待磁盘读写)

这就是为什么:
  - Node.js 用异步I/O 就能处理高并发
  - Redis 用内存代替磁盘就能快100倍
  - 选SSD换HDD,编译速度能翻倍
1
2
3
4
5
6
7
8

# 03.I/O设备接口

# 3.1 什么是设备接口

I/O设备不能直接连到CPU上,它们之间需要一个"翻译官"——这就是I/O接口(I/O Interface),也叫I/O控制器(I/O Controller)或适配器(Adapter)。

疑惑:为什么I/O设备不能直接和CPU通信?

答疑:因为两者之间存在巨大的差异:

差异点 CPU侧 I/O设备侧
速度 纳秒级 毫秒级(差百万倍)
数据格式 并行的数字信号 各种模拟/数字信号
数据宽度 32/64位 各不相同
时序 与CPU时钟同步 各自有自己的时序

I/O接口的作用就是解决这四个差异:速度匹配、格式转换、宽度适配、时序协调。

# 3.2 接口的核心组成

              CPU总线
                │
        ┌───────┴───────┐
        │   I/O 接口     │
        │               │
        │ ┌───────────┐ │
        │ │ 数据寄存器 │ │ ← 暂存传输的数据(速度匹配)
        │ │(Data Reg) │ │
        │ └───────────┘ │
        │ ┌───────────┐ │
        │ │ 状态寄存器 │ │ ← 记录设备状态(忙/空闲/错误)
        │ │(Status Reg)│ │
        │ └───────────┘ │
        │ ┌───────────┐ │
        │ │ 控制寄存器 │ │ ← 接收CPU的控制命令
        │ │(Ctrl Reg)  │ │
        │ └───────────┘ │
        │ ┌───────────┐ │
        │ │ 数据转换  │ │ ← 串并转换、模数转换等
        │ │ 逻辑电路  │ │
        │ └───────────┘ │
        └───────┬───────┘
                │
            I/O 设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3.3 I/O端口编址方式

CPU如何找到特定I/O设备的寄存器?有两种编址方式:

独立编址(I/O映射I/O):

内存地址空间:  0x0000_0000 ~ 0xFFFF_FFFF  (用MOV指令访问)
I/O地址空间:   0x0000 ~ 0xFFFF            (用专门的IN/OUT指令访问)

x86架构采用此方式
  IN  AL, 0x60    ; 从端口0x60(键盘)读数据
  OUT 0x61, AL    ; 向端口0x61写数据
1
2
3
4
5
6

统一编址(内存映射I/O):

整个地址空间:  0x0000_0000 ~ 0xFFFF_FFFF
  其中:
  0x0000_0000 ~ 0xBFFF_FFFF  → 真正的内存
  0xC000_0000 ~ 0xFFFF_FFFF  → 映射到I/O设备寄存器

ARM架构采用此方式
  LDR R0, [0xC000_1000]  ; 读I/O设备寄存器(和读内存一样的指令)
  STR R1, [0xC000_1000]  ; 写I/O设备寄存器
1
2
3
4
5
6
7
8

# 3.4 主流接口标准演进

接口标准演变:
  串口(RS-232) → 并口(LPT) → USB 1.0 → USB 2.0 → USB 3.0 → USB4/Thunderbolt
  IDE → SATA → NVMe(PCIe)
  PCI → PCIe

USB 速度演进:
  USB 1.0:   1.5 Mbps  (1996)
  USB 2.0: 480 Mbps  (2000)
  USB 3.0:   5 Gbps   (2008)
  USB 3.2:  20 Gbps   (2017)
  USB4:     40 Gbps   (2019)
1
2
3
4
5
6
7
8
9
10
11

# 04.I/O控制方式

# 4.1 程序查询方式

最原始的I/O方式:CPU主动不断查询I/O设备的状态,直到设备准备好。

CPU 的行为:
  while (设备状态寄存器 != 就绪) {
      // 什么也不做,一直循环等待
      // CPU 被完全占用!
  }
  读取数据寄存器;
1
2
3
4
5
6
时间线:
  CPU: [查询][查询][查询][查询]...[就绪!][读数据]
  设备:       [准备中.....................][就绪]

  CPU在等待期间完全浪费了!
1
2
3
4
5

优点:实现最简单 缺点:CPU利用率极低,CPU必须一直等待,无法做其他事情

# 4.2 中断驱动方式

改进思路:让设备准备好之后主动通知CPU,CPU在等待期间可以做其他事情。

CPU 的行为:
  启动I/O设备;
  去做其他工作();     // CPU可以执行其他任务
  ...
  // 设备准备好后发出中断信号
  [收到中断] → 暂停当前工作 → 处理I/O数据 → 恢复之前的工作
1
2
3
4
5
6
时间线:
  CPU: [启动设备][做其他事A][做其他事B][中断!处理数据][继续做事C]
  设备:          [准备中.................][就绪→发中断]

  CPU在等待期间可以做其他事情!大幅提高利用率
1
2
3
4
5

优点:CPU利用率大幅提高 缺点:每传输一个字(或字节),CPU都要中断一次,频繁中断开销大

# 4.3 DMA方式

疑惑:中断方式已经很好了,为什么还需要DMA?

答疑:当传输大量数据时(比如从硬盘读取一个文件),每个字节都中断一次CPU,中断频率太高了。DMA(Direct Memory Access,直接内存访问)的思路是:让数据传输完全绕过CPU。

CPU 的行为:
  告诉DMA控制器:从硬盘地址X读取N个字节到内存地址Y;
  去做其他工作();
  ...
  // DMA传输完成后才发一次中断
  [收到中断] → 数据已经在内存中了
1
2
3
4
5
6
时间线:
  CPU:  [配置DMA][做其他事A][做其他事B][做其他事C][DMA完成中断]
  DMA:           [搬运数据 从硬盘→内存.....................][完成]

  整个数据传输过程中,CPU完全自由!
  只在开始(配置)和结束(中断)时参与
1
2
3
4
5
6

# 4.4 通道方式

通道是比DMA更高级的I/O处理器,它有自己的指令系统,可以执行通道程序来控制I/O操作。

CPU → 启动通道 → 通道独立执行I/O通道程序 → 完成后通知CPU

通道相当于一个"简化版CPU",专门处理I/O操作
大型机和服务器中常见
1
2
3
4

# 4.5 四种方式对比

控制方式 CPU参与度 数据传输单位 适用场景 类比
程序查询 100% 字/字节 简单低速设备 你亲自到门口等快递
中断驱动 每次传输参与 字/字节 中速设备 快递到了按门铃通知你
DMA 开始和结束参与 数据块 高速块设备 快递员直接放柜子里,放好通知你
通道 只在开始参与 一组数据块 大型系统 雇了个助理帮你处理所有快递

技术演变过程:

程序查询 →(CPU太浪费了)→ 中断驱动 →(中断太频繁了)→ DMA →(还想更自动化)→ 通道
  │                          │                      │                    │
  CPU占用率: 100%             CPU占用率: ~30%         CPU占用率: ~5%       CPU占用率: ~1%
1
2
3
flowchart LR
    A[程序查询<br/>CPU轮询] -->|开销大| B[中断驱动<br/>设备唤醒CPU]
    B -->|中断太多| C[DMA<br/>设备直接搬运内存]
    C -->|批量托管| D[通道<br/>独立I/O处理器]
    style A fill:#fdd
    style B fill:#fed
    style C fill:#dfd
    style D fill:#ddf
1
2
3
4
5
6
7
8

# 05.中断系统设计

# 5.1 为什么需要中断

疑惑:没有中断的计算机会怎样?

答疑:没有中断,计算机就只能一件一件事情顺序做,无法响应任何突发事件。

没有中断的世界:
  CPU: 执行任务A...执行任务A...执行任务A...
  键盘: [用户按了一个键] → 没人理
  网卡: [收到一个数据包] → 没人理
  定时器: [该切换进程了] → 没人理

有了中断:
  CPU: 执行任务A...[键盘中断!]→处理按键→继续任务A...[时钟中断!]→切换到任务B
1
2
3
4
5
6
7
8

结论:中断机制使得计算机可以及时响应外部事件和实现多任务并发。没有中断,就没有现代操作系统。

# 5.2 中断的基本概念

中断(Interrupt)是指CPU在执行程序过程中,遇到急需处理的事件时,暂停当前程序,转去处理该事件,处理完成后再返回原程序继续执行。

核心概念:
  中断源:   引发中断的设备或事件
  中断请求: 设备向CPU发出的信号
  中断响应: CPU暂停当前任务,转去处理中断
  中断服务程序(ISR):处理中断事件的代码
  中断返回: 处理完成,恢复原来的程序
1
2
3
4
5
6

# 5.3 中断处理流程

完整的中断处理流程:

1. 中断请求
   设备 →[中断请求信号]→ 中断控制器 →[选择优先级最高的]→ CPU

2. 中断响应(CPU硬件自动完成)
   ① 执行完当前指令
   ② 保存现场:将PC、PSW等压入栈
   ③ 关中断(防止处理过程中被打断)
   ④ 根据中断向量找到ISR入口地址
   ⑤ 跳转到ISR

3. 中断服务(软件执行)
   ① 保存额外的寄存器
   ② 执行具体的中断处理逻辑
   ③ 恢复寄存器

4. 中断返回
   ① 开中断
   ② 恢复PC和PSW
   ③ 继续执行被中断的程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sequenceDiagram
    participant 用户程序
    participant CPU
    participant 中断控制器
    participant ISR as 中断服务程序
    用户程序->>CPU: 顺序执行
    中断控制器->>CPU: INTR(中断请求)
    CPU->>CPU: 保存PC/PSW、关中断
    CPU->>ISR: 跳转(查中断向量)
    ISR-->>ISR: 处理事件
    ISR->>CPU: iret 返回
    CPU->>CPU: 恢复现场、开中断
    CPU->>用户程序: 继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.4 中断优先级

当多个设备同时发出中断请求时,需要一个优先级机制来决定先处理谁。

典型的中断优先级(从高到低):
  1. 机器校验中断(硬件故障)     — 不处理机器可能损坏
  2. 时钟中断                    — 维持系统时间和进程调度
  3. 磁盘中断                    — 数据传输完成需要及时处理
  4. 网络中断                    — 网络数据需要及时接收
  5. 终端中断(键盘/鼠标)        — 用户输入
  6. 软件中断(系统调用)          — 应用程序请求
1
2
3
4
5
6
7

# 5.5 软件中断硬件中断

类型 触发方式 来源 示例
硬件中断(外中断) 由外部设备信号触发 I/O设备 键盘按键、网卡收到数据、时钟滴答
软件中断(内中断/异常) 由指令执行触发 CPU内部 除零错误、缺页异常、系统调用(INT/SVC)
// 系统调用本质上就是软件中断
// Linux x86 系统调用示例
// 用户程序想要写文件:
//   syscall号 = 1 (write)
//   参数: 文件描述符, 缓冲区地址, 长度

mov eax, 1          ; 系统调用号
mov edi, 1          ; 文件描述符(stdout)
mov rsi, buffer     ; 缓冲区地址
mov rdx, length     ; 长度
syscall             ; 触发软件中断,切换到内核态
1
2
3
4
5
6
7
8
9
10
11

# 06.DMA深入原理

# 6.1 DMA的设计背景

疑惑:为什么要设计DMA这么复杂的机制?中断不够用吗?

答疑:考虑从硬盘读取一个4KB的文件。

中断方式(每次传输1字节):
  需要中断次数:4096次
  每次中断开销:保存现场 + ISR + 恢复现场 ≈ 500个时钟周期
  总中断开销:4096 × 500 = 2,048,000 个时钟周期
  CPU 几乎被中断"淹没"了!

DMA方式:
  CPU配置DMA:~100个时钟周期
  DMA传输4KB:CPU完全不参与
  传输完成中断:~500个时钟周期
  总开销:~600个时钟周期
  节省了 3000 多倍的 CPU 开销!
1
2
3
4
5
6
7
8
9
10
11
12

# 6.2 DMA工作原理

DMA控制器是一个专门的硬件模块,它可以在不需要CPU参与的情况下,直接在I/O设备和内存之间传输数据。

DMA控制器内部结构:
  ┌──────────────────────┐
  │  DMA 控制器           │
  │  ┌──────────────┐    │
  │  │ 内存地址寄存器 │    │ ← 数据要写入/读取的内存起始地址
  │  │    (MAR)      │    │
  │  └──────────────┘    │
  │  ┌──────────────┐    │
  │  │ 数据计数器    │    │ ← 还剩多少数据要传输
  │  │   (Count)     │    │
  │  └──────────────┘    │
  │  ┌──────────────┐    │
  │  │ 控制/状态    │    │ ← 传输方向、设备号、状态
  │  │  寄存器      │    │
  │  └──────────────┘    │
  └──────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6.3 DMA传输流程

步骤1:CPU 初始化 DMA
  CPU → DMA: "从硬盘读4KB数据到内存地址0x1000"
  设置MAR = 0x1000, Count = 4096, 方向 = 设备→内存

步骤2:DMA 接管总线,开始传输
  DMA 通过总线仲裁获得总线控制权
  设备 → DMA → 内存 (每次传一个字)
  MAR++, Count--
  DMA 释放总线
  (CPU可以在DMA不占用总线时使用总线)

步骤3:传输完成
  Count == 0
  DMA → CPU: 发出中断信号
  CPU: "好的,数据已经在0x1000了,我可以使用了"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sequenceDiagram
    participant CPU
    participant DMA as DMA控制器
    participant DEV as I/O设备
    participant MEM as 内存
    CPU->>DMA: 配置(源/目的/长度)
    CPU-->>CPU: 继续执行其他任务
    DMA->>DEV: 启动传输
    loop 直到Count=0
      DEV->>DMA: 1字(数据)
      DMA->>MEM: 写入(MAR++)
    end
    DMA->>CPU: 完成中断
    CPU->>MEM: 读取结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.4 DMA与中断的配合

实际场景:读取磁盘文件

  应用程序: read(fd, buf, 4096)
      ↓ 系统调用(软件中断)
  操作系统: 配置DMA控制器
      ↓
  DMA控制器: 从磁盘读数据到内核缓冲区
  CPU: 可以调度其他进程执行
      ↓ DMA完成(硬件中断)
  操作系统: 将数据从内核缓冲区拷贝到用户缓冲区
      ↓
  应用程序: read()返回,数据在buf中
1
2
3
4
5
6
7
8
9
10
11
12

# 07.常见I/O设备原理

# 7.1 键盘的工作原理

按下一个键的完整过程:

1. 物理层:按下按键,接通电路
2. 扫描码:键盘控制器检测到按键,生成扫描码(每个键有唯一编码)
3. 数据传输:扫描码通过USB发送到计算机
4. 中断:键盘控制器触发硬件中断(IRQ 1)
5. ISR:操作系统的键盘中断服务程序读取扫描码
6. 转换:将扫描码转换为ASCII码/Unicode
7. 传递:将字符传递给当前活跃的应用程序

整个过程耗时 < 1ms,但涉及了硬件中断、I/O端口读写、缓冲区等多个概念
1
2
3
4
5
6
7
8
9
10
11

# 7.2 显示器的工作原理

从程序到屏幕画面:

1. 应用程序调用图形API(如OpenGL、DirectX)
2. GPU接收绘制命令,执行顶点/片元着色器
3. 渲染结果写入帧缓冲区(Frame Buffer)
   帧缓冲区存储每个像素的颜色值
   分辨率 1920×1080,每像素4字节 = 约8MB
4. 显示控制器按照刷新率(如60Hz)读取帧缓冲区
5. 通过HDMI/DP信号线传输到显示器
6. 显示器逐行扫描,点亮每个像素

刷新率 60Hz = 每秒刷新60次 = 每16.67ms一帧
1
2
3
4
5
6
7
8
9
10
11
12

# 7.3 磁盘的工作原理

机械硬盘读取数据的过程:

         盘片                          磁头
    ┌─────────────┐              ├──── 磁头臂
    │ ○ 磁道(Track)│              │
    │  ○○○○○○○○   │   ←──移动──→ ▼
    │   ○○○○○○    │
    │    扇区(Sector)
    └─────────────┘
    盘片旋转(7200RPM)

读取过程:
  1. 寻道:磁头臂移动到目标磁道(~5-10ms)
  2. 旋转延迟:等待目标扇区转到磁头下方(~4ms @7200RPM)
  3. 传输:数据从盘片传输到控制器缓存(~0.01ms/扇区)

平均访问时间 ≈ 寻道时间 + 旋转延迟 + 传输时间 ≈ 10ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.4 网卡的工作原理

接收一个网络数据包的过程:

1. 物理层:电/光信号到达网卡
2. 网卡接收:将信号转换为数字数据
3. 校验:验证数据完整性(CRC校验)
4. DMA传输:网卡通过DMA将数据写入内存中的接收缓冲区
5. 中断:网卡发出硬件中断
6. 驱动程序:ISR将数据包交给操作系统网络协议栈
7. 协议栈处理:TCP/IP解包,将数据交给应用程序
1
2
3
4
5
6
7
8
9

# 08.I/O设备与编程

# 8.1 设备驱动程序

疑惑:应用程序是怎么操作I/O设备的?需要直接操作硬件寄存器吗?

答疑:不需要。操作系统通过分层抽象,让应用程序可以用统一的接口操作所有设备。

应用程序
  ↓  read()/write()       统一的API
操作系统(虚拟文件系统VFS)
  ↓                        统一的接口
设备驱动程序
  ↓  操作硬件寄存器         每种设备不同
具体I/O设备
1
2
3
4
5
6
7

Unix/Linux 的设计哲学:一切皆文件。

# 读键盘输入 → 就是读文件
cat /dev/input/event0

# 写显示器 → 就是写文件
echo "Hello" > /dev/tty

# 读硬盘 → 就是读文件
cat /dev/sda1

# 从程序员的角度,操作任何设备都是 open/read/write/close
1
2
3
4
5
6
7
8
9
10

# 8.2 IO对软件性能影响

# Python 示例:演示I/O对性能的影响

import time

# 逐行写入(每次write都是一次系统调用+I/O操作)
start = time.time()
with open("test1.txt", "w") as f:
    for i in range(100000):
        f.write(str(i) + "\n")
print(f"逐行写入: {time.time()-start:.2f}s")

# 批量写入(减少I/O次数)
start = time.time()
lines = []
for i in range(100000):
    lines.append(str(i) + "\n")
with open("test2.txt", "w") as f:
    f.writelines(lines)
print(f"批量写入: {time.time()-start:.2f}s")

# 批量写入通常快 3-10 倍
# 原因:减少了系统调用次数,减少了用户态/内核态切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 8.3 零拷贝技术

疑惑:为什么网络服务器要用"零拷贝"技术?

传统的文件发送流程涉及多次数据拷贝:

传统方式(4次拷贝 + 4次上下文切换):
  磁盘 →[DMA]→ 内核缓冲区 →[CPU]→ 用户缓冲区 →[CPU]→ Socket缓冲区 →[DMA]→ 网卡

零拷贝方式 sendfile()(2次拷贝 + 2次上下文切换):
  磁盘 →[DMA]→ 内核缓冲区 →[DMA]→ 网卡
  
  数据全程不经过用户空间!
1
2
3
4
5
6
7
// C语言 零拷贝示例
#include <sys/sendfile.h>

int file_fd = open("large_file.dat", O_RDONLY);
// sendfile: 直接在内核空间完成文件到网络的传输
sendfile(socket_fd, file_fd, NULL, file_size);
// 数据不需要拷贝到用户空间
1
2
3
4
5
6
7

结论:Nginx、Kafka等高性能系统大量使用零拷贝技术,这就是它们能处理超高吞吐量的关键原因之一。

# 09.综合案例Nginx文件

用 Nginx 发一个 500MB 的 mp4 给客户端这个极普通的场景,把本章所有知识(I/O 接口、中断、DMA、零拷贝、设备驱动)串起来。

# 9.1 场景背景

请求:GET /video.mp4 HTTP/1.1,服务器要把磁盘上的 /var/www/video.mp4 读出来,通过 TCP 传给客户端。两种写法的性能差异会是数倍。

# 9.2 readwrite慢版本

// 朴素版本:每次读 4KB,写 4KB
char buf[4096];
while ((n = read(file_fd, buf, sizeof(buf))) > 0) {
    write(socket_fd, buf, n);
}
1
2
3
4
5

这段 500MB 文件大约需要 128,000 次循环。每一圈里发生了什么?

一次循环(读 4KB + 写 4KB):

1. 用户态 → 内核态(read 系统调用,软件中断)
2. 内核检查 Page Cache:
   - 未命中 → 发 DMA 指令给磁盘控制器
   - DMA 把 4KB 从磁盘搬到 Page Cache
   - 磁盘控制器发中断通知 CPU "搞定了"
3. CPU 把 4KB 从 Page Cache 拷到用户空间 buf
4. 内核态 → 用户态(read 返回)
                                       ↓ 跳过 return,小赵没做任何处理
5. 用户态 → 内核态(write 系统调用,软件中断)
6. CPU 把 4KB 从用户 buf 拷到 Socket 缓冲区
7. DMA 把 4KB 从 Socket 缓冲区搬到网卡
8. 网卡发中断通知 "发完了"
9. 内核态 → 用户态(write 返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

一次循环的代价:

次数 动作 类型
4 上下文切换(用户/内核态) 软中断
2 DMA 搬运(磁盘→Cache / SockBuf→网卡) 硬件
2 CPU 拷贝(Cache→用户 / 用户→SockBuf) CPU 工作
2 硬件中断(DMA 完成通知) 硬中断

500MB × 128,000 次,CPU 花在"搬运"上的时间 >> 花在"计算"上的时间。这就是为什么小赵看到 CPU 12% 却传不快——CPU 的 12% 不是在做业务,是在拷贝数据!

# 9.3 sendfile快版本

// 零拷贝版本:一个系统调用搞定
sendfile(socket_fd, file_fd, &offset, count);
1
2
一次 sendfile 调用(Linux 2.4+,SG-DMA 硬件支持):

1. 用户态 → 内核态(sendfile 系统调用)
2. 内核检查 Page Cache:
   - 未命中 → DMA 从磁盘搬到 Page Cache
3. 内核直接把 Page Cache 的描述符(地址+长度)交给 Socket
4. DMA 从 Page Cache 直接搬到网卡(SG-DMA)
5. 内核态 → 用户态(sendfile 返回)
1
2
3
4
5
6
7
8

一次调用的代价:

次数 动作
2 上下文切换
2 DMA 搬运
0 CPU 拷贝
1 硬件中断

CPU 完全从数据通路里解放出来。这就是为什么小赵改一行代码,CPU 占用率从 12% 降到 3%,速度反而快了 8 倍。

# 9.4 串起本章所有知识

这个案例刚好覆盖本章每一节:

章节 知识点 在这个案例里的角色
2.2 I/O 接口寄存器 状态寄存器 磁盘控制器、网卡控制器的状态查询
3.1 程序查询 原始方式 如果去掉中断和 DMA,就是小赵的噩梦
3.2 中断驱动 响应机制 DMA 完成后通知 CPU 的方式
3.3 DMA 批量传输 磁盘→Cache、Cache→网卡两次都是 DMA
5.x DMA 原理 脱离 CPU 让 CPU 去干别的业务
6.3 磁盘原理 Page Cache 第二次读同一文件时跳过磁盘
6.4 网卡原理 DMA + 中断 网卡把帧数据 DMA 出去后触发中断
7.3 零拷贝 sendfile 本案例的主角,直接跳过用户空间

一句话:看起来只是"换个 API",实际上是你"重新认识了一次 I/O 子系统"。真正懂 I/O 原理的人,写 sendfile 那一行的底气,是能画出整条数据通路的。

# 10.思考题与作业

# 10.1 基础思考题

  1. 四种 I/O 控制方式对号入座:下面的场景分别适合哪种?

    • 读键盘输入 / 打印机打印 / 硬盘读 10MB 文件 / 显卡刷新 60fps 画面 / 网卡持续收包
  2. 为什么要有 I/O 接口(控制器)? 不能让 CPU 直接连接设备吗?请从"速度差异、格式差异、时序差异、宽度差异"四个角度分别说明。

  3. 中断 vs 轮询:在什么情况下"轮询"反而比"中断"更好?提示:考虑高速网卡下的 napi 混合模型。

  4. DMA 结束时为什么还要发中断? 既然 DMA 能绕过 CPU,完成后为什么还要惊动它?

  5. 零拷贝的几种实现:mmap、sendfile、splice 各自的适用场景和局限是什么?

# 10.2 进阶思考题

  1. 0.1 节故事的完整复盘:用本章 3.x、5.x、7.3 三节知识,画出小赵"read+write 40 秒"版本的完整数据路径,并在图上标出"CPU 在哪几步参与了数据搬运"。

  2. 为什么 Kafka 号称"磁盘比内存还快"? 请从"顺序写 + sendfile + Page Cache"三个角度论证。

  3. DMA 和 Cache 的冲突:如果 DMA 直接修改了内存,CPU Cache 里的对应数据还是旧的,怎么办?现代硬件用什么机制解决?(提示:DMA 一致性、Cache Flush)

  4. I/O 多路复用和 I/O 控制方式的关系:select/poll/epoll 是不是"第五种 I/O 控制方式"?还是建立在前面四种之上?

  5. 设备驱动的分层:Linux 把驱动分成字符设备、块设备、网络设备三大类,这种分类的本质依据是什么?为什么键盘是字符设备而磁盘是块设备?

# 10.3 动手作业

作业一(必做):复现 sendfile vs read+write 的性能差。

  • 用你熟悉的语言写一个小型文件服务器,实现两个版本:
    • 版本 A:read + write 循环
    • 版本 B:sendfile / transferTo / os.sendfile
  • 传一个 500MB 文件,对比:传输时间、CPU 占用率、网卡利用率
  • 用 strace -c 或 perf 看看两个版本各自产生了多少次系统调用

作业二(选做):写一个简化的 DMA 模拟器。

  • 用多线程模拟 DMA:一个线程负责"搬运"(sleep 模拟传输时间),另一个线程是"CPU"在干业务
  • 让 CPU 线程用两种方式等待:① 忙等(轮询状态变量)② 阻塞(信号量)
  • 观察两种方式下 CPU 的利用率差异,体会"中断/DMA 为什么能释放 CPU"

作业三(拓展):给你的线上服务做一次 I/O 体检。

选一个业务接口,用 iostat、vmstat、perf top、pidstat -d 做这样的体检:

  • CPU 的 %wa(I/O wait)占多少?
  • 磁盘 IOPS 和吞吐是否到达瓶颈?
  • 是不是有哪个系统调用特别频繁?(strace -c -p PID)
  • 能不能用批量写、mmap、sendfile、零拷贝优化?

写一份小型优化报告,你会发现 I/O 优化通常是"四两拨千斤"——花一点时间能带来数量级的性能提升。

上次更新: 2026/06/07, 18:47:40
系统CPU缓存的设计
计算机总线系统设计

← 系统CPU缓存的设计 计算机总线系统设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式