一、poll系统调用
poll是System V引入的I/O多路复用函数,它克服了select的一些限制(如文件描述符数量上限)。poll通过一个结构体数组来监视多个文件描述符的事件。
1. 函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
参数:
-
fds:指向struct pollfd数组的指针,每个元素描述一个待监视的文件描述符及其感兴趣的事件。 -
nfds:fds数组中的元素个数。 -
timeout:超时时间(毫秒)。-
-1:阻塞直到有事件发生; -
0:立即返回,不阻塞; -
>0:等待指定的毫秒数。
-
-
-
返回值:
-
成功:返回就绪(有事件发生)的文件描述符个数。
-
超时:返回0。
-
出错:返回-1,并设置errno。
-
struct pollfd定义如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件掩码 */
short revents; /* 实际发生的事件掩码 */
};
events和revents是由以下标志按位或组成的位掩码:
-
POLLIN:有数据可读。 -
POLLOUT:可写数据。 -
POLLERR:发生错误(仅输出)。 -
POLLHUP:挂起(仅输出)。 -
等等。
2. 使用示例
struct pollfd fds[2];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
fds[1].fd = STDIN_FILENO;
fds[1].events = POLLIN;
int ret = poll(fds, 2, 5000); // 等待5秒
if (ret > 0) {
for (int i = 0; i < 2; i++) {
if (fds[i].revents & POLLIN) {
// 处理该文件描述符的读事件
}
}
} else if (ret == 0) {
// 超时处理
} else {
perror("poll");
}
3. poll的特点与局限性
-
优点:
-
没有最大文件描述符数量的限制(基于链表存储,受系统内存约束)。
-
接口相对简单,支持多种事件类型。
-
-
缺点:
-
每次调用都需要将
pollfd数组从用户态拷贝到内核态,当监视大量文件描述符时开销较大。 -
内核检测到事件后,仍需遍历整个数组以查找哪些描述符就绪(线性扫描),时间复杂度O(n)。
-
无法动态修改监视的描述符集合(需要重新组织数组并调用poll)。
-
只能工作在水平触发模式(Level-Triggered, LT),即只要文件描述符处于就绪状态,每次poll都会报告该事件。
-
二、epoll:Linux特有的高性能I/O事件通知机制
epoll是Linux内核为处理大批量文件描述符而引入的增强版I/O多路复用接口,它解决了poll和select的性能瓶颈。epoll通过内核事件表、回调机制和内存映射等技术,实现了高效的I/O事件通知。
1. 核心函数
epoll提供三个系统调用:
(1) epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
-
功能:创建一个epoll实例,返回一个指向内核事件表的文件描述符(称为epfd)。
-
参数 :
size提示内核事件表的大小(Linux 2.6.8之后被忽略,但必须大于0)。 -
返回值:成功返回新的文件描述符,失败返回-1。
(2) epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
功能:对内核事件表进行控制:添加、修改或删除一个监视的文件描述符。
-
参数:
-
epfd:epoll实例的文件描述符。 -
op:操作类型,可取:-
EPOLL_CTL_ADD:添加fd到事件表。 -
EPOLL_CTL_MOD:修改fd上已注册的事件。 -
EPOLL_CTL_DEL:从事件表中删除fd。
-
-
fd:要操作的文件描述符。 -
event:指向struct epoll_event的指针,描述感兴趣的事件和用户数据。
-
struct epoll_event定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* 感兴趣的事件掩码 */
epoll_data_t data; /* 用户数据 */
};
events可以是以下宏的按位或:
-
EPOLLIN:可读。 -
EPOLLOUT:可写。 -
EPOLLRDHUP:流套接字对端关闭连接。 -
EPOLLPRI:有紧急数据可读。 -
EPOLLERR:发生错误(自动设置,无需手动注册)。 -
EPOLLHUP:挂起(自动设置)。 -
EPOLLET:设置为边缘触发模式(Edge-Triggered, ET)。 -
EPOLLONESHOT:事件只触发一次,触发后需要重新注册。
(3) epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
功能:等待事件表中的文件描述符产生事件。
-
参数:
-
epfd:epoll实例的文件描述符。 -
events:用户提供的数组,用于存放内核返回的就绪事件。 -
maxevents:events数组的大小(即最多返回多少个事件)。 -
timeout:超时时间(毫秒),语义与poll相同。
-
-
返回值:成功返回就绪事件个数;超时返回0;失败返回-1。
2. 使用示例(服务器监听socket)
int epfd = epoll_create(1);
struct epoll_event ev, events[10];
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_sock) {
// 处理新连接
int conn = accept(listen_sock, ...);
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = conn;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
} else {
// 处理已连接套接字的读写
}
}
}
3. epoll的工作模式:水平触发与边缘触发
-
水平触发(LT,Level-Triggered):默认模式。当文件描述符就绪时,epoll_wait会返回该事件,如果程序没有一次性处理完所有数据,下一次调用epoll_wait会再次报告该事件,直到数据被处理完。这种模式编程简单,不容易遗漏事件,但可能重复触发。
-
边缘触发(ET,Edge-Triggered) :需要设置
EPOLLET标志。当文件描述符从未就绪变为就绪时,epoll_wait仅返回一次该事件。如果程序没有处理完所有数据,后续不再通知,除非描述符再次出现新的状态变化。ET模式要求程序员必须一次性将数据全部读取或写入(通常使用非阻塞I/O循环处理),否则可能造成数据丢失或饥饿。ET模式效率更高,减少了epoll的重复触发次数,但编程复杂度较高。
4. epoll的优势
-
无文件描述符数量上限:epoll监视的描述符数量只受系统内存限制。
-
事件驱动,避免线性扫描:内核通过回调机制将就绪的描述符加入就绪队列,epoll_wait直接返回就绪队列,时间复杂度O(1)(仅返回就绪个数)。
-
内存映射减少拷贝:epoll使用mmap在内核和用户空间共享事件表,避免了用户态到内核态的数据拷贝(事件注册时仍需拷贝,但相比poll的每次全量拷贝要少)。
-
支持边缘触发,在高并发场景下可进一步减少系统调用次数。
-
可修改监视事件:通过epoll_ctl动态添加、删除、修改监视的描述符,无需重新构建整个集合。
三、select、poll、epoll对比总结
| 特性 | select | poll | epoll |
|---|---|---|---|
| 底层数据结构 | 位数组(fd_set) | pollfd数组(链表) | 红黑树+就绪链表 |
| 最大连接数 | 有限(通常1024) | 无上限(受内存限制) | 无上限(受内存限制) |
| 事件集合拷贝 | 每次调用都从用户态拷贝到内核态 | 每次调用都从用户态拷贝到内核态 | 使用epoll_ctl注册,通过mmap共享,减少拷贝 |
| 查找就绪描述符方式 | 线性遍历所有fd | 线性遍历所有fd | 直接返回就绪队列,无需遍历 |
| 工作模式 | 仅LT | 仅LT | 支持LT和ET |
| 修改监视集 | 需要重新构造fd_set并重调select | 需要重新组织pollfd数组并重调poll | 使用epoll_ctl动态增删改,无需重建 |
| 时间复杂度(获取就绪fd) | O(n) | O(n) | O(1)(就绪个数) |
| 可移植性 | 广泛支持(POSIX) | 广泛支持(POSIX) | Linux特有 |
四、适用场景建议
-
select:适用于连接数较少(<1024)且对可移植性要求高的场景,代码简单。
-
poll:相比select没有最大连接数限制,但仍有线性遍历开销,适合中等规模的连接数(几千以内)。
-
epoll:高并发服务器(如C10K问题)的首选,尤其是当连接数巨大且活动连接比例较低时,ET模式能最大化性能。但需注意epoll是Linux专属,跨平台需考虑替代方案(如libevent、libuv等封装库)。