在C++网络编程里,I/O多路复用是指利用操作系统提供的机制,让一个进程或线程可以同时监视多个套接字或其他文件描述符,一旦其中某些描述符就绪(可读、可写或出现异常),就通知程序进行处理,从而避免为每个连接创建一个线程带来的资源消耗。

什么是I/O多路复用
传统的阻塞式I/O模型中,调用recv或accept时线程会一直等待,直到数据到达。当并发连接变多,线程数量暴涨,上下文切换成本很高。I/O多路复用把“等待多个描述符就绪”这件事交给内核,程序只需要在内核通知后,对已经就绪的描述符做非阻塞读写。
常见的三种实现
- select:出现最早,跨平台支持好
- poll:解决了select的部分限制
- epoll:Linux下高性能方案
select原理与使用
select通过三个fd_set位图分别监听读、写和异常事件,每次调用都需要把集合从用户态拷贝到内核态,内核遍历所有描述符,返回后用户也要遍历来找出就绪的描述符。
#include <sys/select.h>
#include <unistd.h>
#include <iostream>
int main() {
fd_set readfds;
FD_ZERO(&readfds);
int listen_fd = 3; // 假设监听套接字为3
FD_SET(listen_fd, &readfds);
// 设置超时时间
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
// 调用select,最大描述符+1
int ret = select(listen_fd + 1, &readfds, NULL, NULL, &tv);
if (ret > 0) {
if (FD_ISSET(listen_fd, &readfds)) {
std::cout << "listen_fd可读,有新连接" << std::endl;
}
}
return 0;
}
poll原理与使用
poll使用pollfd结构体数组代替select的位图,没有描述符数量硬限制,但每次调用仍要遍历全部描述符,随着监控数量增加,效率会下降。
#include <poll.h>
#include <iostream>
int main() {
struct pollfd fds[1];
fds[0].fd = 3; // 监听套接字
fds[0].events = POLLIN; // 关注可读事件
fds[0].revents = 0;
int ret = poll(fds, 1, 5000); // 超时5秒
if (ret > 0) {
if (fds[0].revents & POLLIN) {
std::cout << "描述符可读" << std::endl;
}
}
return 0;
}
epoll原理与使用
epoll是Linux特有的机制,内核使用红黑树管理监听的描述符,并用就绪链表记录就绪事件。程序通过epoll_wait只取回已经就绪的描述符,不需要遍历全部监控项,非常适合海量连接。
#include <sys/epoll.h>
#include <iostream>
int main() {
int epfd = epoll_create1(0);
struct epoll_event ev, events[10];
ev.events = EPOLLIN;
ev.data.fd = 3; // 监听套接字
epoll_ctl(epfd, EPOLL_CTL_ADD, 3, &ev);
int n = epoll_wait(epfd, events, 10, 5000);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == 3) {
std::cout << "有新连接或数据到达" << std::endl;
}
}
return 0;
}
三者对比
| 机制 | 描述符上限 | 效率特点 | 平台 |
|---|---|---|---|
| select | 有限(如1024) | 每次全量拷贝和遍历 | 跨平台 |
| poll | 无硬限制 | 仍全量遍历 | 跨平台 |
| epoll | 系统限制 | 只返回就绪项 | Linux |
使用建议
在编写C++网络程序时,如果目标环境是Linux且需要支撑高并发,优先使用epoll;如果需要可移植性且连接数不多,select或poll更简单。无论使用哪种方式,都应将套接字设为非阻塞模式,配合多路复用避免线程卡死。
C++_I/O多路复用selectpollepoll网络编程修改时间:2026-07-25 12:54:25