操作系统IO模型与多路复用
2026/3/20大约 16 分钟
操作系统 IO 模型与多路复用
I/O 操作的本质
什么是 I/O
I/O(Input/Output)是指计算机与外部世界进行数据交换的过程。在网络编程中,I/O 主要包括:
I/O 操作的两个阶段
网络 I/O 操作可以分为两个明确的阶段:
五种 I/O 模型
Unix/Linux 系统定义了五种 I/O 模型,理解这些模型是掌握事件循环的基础。
1. 阻塞 I/O(Blocking I/O)
最传统的 I/O 模型,进程在 I/O 操作期间被阻塞:
代码示例:
// 阻塞I/O示例
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 默认是阻塞模式
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
// 程序会在这里阻塞,直到有数据到达
2. 非阻塞 I/O(Non-blocking I/O)
进程发起 I/O 请求后立即返回,通过轮询检查数据是否就绪:
代码示例:
// 非阻塞I/O示例
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置为非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
char buffer[1024];
while (1) {
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0) {
// 成功读取数据
break;
} else if (n == -1 && errno == EWOULDBLOCK) {
// 数据未就绪,继续轮询
continue;
} else {
// 发生错误
break;
}
}
3. I/O 多路复用(I/O Multiplexing)
使用单个线程监控多个文件描述符,这是事件循环的核心技术:
4. 信号驱动 I/O(Signal-driven I/O)
使用信号通知进程数据就绪:
5. 异步 I/O(Asynchronous I/O)
真正的异步模型,内核完成所有工作后通知进程:
五种模型对比
I/O 多路复用技术详解
select
select 是最古老的 I/O 多路复用技术,POSIX 标准,跨平台支持:
#include <sys/select.h>
int select(int nfds,
fd_set *readfds, // 监控可读事件
fd_set *writefds, // 监控可写事件
fd_set *exceptfds, // 监控异常事件
struct timeval *timeout);
工作原理:
代码示例:
#include <sys/select.h>
#include <sys/time.h>
int main() {
fd_set readfds;
struct timeval timeout;
int max_fd = sockfd;
while (1) {
// 每次循环都要重新设置
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(sockfd, &readfds)) {
// sockfd可读
char buffer[1024];
recv(sockfd, buffer, sizeof(buffer), 0);
}
} else if (ret == 0) {
// 超时
} else {
// 错误
}
}
}
select 的局限性:
| 局限 | 说明 |
|---|---|
| fd 数量限制 | 默认最大 1024 个(FD_SETSIZE) |
| 效率问题 | 每次调用都需要拷贝 fd_set 到内核 |
| 线性扫描 | 需要遍历所有 fd 检查就绪状态 |
| 状态重置 | 每次调用后 fd_set 被修改,需要重新设置 |
poll
poll 是 select 的改进版本,解决了 fd 数量限制:
#include <poll.h>
struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件
short revents; // 返回的事件
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
工作原理:
代码示例:
#include <poll.h>
#define MAX_CLIENTS 10000
int main() {
struct pollfd fds[MAX_CLIENTS];
int nfds = 0;
// 添加监听socket
fds[0].fd = listen_fd;
fds[0].events = POLLIN;
nfds = 1;
while (1) {
int ret = poll(fds, nfds, 5000); // 5秒超时
if (ret > 0) {
for (int i = 0; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
if (fds[i].fd == listen_fd) {
// 新连接
int client_fd = accept(listen_fd, NULL, NULL);
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN;
nfds++;
} else {
// 客户端数据
char buffer[1024];
recv(fds[i].fd, buffer, sizeof(buffer), 0);
}
}
}
}
}
}
poll vs select:
| 特性 | select | poll |
|---|---|---|
| fd 数量限制 | 默认 1024 | 无限制(受系统资源限制) |
| 数据结构 | 位图(fd_set) | 数组(pollfd) |
| 状态重置 | 需要每次重置 | events 和 revents 分离,无需重置 |
| 效率 | O(n) | O(n) |
| 跨平台 | 更好 | 较好 |
epoll
epoll 是 Linux 特有的 I/O 多路复用技术,专为高并发设计:
#include <sys/epoll.h>
// 创建epoll实例
int epoll_create(int size);
int epoll_create1(int flags);
// 控制epoll
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);
struct epoll_event {
uint32_t events; // epoll事件
epoll_data_t data; // 用户数据
};
工作原理:
代码示例:
#include <sys/epoll.h>
#define MAX_EVENTS 10000
int main() {
// 创建epoll实例
int epfd = epoll_create1(0);
// 添加监听socket到epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
// 等待事件,只返回就绪的fd
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nready; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
// 新连接
int client_fd = accept(listen_fd, NULL, NULL);
// 设置非阻塞
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 添加到epoll
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
// 客户端数据
char buffer[1024];
ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
if (n <= 0) {
// 连接关闭
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}
}
}
epoll 的触发模式
边缘触发的正确用法:
// 边缘触发必须一次读完所有数据
while (1) {
ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
if (n > 0) {
// 处理数据
process_data(buffer, n);
} else if (n == 0) {
// 连接关闭
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据读完了
break;
}
// 发生错误
break;
}
}
跨平台多路复用
不同操作系统有不同的 I/O 多路复用实现:
| 系统 | 技术 | 特点 | 场景 |
|---|---|---|---|
| Linux | epoll | 高效、支持 ET | 大规模服务器 |
| macOS,FreeBSD | kqueue | 功能丰富 | macOS/BSD 服务 |
| Windows | IOCP | 真异步 I/O | Windows 服务器 |
| Solaris | /dev/poll/event ports | 类似 epoll | Solaris 服务 |
| 跨平台 | libuv,libevent | 封装各平台统一 API | Node.js 等, 跨平台应用 |
kqueue(macOS/BSD)
#include <sys/event.h>
int main() {
int kq = kqueue();
struct kevent ev;
EV_SET(&ev, listen_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &ev, 1, NULL, 0, NULL);
struct kevent events[1024];
while (1) {
int nev = kevent(kq, NULL, 0, events, 1024, NULL);
for (int i = 0; i < nev; i++) {
int fd = events[i].ident;
// 处理事件...
}
}
}
IOCP(Windows)
Windows 的 I/O 完成端口是真正的异步 I/O:
// Windows IOCP示例(伪代码)
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE,
NULL, 0, 0);
// 关联socket到IOCP
CreateIoCompletionPort((HANDLE)socket, iocp, (ULONG_PTR)socket, 0);
// 发起异步读取
WSARecv(socket, &buffer, 1, NULL, &flags, &overlapped, NULL);
// 等待完成
DWORD bytes;
ULONG_PTR key;
LPOVERLAPPED ov;
GetQueuedCompletionStatus(iocp, &bytes, &key, &ov, INFINITE);
性能对比
基准测试结果
注:select 在 Linux 上默认限制 1024 个 fd,N/A 表示无法处理该数量级的连接
时间复杂度分析
| 操作 | select | poll | epoll |
|---|---|---|---|
| 添加 fd | O(1) | O(1) | O(log n) |
| 删除 fd | O(1) | O(1) | O(log n) |
| 等待事件 | O(n) | O(n) | O(1) |
| 返回就绪 | O(n) | O(n) | O(k),k 为就绪数 |
本章小结
核心要点
- I/O 模型理解:掌握五种 I/O 模型的区别和适用场景
- 多路复用本质:用一个线程监控多个 I/O 连接
- epoll 优势:红黑树存储、就绪队列返回、边缘触发