Linux五种I/O模型
什么是I/O
I/O = Input/Output,输入/输出。本质上就是:数据的流动
- 输入:数据从外部设备 → 进入内存(比如读文件、收网络包、键盘输入)
- 输出:数据从内存 → 到外部设备(比如写文件、发网络包、屏幕显示)
常见的I/O设备
| 设备 | 输入 | 输出 |
|---|---|---|
| 磁盘 | 读文件 | 写文件 |
| 网卡 | 接收数据包 | 发送数据包 |
| 键盘 | 按键输入 | - |
| 显示器 | - | 显示内容 |
| 鼠标 | 点击/移动 | - |
I/O在计算机中的位置
┌─────────────────────────────────────┐
│ 应用程序 │
│ (用户空间) │
└─────────────┬───────────────────────┘
│ 系统调用
↓
┌─────────────────────────────────────┐
│ 操作系统内核 │
│ (内核空间) │
│ ┌─────────────────────────┐ │
│ │ I/O子系统 │ │
│ │ (文件系统、网络协议栈) │ │
│ └──────────┬──────────────┘ │
└───────────────┼─────────────────────┘
│ 设备驱动
↓
┌─────────────────────────────────────┐
│ 硬件设备 │
│ (磁盘、网卡、显卡...) │
└─────────────────────────────────────┘
为什么I/O慢?
I/O是计算机中最慢的操作,原因:
| 操作 | 速度量级 | 对比 |
|---|---|---|
| CPU指令 | 纳秒级 | 🚀🚀🚀 |
| 内存访问 | ~100ns | 🚀🚀 |
| SSD读取 | ~100μs | 🚀 |
| 机械硬盘 | ~10ms | 🐢 |
| 网络传输 | 1-100ms+ | 🐌🐌🐌 |
CPU太快,I/O太慢,差距在10⁵-10⁸倍
所以I/O模型的核心问题就是:怎么让CPU不等I/O?
I/O的两个核心动作
对于一次读操作:
1. 等待数据准备好
磁盘/网卡 → 内核缓冲区
2. 拷贝数据
内核缓冲区 → 用户缓冲区(应用程序内存)
所有I/O模型的差异,都在于如何处理这两个阶段。
内核态 vs 用户态
这是理解I/O的关键:
- 用户态:应用程序运行的权限级别,不能直接访问硬件
- 内核态:操作系统运行的权限级别,可以访问一切
一次I/O必定涉及用户态↔内核态切换
切换开销:
- 保存/恢复寄存器
- 切换页表
- 刷新缓存
频繁的系统调用是性能杀手
缓冲区的作用
内核缓冲区:操作系统维护的缓冲区
用户缓冲区:应用程序自己的缓冲区
写入流程:
用户缓冲区 → 内核缓冲区 → 磁盘/网卡
读取流程:
磁盘/网卡 → 内核缓冲区 → 用户缓冲区
为什么有缓冲区?
- 减少实际I/O次数(合并小请求)
- 解耦速度差异(生产者-消费者模式)
零拷贝技术
传统I/O需要多次拷贝:
磁盘 → 内核缓冲区 → 用户缓冲区 → 内核缓冲区 → 网卡
(DMA拷贝) (CPU拷贝) (CPU拷贝) (DMA拷贝)
零拷贝技术(sendfile、mmap)可以减少拷贝次数:
磁盘 → 内核缓冲区 → 网卡
(DMA拷贝) (DMA拷贝)
应用场景: Kafka、Nginx等高性能服务器
| 概念 | 核心理解 |
|---|---|
| I/O本质 | 数据在内存与外部设备间流动 |
| I/O之痛 | 速度慢,CPU要等 |
| I/O模型 | 解决"怎么等"的问题 |
| 核心矛盾 | CPU太快,I/O太慢 |
| 优化方向 | 减少等待、减少拷贝、减少切换 |
Linux的五种I/O模型是理解网络编程和高并发服务器的核心。
对于一次I/O操作(比如read),会经历两个阶段:
- 等待数据准备:数据从网卡/磁盘读取到内核缓冲区
- 将数据从内核拷贝到用户空间:从内核缓冲区拷贝到应用程序缓冲区
五种I/O模型的区别主要在这两个阶段如何处理。
1. 阻塞I/O(Blocking I/O)
最简单的模型,默认行为。
应用进程调用recvfrom
↓
阻塞等待...
↓
数据准备好
↓
内核拷贝数据到用户空间
↓
返回成功
特点:
- 调用后进程挂起,直到数据准备好并拷贝完成
- 简单直观,代码容易理解
- 缺点: 一个线程只能处理一个连接,效率极低
典型场景: 简单的客户端程序
2. 非阻塞I/O(Non-blocking I/O)
不断轮询,不等待。
应用进程调用recvfrom
↓
数据未准备好 → 立即返回EAGAIN错误
↓
再次调用recvfrom
↓
再次返回EAGAIN...
↓
(循环轮询)
↓
数据准备好了 → 拷贝数据 → 返回成功
特点:
- 设置socket为
NONBLOCK,调用立即返回 - 数据未准备好返回错误(EAGAIN/EWOULDBLOCK)
- 缺点: CPU空转严重,轮询消耗大量资源
典型场景: 很少单独使用,通常配合I/O多路复用
3. I/O多路复用
用select/poll/epoll同时监控多个连接
多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select,,select会监听所有注册进来的IO。
如果select监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可读数据时,select调用就会返回;而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
Linux中IO复用的实现方式主要有Select,Poll和Epoll:
Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_ SIZE(1024)。
Poll:原理和Select相似,没有数量限制,但IO数量大,扫描线性性能下降。
Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持。
应用进程调用select/epoll_wait
↓
阻塞等待任意一个socket可读
↓
有socket可读了
↓
返回就绪的socket列表
↓
对就绪socket调用recvfrom
↓
内核拷贝数据到用户空间
↓
返回成功
特点:
- 一个线程可以监控多个文件描述符
- select/poll/epoll是系统调用,本身也会阻塞
- 优点: 单线程处理大量连接,性能好
- 缺点: 需要两次系统调用(select + recvfrom)
三种实现对比:
select:有fd数量限制(1024),O(n)遍历poll:无数量限制,仍需O(n)遍历epoll:事件驱动,只返回就绪的fd,O(1)
典型场景: Nginx、Redis、Netty等高性能服务器
4. 信号驱动I/O(Signal-driven I/O)
数据准备好时发信号通知。
应用进程建立SIGIO信号处理函数
↓
调用sigaction注册信号
↓
进程继续执行其他任务...
↓
数据准备好 → 内核发送SIGIO信号
↓
信号处理函数中调用recvfrom
↓
内核拷贝数据到用户空间
特点:
- 等待数据阶段不阻塞
- 数据准备好后通过信号通知
- 缺点: 信号处理复杂,TCP场景信号过多,实际很少使用
典型场景: UDP应用较多,TCP很少用
5. 异步I/O(Asynchronous I/O)
真正的异步,两个阶段都不阻塞。
应用进程调用aio_read
↓
立即返回,进程继续执行
↓
(同时)内核等待数据 + 拷贝数据
↓
全部完成后,通知应用进程
特点:
- 两个阶段都不阻塞
- 应用进程只需发起请求,完成后会被通知
- 优点: 真正的异步,性能最优
- 缺点: Linux的aio支持长期不完善,编程模型复杂
典型场景: Windows的IOCP用得比较多,Linux上用较少
五种模型对比
| 模型 | 第一阶段(等待数据) | 第二阶段(拷贝数据) | 特点 |
|---|---|---|---|
| 阻塞I/O | 阻塞 | 阻塞 | 简单但低效 |
| 非阻塞I/O | 非阻塞(轮询) | 阻塞 | CPU空转 |
| I/O多路复用 | 阻塞(在select上) | 阻塞 | 高并发首选 |
| 信号驱动I/O | 非阻塞(信号通知) | 阻塞 | TCP下较少用 |
| 异步I/O | 非阻塞 | 非阻塞 | 真正异步 |
同步 vs 异步
- 同步I/O:导致请求进程阻塞,直到I/O操作完成(前四种都是同步)
- 异步I/O:不导致请求进程阻塞(只有AIO是真正的异步)
实际开发中,I/O多路复用(epoll) 是Linux高性能服务器的标准选择,Redis、Nginx都用这个。阻塞I/O适合简单场景,异步I/O理论上最优但Linux支持不够成熟。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)