当前位置: 首页 > news >正文

IO复用:epoll

一、epoll含义

epoll 是 Linux 特有的高性能 I/O 事件通知机制,主要用来替代传统的 select/poll,尤其在连接数非常多时性能优势极其明显,被几乎所有高并发网络服务器广泛使用。

二、为什么需要epoll?

机制最大描述符数限制每次调用时间复杂度是否需要每次都拷贝 fd 集合到内核返回后是否需要遍历全部 fd
select通常 1024(可改)O(n)是(fd_set 拷贝)是(O(n) 轮询)
poll无硬性限制O(n)是(pollfd 数组拷贝)是(O(n) 轮询)
epoll无硬性限制O(1) 注册,O(k) 等待(k 为就绪事件数)只需要注册一次

只需要遍历就绪的 k 个

三、epoll的三种核心系统调用

// 1. 创建一个 epoll 实例(返回一个 epoll 文件描述符) int epoll_create(int size); // size 只是提示,内核现在忽略它 int epoll_create1(int flags); // 推荐使用,flags 可设 EPOLL_CLOEXEC // 2. 往 epoll 实例中添加/修改/删除感兴趣的文件描述符和事件 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); op 可取值: EPOLL_CTL_ADD // 注册 EPOLL_CTL_MOD // 修改已注册 fd 的事件 EPOLL_CTL_DEL // 删除(event 可为 NULL) 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; // 常用 events 标志(可 | 组合) EPOLLIN // 可读(包括对端正常关闭) EPOLLOUT // 可写 EPOLLERR // 错误 EPOLLHUP // 挂起(对端异常关闭) EPOLLRDHUP // 半关闭(对端 shutdown WR 或关闭,Linux 2.6.17+) EPOLLPRI // 带外数据 EPOLLONESHOT // 一旦触发后自动禁用,需重新 EPOLL_CTL_MOD 启用(防惊群) EPOLLET // 边缘触发 Edge Triggered(关键!默认是水平触发 LT) // 3. 等待事件发生(类似 select) int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // timeout: -1 永久阻塞,0 立即返回,>0 毫秒 // 返回值:就绪的事件个数(0 表示超时)

四、两种工作模式

特性水平触发 LT(默认)边缘触发 ET(需显式设置 EPOLLET)
触发条件只要缓冲区还有数据可读/可写就会一直通知只有在状态变化时才通知一次(比如从不可读→可读)
是否需要读完所有数据不需要,读一部分下次还会再通知必须一次读完(用非阻塞 + 循环读到 EAGAIN 为止)
编程难度简单,和 select/poll 几乎一样复杂,容易漏事件,必须配合非阻塞 I/O
性能稍低(可能产生更多系统调用)更高(系统调用次数最少)
典型使用场景初学者、逻辑简单、少量连接高并发服务器(Nginx、Redis 都是 ET 模式)

ET 模式必备技巧

把 socket 设置为非阻塞(fcntl(fd, F_SETFL, O_NONBLOCK))

读时循环 read/recv 直到返回 EAGAIN

写时同理(通常只有在 write 返回 EAGAIN 时才关注 EPOLLOUT)

五、高并发tcp服务器

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/epoll.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <errno.h> #define MAXFD 10 int socket_init(); void setnonblock(int fd) { int oldfl = fcntl(fd,F_GETFL); int newfl = oldfl | O_NONBLOCK; if( fcntl(fd,F_SETFL,newfl) == -1) { printf("fcntl err\n"); } } void epoll_add(int epfd, int fd) { struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN | EPOLLET;//开启ET模式 setnonblock(fd);//设置非阻塞 if( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1) { printf("epoll add err\n"); } } void epoll_del(int epfd, int fd) { if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1) { printf("epoll del err\n"); } } void accept_client(int sockfd,int epfd) { int c = accept(sockfd,NULL,NULL); if( c < 0 ) { return; } printf("accept c=%d\n",c); epoll_add(epfd,c);//把新接受的连接添加到内核事件表(红黑树) } void recv_data(int c,int epfd) { while( 1 ) { char buff[128] = {0}; int n = recv(c,buff,1,0); if( n == -1 ) { if( errno != EAGAIN && errno != EWOULDBLOCK ) { printf("recv err"); } else { send(c,"ok",2,0); } break; } else if( n == 0 ) { epoll_del(epfd,c); close(c); printf("client close\n"); break; } else { printf("buff=%s\n",buff); } } } int main() { int sockfd = socket_init(); if( sockfd == -1) { exit(1); } int epfd = epoll_create(MAXFD);//创建内核事件表(收集描述符) 红黑树 if( epfd == -1) { exit(1); } epoll_add(epfd,sockfd);//将监听套接字sockfd添加到内核事件表 struct epoll_event evs[MAXFD];//存放就绪描述符 while(1) { int n = epoll_wait(epfd,evs,MAXFD,5000);//获取就绪描述符 可能阻塞 printf("------wait----\n"); if( n == -1 ) { printf("epoll err\n"); } else if ( n == 0 ) { printf("time out\n"); } else { for(int i = 0; i < n; i++) { int fd = evs[i].data.fd; if( evs[i].events & EPOLLIN) { if( fd == sockfd ) { accept_client(sockfd,epfd); } else { recv_data(fd,epfd); } } //if( evs[i].events & EPOLLOUT) } } } } int socket_init() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { return -1; } struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(6000); saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); if (res == -1) { printf("bind err\n"); return -1; } res = listen(sockfd, 5); if (res == -1) { return -1; } return sockfd; }

六、epoll内部

红黑树:管理所有被监听的 fd(epoll_ctl 的 O(log n) → 实际几乎 O(1))

