Linux I/O 多路复用:select、poll 和 epoll 详解
引言
在 Linux 网络编程中,I/O 多路复用技术是构建高性能服务器的关键。它允许程序同时监控多个文件描述符(如套接字)的状态变化(可读、可写、异常等)。本文将详细介绍三种主要的 I/O 多路复用机制:select、poll 和 epoll,分析它们的工作原理、API 使用、示例代码以及各自的优缺点。
- select
1.1 基本概念
select 是最早出现的 I/O 多路复用机制,它使用位图(fd_set)来表示文件描述符集合。
1.2 函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
1.3 参数说明
• nfds
: 需要监控的文件描述符的最大值加 1
• readfds
: 监控可读事件的文件描述符集合
• writefds
: 监控可写事件的文件描述符集合
• exceptfds
: 监控异常事件的文件描述符集合
• timeout
: 超时时间,NULL 表示阻塞等待,0 表示立即返回
1.4 fd_set 操作宏
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符到集合
FD_CLR(int fd, fd_set *set); // 从集合移除描述符
FD_ISSET(int fd, fd_set *set); // 检查描述符是否在集合中
1.5 示例代码
#include <sys/select.h>
#define MAX_SOCK_FD 1024
int main() {
fd_set set, tmpset;
FD_ZERO(&set);
// 添加监听套接字到集合
int listen_fd = socket(...);
FD_SET(listen_fd, &set);
while(1) {
tmpset = set;
int ret = select(MAX_SOCK_FD, &tmpset, NULL, NULL, NULL);
if (FD_ISSET(listen_fd, &tmpset)) {
// 处理新连接
int new_fd = accept(...);
FD_SET(new_fd, &set);
}
// 检查其他描述符
for (int i = 0; i < MAX_SOCK_FD; i++) {
if (i != listen_fd && FD_ISSET(i, &tmpset)) {
// 处理数据
if (handle_data(i) <= 0) {
FD_CLR(i, &set);
close(i);
}
}
}
}
return 0;
}
- poll
2.1 基本概念
poll 改进了 select 的一些限制,使用链表结构代替位图,没有最大文件描述符数量的限制。
2.2 函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.3 pollfd 结构体
struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件
short revents; // 实际发生的事件
};
2.4 事件标志
• POLLIN
: 有数据可读
• POLLOUT
: 可以写入数据
• POLLERR
: 发生错误
• POLLHUP
: 连接挂起
• POLLNVAL
: 文件描述符未打开
2.5 示例代码
#include <poll.h>
#define MAX_SOCK_FD 1024
int main() {
struct pollfd fds[MAX_SOCK_FD];
nfds_t nfds = 1;
// 初始化监听套接字
int listen_fd = socket(...);
fds[0].fd = listen_fd;
fds[0].events = POLLIN;
while(1) {
int ret = poll(fds, nfds, -1);
for (int i = 0; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
if (fds[i].fd == listen_fd) {
// 处理新连接
int new_fd = accept(...);
fds[nfds].fd = new_fd;
fds[nfds].events = POLLIN;
nfds++;
} else {
// 处理数据
if (handle_data(fds[i].fd) <= 0) {
close(fds[i].fd);
// 从数组中移除
for (int j = i; j < nfds-1; j++) {
fds[j] = fds[j+1];
}
nfds--;
i--;
}
}
}
}
}
return 0;
}
- epoll
3.1 基本概念
epoll 是 Linux 特有的高性能 I/O 多路复用机制,解决了 select 和 poll 的性能瓶颈。
3.2 核心 API
3.2.1 epoll_create
#include <sys/epoll.h>
int epoll_create(int size); // size 参数现在被忽略,但必须大于0
int epoll_create1(int flags); // 支持更多选项
3.2.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
操作类型:
• EPOLL_CTL_ADD
: 添加
• EPOLL_CTL_MOD
: 修改
• EPOLL_CTL_DEL
: 删除
3.2.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
3.3 epoll_event 结构体
struct epoll_event {
uint32_t events; // 事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
3.4 事件标志
• EPOLLIN
: 可读
• EPOLLOUT
: 可写
• EPOLLERR
: 错误
• EPOLLHUP
: 挂起
• EPOLLET
: 边缘触发模式
3.5 工作模式
- 水平触发(LT): 默认模式,只要状态满足就会通知
- 边缘触发(ET): 只在状态变化时通知,需要一次性处理完所有数据
3.6 示例代码
#include <sys/epoll.h>
#define MAX_EVENTS 64
int main() {
int epfd = epoll_create1(0);
struct epoll_event event, events[MAX_EVENTS];
// 添加监听套接字
int listen_fd = socket(...);
event.events = EPOLLIN;
event.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event);
while(1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
// 处理新连接
int new_fd = accept(...);
event.events = EPOLLIN | EPOLLET; // 边缘触发
event.data.fd = new_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);
} else {
// 处理数据
if (handle_data(events[i].data.fd) <= 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
}
}
}
}
close(epfd);
return 0;
}
- 三种机制对比
特性 | select | poll | epoll |
---|---|---|---|
最大文件描述符数 | 有限制(通常1024) | 无限制 | 无限制 |
效率 | O(n)轮询 | O(n)轮询 | O(1)回调 |
内存拷贝 | 每次调用都需要拷贝 | 每次调用都需要拷贝 | 仅注册时拷贝 |
触发方式 | 水平触发 | 水平触发 | 支持水平/边缘触发 |
实现方式 | 轮询 | 轮询 | 回调 |
适用场景 | 低并发、跨平台 | 中低并发 | 高并发、Linux专用 |
-
性能分析与选择建议
-
select:
• 优点:跨平台支持好• 缺点:性能差,文件描述符数量有限
• 适用:低并发、需要跨平台的场景
-
poll:
• 优点:无文件描述符数量限制• 缺点:性能仍不理想
• 适用:中低并发、需要支持更多连接
-
epoll:
• 优点:高性能,支持边缘触发• 缺点:仅限Linux
• 适用:高并发服务器