I/O 模型
hard操作系统IO模型阻塞IO非阻塞IOIO多路复用selectpollepoll
I/O 操作的两个阶段
一次网络 I/O(以 read 为例)涉及两个阶段:
应用程序 内核
│ │
│ read() │
│ ────────────────▶│
│ │ 阶段一:等待数据就绪
│ │ (等网卡收到数据包并
│ │ 复制到内核缓冲区)
│ │
│ │ 阶段二:数据拷贝
│ │ (从内核缓冲区复制到
│ 返回数据 │ 用户空间缓冲区)
│ ◀────────────────│
不同 I/O 模型的区别就在于这两个阶段是否阻塞进程。
五种 I/O 模型
1. 阻塞 I/O(BIO)
用户进程 内核
│ │
│ read() │ ┐
│ ────────────────▶│ │
│ 阻塞... │ 等待数据 │ 两个阶段
│ 阻塞... │ │ 都阻塞
│ 阻塞... │ 拷贝数据 │
│ ◀────────────────│ ┘
│ 处理数据 │
进程调用 read() 后一直阻塞,直到数据准备好并拷贝到用户空间才返回。最简单但效率最低——一个线程同一时刻只能处理一个连接。
2. 非阻塞 I/O(NIO)
用户进程 内核
│ read() │
│ ────────────────▶│ 数据没准备好
│ ◀──── EAGAIN ────│ 立即返回
│ read() │
│ ────────────────▶│ 数据没准备好
│ ◀──── EAGAIN ────│ 立即返回
│ ... 反复轮询 ... │
│ read() │
│ ────────────────▶│ 数据准备好了
│ 阻塞... │ 拷贝数据(仍阻塞)
│ ◀────────────────│
│ 处理数据 │
read() 不阻塞,数据没准备好就立即返回错误码。进程需要不断轮询检查数据是否就绪,CPU 浪费在无意义的轮询上。
3. I/O 多路复用
用一个线程监控多个 fd(文件描述符),哪个就绪就处理哪个。这是高并发服务器的核心技术。
用户进程 内核
│ │
│ select/poll/ │
│ epoll_wait() │
│ ────────────────▶│ 同时监控 fd1, fd2, fd3...
│ 阻塞等待... │
│ │ fd2 数据就绪!
│ ◀────────────────│ 返回就绪的 fd 列表
│ │
│ read(fd2) │
│ ────────────────▶│ 拷贝数据
│ ◀────────────────│
│ 处理数据 │
4. 信号驱动 I/O
注册信号处理函数,数据就绪时内核发送 SIGIO 信号通知进程。实践中很少使用。
5. 异步 I/O(AIO)
用户进程 内核
│ aio_read() │
│ ────────────────▶│ 立即返回(不阻塞)
│ 做其他事... │
│ │ 等待数据
│ │ 拷贝数据到用户空间
│ ◀── 信号/回调 ────│ 全部完成后通知
│ 处理数据 │
两个阶段都不阻塞,是真正的异步。Linux 的 io_uring(5.1+)是现代高性能异步 I/O 接口。
select / poll / epoll 对比
这三个是 Linux 上 I/O 多路复用的三个 API,性能逐代提升:
select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 将关注的 fd 集合从用户态复制到内核态
- 内核线性遍历所有 fd,检查是否就绪
- 返回后用户需要再次遍历找出就绪的 fd
poll
与 select 类似,但用链表代替位图,没有 1024 的限制。仍然需要线性遍历。
epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll 的三大改进:
| 改进 | select/poll | epoll |
|---|---|---|
| fd 注册 | 每次调用都要传全部 fd | epoll_ctl 一次注册,内核维护红黑树 |
| 就绪检测 | 线性遍历所有 fd — O(n) | 回调机制,就绪的 fd 直接加入就绪链表 — O(1) |
| 结果获取 | 需要遍历全部 fd 找就绪的 | epoll_wait 直接返回就绪列表 |
| fd 数量限制 | select 最多 1024 | 无限制(取决于系统资源) |
| 内存拷贝 | 每次调用拷贝全部 fd | mmap 共享内存,减少拷贝 |
epoll 的两种触发模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
| LT(水平触发) | 只要 fd 有数据可读就会通知,没读完下次还通知 | 默认模式,编程简单 |
| ET(边缘触发) | 只在 fd 状态变化时通知一次,必须一次读完 | 高性能场景,需配合非阻塞 I/O |
生产高频题
select、poll、epoll 的区别?
select 有 1024 fd 限制,每次调用需拷贝全部 fd 集合并线性遍历。poll 去掉了数量限制但仍需线性遍历。epoll 用红黑树管理 fd、回调机制通知就绪事件,时间复杂度 O(1),适合高并发。
什么是 epoll 的 ET 和 LT 模式?
LT(水平触发):只要 fd 可读就一直通知,编程简单。ET(边缘触发):仅在状态变化时通知一次,必须一次读完所有数据,性能更高但编程复杂,需配合非阻塞 I/O。
为什么 Nginx/Redis 使用 epoll?
因为它们需要同时处理大量并发连接(C10K 问题)。epoll 在大量连接时性能远优于 select/poll,其 O(1) 的事件通知机制不会随连接数增加而性能下降。