rdlist(就绪链表):双向链表,存放已经就绪的 fd,epoll_wait 直接返回这个链表(O(1))

回调机制:当 fd 状态变化时,内核调用 ep_insert 把 fd 插入 rdlist(这就是为什么 ET 只需要通知一次)

七、总结

时间复杂度优秀

select/poll:每次调用都需要把整个关注的文件描述符集合(fd set)从用户态拷贝到内核态,并且内核需要线性扫描整个集合来找出哪些 fd 就绪,时间复杂度是 O(n)。

epoll:只需要在初次注册兴趣事件时(epoll_ctl)拷贝一次,之后每次 epoll_wait 调用时,内核直接返回已经就绪的事件列表,时间复杂度接近 O(1)(实际上是 O(就绪事件数量)),不需要再扫描全部 fd。

文件描述符数量限制极高

select:受限于 FD_SETSIZE(通常是 1024),并且 fd 必须是低位编号。

poll:理论上没有 fd 个数限制,但内部用数组线性存储,fd 越多内存和扫描开销越大。

epoll:使用红黑树 + 就绪链表(或回调)结构管理关注的 fd,单个 epoll 实例可以轻松支持数十万甚至上百万个连接(只受系统内存和 max user watches 限制)。

无需每次都重新传入感兴趣的 fd 集合

select/poll:每次调用都要把整个 fd 集合和感兴趣的事件重新传给内核(即使大部分都没变化)。

epoll:通过 epoll_ctl 一次性注册/修改/删除感兴趣的事件,后续 epoll_wait 只负责取结果,极大减少系统调用和数据拷贝开销。

支持两种高效触发模式

水平触发(LT):默认模式,只要 fd 可读/可写就会一直通知,和 poll 行为类似,适合大多数场景。

边缘触发(ET):只有状态发生变化时才通知一次,能极大减少事件通知次数(高并发服务器常用),配合非阻塞 I/O 可以达到最高性能。

事件就绪通知机制更高效(内核回调 + 链表)

当某个 fd 就绪时,内核会直接把它挂到 epoll 实例的就绪链表(rdlist)上,epoll_wait 只需要把这条链表 splice 到用户空间即可,几乎没有额外开销。

支持 EPOLLONESHOT 和线程安全

EPOLLONESHOT:可以让某个 fd 在被某个线程处理后自动禁用,避免多个线程同时处理同一个 socket 的“惊群”问题。

多个线程/进程可以安全地对同一个 epoll 实例调用 epoll_wait(底层用了 mutex + wait queue)。

内存拷贝开销极低

select/poll 每次都要把几千甚至几万个 fd 的 bitmap/数组拷贝来拷贝去,而 epoll 只需要在注册时拷贝一次 struct epoll_event,之后只拷贝真正就绪的那几个。

http://www.cnnetsun.cn/news/1932081.html

相关文章:

  • Jasminum插件:中文文献管理的终极解决方案
  • PyTorch 2.8镜像代码实例:基于Accelerate的跨GPU模型并行训练脚本
  • Step3-VL-10B开源大模型:模型权重分片加载与内存峰值控制
  • 幻境·流金多场景落地:游戏原画概念图快速迭代与美术风格统一控制
  • 去掉 UWB / 蓝牙标签:浙江普陀时空大数据应用技术联合研究院无感定位,免穿戴、免部署、降本、全场景覆盖
  • 2026年,还需要学Docker吗?容器运行时选型完全指南
  • 从三甲试点到基层覆盖,AI医疗咨询规模化落地的4个生死关卡,错过2026奇点大会将延迟部署周期11.8个月
  • 从HSPICE到Simscape:一个电路工程师如何用Simscape Language搞定大规模图像信号仿真
  • Java+YOLOv8+Redis实战:工业视觉检测缓存加速+实时数据同步,毫秒级生产级落地
  • USART 串口通信进阶指南:从寄存器配置到中断优化
  • AIoT产品的终极竞争:Jobs To Be Done 如何驱动从设备到服务的跃迁
  • Python3 数字(Number)
  • Linux系统与系统编程(5)——动静态库、自动化构建make/Makefile、回车与换行相关的行缓冲区
  • 【生成式AI成本控制黄金法则】:20年架构师亲授5大降本增效实战策略
  • 讯飞虚拟人简单交互实现
  • 避坑指南:在Unity中正确配置PICO 4 Ultra的外部文件读写权限(含AndroidManifest.xml模板)
  • LeetCode 232. Implement Queue using Stacks 题解
  • 三菱FX3U与多台力士乐VFC-x610变频器通讯实现案例详解:频率设定、启停及读取功能附程序...
  • 最小二乘法的5种变体:从加权到自适应,选对方法让模型精度翻倍
  • 科研党必备:scihub-cn命令行工具5分钟搞定文献批量下载(附常见报错解决方案)
  • 三大权威MEG公开数据集深度解析与应用指南
  • 从卫星原始数据到地表真实信息:一份给Python+GDAL用户的辐射校正全流程脚本指南
  • 缓存命中率暴跌?生成式AI应用缓存策略全解析,从Token级语义缓存到动态TTL自适应调优
  • 开关电源功率因数与效率测试实战指南
  • 爆肝整理,智能体发展万字综述!!!
  • c++学习笔记——堆
  • 没做这4类边界测试就部署AI服务?:金融/医疗/客服场景下 hallucination 爆发的3小时应急响应流程
  • AI服务注册延迟超800ms?2024年最严苛生产环境实测:3种服务发现方案TPS对比(含Latency P999数据)
  • 机器学习之预测概率问题
  • 朱雀AI检测率高怎么降?嘎嘎降AI使用教程:3步降到5%以下