IO 模型 (I/O Models)
一、IO 概述
什么是 IO
IO (Input/Output):数据在 CPU/内存 与外部设备(磁盘、网络、终端)之间的传输。
IO 类型:
| 类型 | 设备 | 速度 |
|---|---|---|
| 磁盘 IO | 硬盘/SSD | 慢 |
| 网络 IO | 网卡 | 很慢 |
| 终端 IO | 键盘/鼠标 | 中 |
| 内存 IO | 内存 | 很快 |
| GPU IO | 显卡 | 中-很快 |
IO 的核心问题
- 慢:IO 设备比 CPU 慢几个数量级
- 阻塞:IO 期间 CPU 不能干别的
- 并发:一个进程同时处理多个 IO
- 多路复用:同时监控多个 IO
IO 性能指标
- 延迟 (Latency):单次操作的响应时间 (μs 或 ms)
- 吞吐量 (Throughput):单位时间传输量 (MB/s、IOPS)
- 带宽 (Bandwidth):理论最大传输率
IO 的层次
应用层: read()/write() / send()/recv()
↓
标准库: fread/fwrite / fgets / ...
↓
系统调用层: read/write/sendmsg/recvmsg
↓
VFS / Socket 层
↓
具体驱动: Ext4 / TCP/IP / USB / ...
↓
设备: 磁盘 / 网卡 / ...
二、五大 IO 模型
1. 同步 vs 异步 (Synchronous vs Asynchronous)
关注点:消息通知机制
- 同步:调用方主动等待结果
- 异步:调用方不等结果,通过回调/事件通知
2. 阻塞 vs 非阻塞 (Blocking vs Non-Blocking)
关注点:调用时进程状态
- 阻塞:调用时进程挂起
- 非阻塞:调用时立即返回(可能带 EAGAIN/EWOULDBLOCK)
3. POSIX 的 5 大 IO 模型 (Unix Network Programming)
| 模型 | 同步/异步 | 阻塞/非阻塞 | 关键 |
|---|---|---|---|
| 阻塞 IO | 同步 | 阻塞 | 等待数据 + 等待内核 |
| 非阻塞 IO | 同步 | 非阻塞 | 轮询 |
| IO 多路复用 | 同步 | 阻塞(等 select)/可优化 | select/poll/epoll |
| 信号驱动 IO | 异步 | 异步 | SIGIO 通知 |
| 异步 IO (AIO) | 异步 | 异步 | 内核全程处理 |
注意: - "同步/异步" 在 IO 上下文中:同步 = 等数据完整就绪,异步 = 数据就绪后通知 - "阻塞/非阻塞" 在调用上下文:阻塞 = 挂起,非阻塞 = 立即返回
4. 经典图示
┌──────────┐
│ 用户态 │
└────┬─────┘
│ 1. recvfrom (发起调用)
↓
┌──────────┐
│ 内核态 │
└────┬─────┘
│ 2. 等待数据 (网络包到达)
│ 3. 复制数据到用户缓冲
↓
┌──────────┐
│ 用户态 │
└──────────┘
│ 4. 处理数据
- 阻塞 IO:1-2-3-4 全程等待
- 非阻塞 IO:2 立即返回 EAGAIN,轮询直到数据到
- IO 复用:多个 IO 同时监控(单线程)
- 信号驱动:内核通过 SIGIO 通知
- 异步 IO:1 后立即返回,内核完成所有事,完成后通知
三、阻塞 IO (Blocking I/O)
1. 原理
// 最简单的阻塞读
char buf[1024];
int n = read(fd, buf, sizeof(buf)); // 阻塞直到数据到达
process(buf);
- 进程调用 read,内核等待数据
- 数据到达后,内核复制到用户缓冲
- 复制完成后,read 返回
- 整个过程进程阻塞
2. 特点
- 优点:简单,易理解
- 缺点:等待期间 CPU 闲置
- 适合:连接少、业务简单
3. 阻塞 accept
// 经典阻塞式服务器
int listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, 128);
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client, &len);
// 阻塞,直到有客户端连接
handle_client(conn_fd);
}
致命问题:每来一个客户端,就阻塞等待,无法处理多个并发。
四、非阻塞 IO (Non-Blocking I/O)
1. 原理
把 fd 设为非阻塞 (O_NONBLOCK),read 不再阻塞,返回 EAGAIN/EWOULDBLOCK
// 设置非阻塞
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// 读
char buf[1024];
while (1) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
process(buf, n);
} else if (n == 0) {
// EOF
break;
} else if (errno == EAGAIN) {
// 数据未就绪,稍后再试
usleep(1000);
} else {
// 错误
break;
}
}
2. 特点
- 优点:不阻塞,可以做别的事
- 缺点:轮询 (busy polling) 浪费 CPU
3. 应用场景
- 与多路复用结合 (
select/poll/epoll配合非阻塞) - 单独使用通常不好(浪费 CPU)
五、IO 多路复用 (I/O Multiplexing)
1. 原理
用一个进程/线程监控多个 IO,当某个 IO 就绪时通知程序处理
核心系统调用:
- select() (1983,古老)
- poll() (1986,简单)
- epoll() (Linux 2.6,2002,高性能)
- kqueue() (BSD/macOS,等效 epoll)
2. select 详解
接口:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
特点: - fd_set 是位图:每一位代表一个 fd - 限制:fd_set 1024 位 → 最多 1024 个 fd - 每次调用都要: - 从用户态拷贝 fd_set 到内核态 - 内核轮询所有 fd - 从内核态拷贝回用户态 - O(n) 复杂度
使用:
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
struct timeval tv = {5, 0}; // 5 秒超时
int n = select(maxfd+1, &readfds, NULL, NULL, &tv);
if (n > 0) {
if (FD_ISSET(fd1, &readfds)) {
// fd1 可读
}
if (FD_ISSET(fd2, &readfds)) {
// fd2 可读
}
}
缺点: - fd 数量有限 - 每次都要遍历 - 大量 fd 性能差
3. poll 详解
接口:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 关注的事件
short revents; // 实际发生的事件
};
改进: - 无 fd 数量限制 - 每次要重新设置 pollfd 数组 - 仍是 O(n)
4. epoll 详解 (Linux 高性能)
为什么 epoll 高效: - 不限制 fd 数量 - O(1) 时间复杂度 (用红黑树 + 就绪链表) - mmap 共享内存 (避免拷贝) - 边缘触发 (ET) 支持
(1) 三个核心 API
#include <sys/epoll.h>
// 1. 创建 epoll 实例
int epoll_create(int size); // size 提示,实际不限
int epoll_create1(int flags); // 推荐
// 2. 注册/修改/删除 fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op: EPOLL_CTL_ADD / MOD / DEL
struct epoll_event {
uint32_t events; // EPOLLIN, EPOLLOUT, EPOLLET, ...
epoll_data_t data; // 用户数据
};
// 3. 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
(2) 工作模式
| 模式 | 触发条件 |
|---|---|
| 水平触发 (LT, default) | fd 有数据就触发(默认) |
| 边缘触发 (ET) | fd 状态变化时触发(必须用非阻塞 I/O) |
ET 优势: - 事件少,系统调用少 - 性能高
ET 注意事项: - 一次必须读完(read 返回 < buffer,或 EAGAIN) - 必须用非阻塞 fd
(3) 经典 ET 例子
// epoll + ET + 非阻塞 + 完整读取
void et_read(int fd) {
char buf[1024];
while (1) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
process(buf, n);
} else if (n == 0) {
// 客户端关闭
close(fd);
break;
} else {
if (errno == EAGAIN) {
// 全部读完
break;
} else {
// 错误
close(fd);
break;
}
}
}
}
(4) epoll 完整示例 (回声服务器)
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_EVENTS 64
#define PORT 8080
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int flags = fcntl(listen_fd, F_GETFL);
fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
int epfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
// 接受新连接
int client = accept(listen_fd, NULL, NULL);
int cflags = fcntl(client, F_GETFL);
fcntl(client, F_SETFL, cflags | O_NONBLOCK);
ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = client;
epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
} else if (events[i].events & EPOLLIN) {
// 读客户端
char buf[1024];
ssize_t len = read(fd, buf, sizeof(buf));
if (len <= 0) {
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
} else {
write(fd, buf, len); // 回声
}
}
}
}
}
5. 三种多路复用对比
| 维度 | select | poll | epoll |
|---|---|---|---|
| 时间复杂度 | O(n) | O(n) | O(1) |
| fd 数量 | 1024 (FD_SETSIZE) | 无限制 | 无限制 |
| 触发模式 | LT | LT | LT / ET |
| 数据拷贝 | 每次双向 | 每次双向 | mmap 共享 |
| 跨平台 | 几乎所有 Unix | 几乎所有 Unix | 仅 Linux |
| 内核实现 | 位图 | 数组轮询 | 红黑树 + 就绪链表 |
| 适用 | fd 少 / 老代码 | fd 少 | 高并发 |
6. kqueue (BSD/macOS)
- 等效 epoll 的 BSD 版本
kqueue()创建,kevent()等待- 还能监控信号、子进程、文件等
- 性能与 epoll 相当
7. IOCP (Windows)
- I/O Completion Port:Windows 专用
- 真正的异步 I/O
- 性能好,但 API 复杂
六、信号驱动 IO (Signal-Driven I/O)
1. 原理
让内核在数据就绪时发信号给进程
// 设置信号处理
signal(SIGIO, sigio_handler);
// 设置 fd 为信号驱动 + 属主进程
fcntl(fd, F_SETFL, O_ASYNC);
fcntl(fd, F_SETOWN, getpid());
// 进程继续执行
// 当 fd 有数据,SIGIO 信号到达,触发 sigio_handler
2. 优缺点
- 优点:不用轮询,数据到达时被通知
- 缺点:
- 信号处理复杂
- 全局 SIGIO 只能一个 fd
- 实际很少用,被 epoll 取代
七、异步 IO (AIO / Async I/O)
1. POSIX AIO
#include <aio.h>
struct aiocb cb;
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = count;
cb.aio_offset = offset;
aio_read(&cb); // 立即返回
// 做别的事
do_other_things();
// 等待完成
struct timespec ts = {0, 0};
while (aio_error(&cb) == EINPROGRESS) {
nanosleep(&ts, NULL); // 或做别的事
}
ssize_t n = aio_return(&cb);
2. Linux io_uring (现代高性能异步 IO)
io_uring (Linux 5.1+, 2019):Linux 现代化的异步 I/O 接口
特点: - 共享 ring buffer (mmap,零拷贝) - 支持 sqe (Submission Queue Entry) 提交 - cqe (Completion Queue Entry) 完成 - 极高性能,比 epoll 还快 - 适合高并发服务器、数据库
使用:
#include <liburing.h>
struct io_uring ring;
io_uring_queue_init(64, &ring, 0);
// 准备 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, count, 0);
// 提交
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 处理结果
int res = cqe->res;
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
高级特性: - 固定文件 (Fixed Files) - 注册 buffer (Registered Buffers) - 轮询模式 (Polled IO) - io_uring 的 io_uring,网络 IO 支持,等等
性能: - 比 epoll + 4 次 copy 提升显著 - 微秒级响应
3. mmap + epoll 模式 (高性能服务器常用)
// 接收数据
recv(fd, ..., MSG_PEEK | MSG_WAITALL); // 看数据大小
// mmap 一段 DMA 内存
// 直接处理,免去内存拷贝
4. sendfile 零拷贝
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- 适合静态文件服务(Nginx)
- 2 次上下文切换 → 0 次(直接 DMA 复制)
- 性能大幅提升
5. splice / tee
ssize_t splice(int fd_in, off64_t *off_in, int fd_out,
off64_t *off_out, size_t len, unsigned int flags);
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
- splice:两个 fd 间零拷贝移动数据
- tee:两个 fd 间零拷贝复制数据
- 适合管道(pipe)
6. io_uring vs epoll
| 维度 | epoll | io_uring |
|---|---|---|
| 同步/异步 | 同步(等待就绪) | 异步(等待完成) |
| 系统调用 | 1 次(epoll_wait) | 2 次(submit + wait_cqe) |
| 上下文切换 | 提交和等待各 1 次 | mmap 共享,几乎 0 切换 |
| 适用 | 网络 IO 事件 | 通用 IO(网络 + 磁盘) |
| 性能 | 极好 | 更好(零拷贝) |
| 复杂度 | 简单 | 复杂 |
| 内核版本 | 2.6+ | 5.1+ |
八、IO 设计模式 (Reactor / Proactor)
1. Reactor 模式 (同步)
Reactor = 反应器,事件驱动 + 单线程/多线程
Event Loop:
while (true) {
events = epoll_wait()
for (event in events) {
dispatch(event)
// 根据事件类型,调用 handler
}
}
特点: - 同步 IO (read/write 仍由程序调用) - 多个 IO 复用单线程 - 性能高
代表: - Redis (单线程 Reactor) - Netty (Java) - libuv (Node.js) - libevent / libev (C) - Nginx (多 Reactor)
Nginx 多 Reactor 模式:
Master 进程
├─ Worker 1 (Reactor + Worker pool)
├─ Worker 2
└─ Worker N
每个 Worker:
- Accept 事件
- Read/Write
- 子请求交给线程池
2. Proactor 模式 (异步)
Proactor = 主动器,全程异步
1. 发起异步 IO
2. 内核完成所有 IO
3. 内核(或通过回调)通知结果
4. 程序处理结果
特点: - 真正的异步 IO - 程序不调用 read/write - 实现复杂
代表: - Windows IOCP - Boost.Asio (Proactor 模拟器) - 一些 AIO 实现
3. Reactor vs Proactor
| 维度 | Reactor | Proactor |
|---|---|---|
| 同步性 | 同步 IO | 异步 IO |
| 实现 | epoll / select | IOCP / io_uring |
| 复杂度 | 中 | 高 |
| 性能 | 高 | 更高 |
| 主流程度 | 主流 | 较少 |
| 代表 | Netty、Redis、Nginx | IOCP、Boost.Asio |
九、磁盘 IO 调度
1. IO 调度器
Linux 多队列块设备 IO 调度器 (现代):
| 调度器 | 特点 | 适用 |
|---|---|---|
| none | 不调度,直通 | NVMe SSD |
| mq-deadline | 截止时间调度 | SSD / 一般场景 |
| bfq | Budget Fair Queueing | 桌面/混合 |
| kyber | 令牌桶,简单低延迟 | NVMe SSD |
老式单队列调度器(已废弃): - cfq - deadline - noop
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 设置调度器
echo mq-deadline > /sys/block/sda/queue/scheduler
2. 关键 IO 参数
# 块大小
blockdev --getbsz /dev/sda
fdisk -l /dev/sda
# 预读 (readahead)
blockdev --getra /dev/sda
echo 256 > /sys/block/sda/queue/read_ahead_kb
# 队列深度
cat /sys/block/sda/queue/queue_depth
echo 1024 > /sys/block/sda/queue/queue_depth
3. 磁盘 IO 性能
# 顺序读写测试
dd if=/dev/zero of=/tmp/test bs=1M count=1024 oflag=direct
dd if=/tmp/test of=/dev/null bs=1M iflag=direct
# 随机 IO 测试
fio --name=randread --ioengine=libaio --direct=1 \
--filename=/dev/sda --bs=4k --size=1G \
--rw=randread --iodepth=32 --runtime=60
4. 优化建议
- SSD/NVMe 用
none或mq-deadline - HDD 用
bfq或mq-deadline - 数据库 用
none调度 + 直接 IO (O_DIRECT) - 挂载选项:
noatime,discard - RAID 卡:开启写缓存(配 BBU)
- NUMA 优化:绑本地 NUMA 节点
十、网络 IO
1. 网络 IO 性能
- 延迟:同机房 0.1-0.5 ms,跨城 5-30 ms,跨洲 100-200 ms
- 吞吐:1 Gbps / 10 Gbps / 25 Gbps / 100 Gbps / 200 Gbps / 400 Gbps
- PPS (包每秒):千兆网 ~ 1.5 Mpps,万兆 ~ 15 Mpps
2. 网络优化
# 网卡多队列 (RSS)
ethtool -l eth0
ethtool -L eth0 combined 8
# 中断亲和
echo 0-7 > /proc/irq/24/smp_affinity_list
# 网卡缓冲区
ethtool -G eth0 rx 4096 tx 4096
# TCP 缓冲区
sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
sysctl -w net.ipv4.tcp_wmem="4096 65536 6291456"
# 各种 TCP 调优
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
3. 网络 IO 模型
- Nginx:多 Reactor + epoll
- HAProxy:单 Reactor + epoll
- Envoy:多线程异步 + iouring/IO
- DPDK:用户态驱动
- RDMA:远程直接内存访问
十一、零拷贝 (Zero Copy)
1. 为什么需要
- 传统 IO:磁盘 → 内核缓冲 → 用户缓冲 → socket
- 多次拷贝,浪费 CPU
- 零拷贝:减少甚至消除数据拷贝
2. 零拷贝技术
| 技术 | 拷贝次数 | 系统调用 | 适用 |
|---|---|---|---|
| 传统 read+write | 4 次 | read + write | - |
| mmap + write | 3 次 | mmap + write | 通用 |
| sendfile | 2 次 | sendfile | 静态文件 |
| splice | 2 次 | splice | 管道/网络 |
| SG-DMA + sendfile | 1 次 | sendfile | 网卡支持 SG |
| DPDK | 0 次 | 用户态驱动 | 极致性能 |
| io_uring | 0~1 次 | 共享 ring | 现代 |
3. sendfile 详解
// 0 拷贝发送文件内容
int sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- in_fd:文件描述符
- out_fd:目标文件描述符(通常是 socket)
- 数据直接在内核的 file cache → socket buffer
- 不经过用户空间
4. 静态文件服务的零拷贝
// 简单回声 + 零拷贝
int server = socket(...);
while (1) {
int client = accept(server, ...);
// 客户端请求 GET /bigfile.mp4
int file = open("/bigfile.mp4", O_RDONLY);
struct stat st;
fstat(file, &st);
sendfile(client, file, NULL, st.st_size);
close(file);
close(client);
}
十二、IO 命令与工具
1. 监控 IO
# 整体
iostat -x 1
iotop
vmstat 1
dstat
# 进程级
iotop -p PID
cat /proc/PID/io
# 详细
perf stat -e block:* command
bpftrace -e 'tracepoint:block:block_rq_issue { @[comm] = count(); }'
2. 压测工具
# 磁盘
fio
dd
hdparm -tT /dev/sda
# 网络
iperf3
netperf
wrk
ab (Apache Bench)
3. 调优
# 内核参数
sysctl -a | grep vm
sysctl -a | grep net
sysctl -a | grep fs
# /proc/sys/vm
vm.dirty_ratio
vm.dirty_background_ratio
vm.swappiness
vm.vfs_cache_pressure
十三、IO 性能调优实战
1. Web 服务器
# Nginx
worker_processes auto;
worker_rlimit_nofile 1000000;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
open_file_cache max=1000;
}
2. 数据库 (MySQL/PG)
# 文件系统挂载
mount -o noatime,discard,data=writeback /dev/sda1 /var/lib/mysql
# IO 调度器
echo none > /sys/block/sda/queue/scheduler
# 关键参数
innodb_flush_method=O_DIRECT
innodb_io_capacity=2000
innodb_read_io_threads=8
innodb_write_io_threads=8
3. 高性能服务器
- 单 Reactor + 多线程
- 多 Reactor + 线程池
- io_uring + coroutine
- DPDK 旁路内核
十四、核心要点速记
- 5 大 IO 模型:阻塞、非阻塞、多路复用、信号驱动、AIO
- epoll = Linux 高性能 IO 多路复用,O(1) 复杂度
- LT (水平触发) = 默认,有数据就触发
- ET (边缘触发) = 状态变化才触发,必须用非阻塞 + 完整读
- Reactor = 事件驱动 + 多路复用 (主流)
- Proactor = 真异步,需系统支持 (Windows IOCP / io_uring)
- io_uring = Linux 5.1+ 现代化异步 IO,零拷贝
- 零拷贝 = sendfile / splice / mmap / io_uring
- IO 调度器:NVMe=none/mq-deadline,HDD=bfq
- select 限制 1024 fd,poll/epoll 不限
- epoll 三 API:epoll_create / epoll_ctl / epoll_wait
- mmap = 高效内存映射
- sendfile = 静态文件服务最佳
- 直接 IO O_DIRECT = 绕过 Page Cache
- iostat -x 1 = 看磁盘 IO
- iotop = 实时进程 IO
- fio = 磁盘压测神器
- net.ipv4.tcp_rmem/wmem = TCP 缓冲区
- Reactor 单线程 = Redis,Nginx
- 多 Reactor = Netty、Nginx worker
- ASLR = 内存地址随机化(安全)
- DPDK = 用户态驱动,极致性能
- RDMA = 远程直接内存访问
- ASLR / PIE = 缓解缓冲区溢出攻击