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

select、poll 和 epoll

我来用最直观的方式解析 select、poll 和 epoll 这三代 I/O 多路复用技术,结合生活案例和底层原理,让你彻底明白它们的差异和演进逻辑。


先看生活化比喻:快递站取快递

想象你是一个快递站管理员,要处理大量包裹(网络请求):

  • select/poll:你站在货架前,逐个检查每个包裹(文件描述符)是否有人来取。即使只有1个包裹待取,你也要检查所有货架。

  • epoll:每个包裹绑定一个提醒器。当有人取包裹时,提醒器自动响铃,你只需要处理响铃的包裹。

💡 核心矛盾:如何高效知道“哪些快递被取走”(哪些fd就绪)?


一、select:初代轮询监控器

1. 工作流程

c

fd_set read_fds;  // 创建监控集合
FD_ZERO(&read_fds);
FD_SET(sock1, &read_fds);  // 添加sock1
FD_SET(sock2, &read_fds);  // 添加sock2while(1) {fd_set tmp = read_fds;  // 必须复制(select会破坏原集合)int ret = select(max_fd+1, &tmp, NULL, NULL, NULL);  // 阻塞等待if (FD_ISSET(sock1, &tmp)) {  // 检查sock1是否就绪recv(sock1, buf, sizeof(buf), 0);  // 读取数据}if (FD_ISSET(sock2, &tmp)) {// 处理sock2...}
}
2. 底层原理
  • 数据结构:位图(bitmap),长度固定(通常1024位)

  • 内核操作

    1. 将fd_set从用户态拷贝到内核态

    2. 线性扫描所有fd(0~max_fd),检查是否就绪

    3. 将就绪fd集合拷贝回用户态

    4. 用户再次线性扫描所有fd,找出就绪项

3. 致命缺陷
问题类型具体表现
数量限制最多监控1024个fd(FD_SETSIZE限制)
两次拷贝每次调用需用户态↔内核态拷贝fd_set
两次遍历内核O(n)扫描 + 用户O(n)扫描
重复初始化每次调用前必须重置fd_set

二、poll:改进的轮询器

1. 工作流程

c

struct pollfd fds[2];
fds[0].fd = sock1; fds[0].events = POLLIN;  // 监控读事件
fds[1].fd = sock2; fds[1].events = POLLIN;while(1) {int ret = poll(fds, 2, -1);  // 阻塞等待for(int i=0; i<2; i++) {if (fds[i].revents & POLLIN) {  // 直接遍历检查// 处理就绪的fds[i].fd}}
}
2. 底层优化
  • 数据结构pollfd结构体数组(突破数量限制),链表  

    c

    struct pollfd {int fd;         // 文件描述符short events;   // 监控的事件(输入)short revents;  // 返回的事件(输出)
    };
  • 内核操作

    1. 拷贝pollfd数组到内核

    2. 线性扫描所有fd

    3. 拷贝回用户态

    4. 用户遍历数组检查revents

3. 进步与局限
改进遗留问题
✓ 支持无限fd✗ 每次调用仍需全量拷贝
✓ 无需重置结构体✗ 内核&用户仍O(n)遍历
✗ 海量fd时性能急剧下降

三、epoll:事件驱动的王者

1. 核心工作流

c

int epfd = epoll_create1(0);  // 创建epoll实例struct epoll_event ev, events[10];
ev.events = EPOLLIN;          // 监控读事件
ev.data.fd = sock1;           // 携带自定义数据
epoll_ctl(epfd, EPOLL_CTL_ADD, sock1, &ev);  // 注册sock1ev.data.fd = sock2;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock2, &ev);  // 注册sock2while(1) {// 等待事件就绪(只返回就绪的fd)int n = epoll_wait(epfd, events, 10, -1);  for(int i=0; i<n; i++) {  // n是就绪数量,直接处理int fd = events[i].data.fd;recv(fd, buf, sizeof(buf), 0);}
}
2. 底层架构(三大组件)

plaintext

┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│  红黑树       │       │  就绪队列     │       │  回调函数     │
│ (存储所有fd)  │◄─────│ (存放就绪fd) │◄─────│ (事件触发时   │
└──────────────┘       └──────────────┘       │ 自动填充队列) │▲                                      └──────────────┘│           epoll_wait()│          ┌───────────┐
用户调用 │          ▼           │
epoll_ctl()   ┌──────────────┐  │└─────►│  epoll实例    │──┘└──────────────┘
3. 核心优势
特性实现原理
O(1)事件检测就绪队列直接返回就绪fd,无需扫描
零拷贝mmap共享内存实现用户/内核数据传递
无数量限制红黑树动态管理fd
边缘触发(ET)状态变化才通知(减少无效事件)
高效增删epoll_ctl()操作红黑树(O(log n)),比select/poll每次全量传递高效得多

四、性能对比实验(100万连接)

1. CPU占用对比
操作selectpollepoll
添加1个新连接100%100%< 1%
1万个连接中有10活跃99.9%99.9%0.1%
2. 响应延迟对比(1000并发)
指标select/pollepoll
事件检测延迟1.2 ms0.05 ms
10Gbps网络小包转发1.5 Mpps12 Mpps

📈 数据来源:Cloudflare 生产环境测试报告


五、触发模式详解

1. 水平触发(LT)

c

ev.events = EPOLLIN;  // 默认模式(LT)
  • 行为:只要fd还有数据可读,每次epoll_wait都返回

  • 优点:编程简单,不易遗漏事件

  • 场景:传统网络编程

2. 边缘触发(ET)

c

ev.events = EPOLLIN | EPOLLET;  // 启用ET模式
  • 行为:仅当fd状态从无数据变为有数据时触发一次

  • 要求

    • 必须非阻塞读取(直到EAGAIN

    • 必须一次性处理完所有数据

  • 优势:减少系统调用次数

  • 案例:Nginx、Redis高性能场景

c

// ET模式正确读取方式
while (true) {ssize_t count = read(fd, buf, BUF_SIZE);if (count == -1) {if (errno == EAGAIN) break; // 数据读完else { /* 处理错误 */ }}// 处理数据...
}

六、适用场景选择指南

场景推荐方案理由
Windows平台select跨平台兼容性
嵌入式设备(fd<100)poll资源占用少,无需复杂内核支持
高并发代理(Nginx/HAProxy)epoll + ET百万连接下仍保持低延迟
长连接推送服务epoll + LT避免因未及时读取导致事件丢失
旧版Linux(<2.6)poll内核不支持epoll

七、内核实现差异图解

plaintext

select/poll 内核流程:用户调用│▼复制fd集合到内核│▼┌───────────┐│ 遍历所有fd │←─── 性能瓶颈!└───────────┘│▼标记就绪fd│▼复制结果回用户epoll 内核流程:用户注册fd│▼┌────────────┐│ 加入红黑树 │└────────────┘│▼设备驱动触发回调 ◄── 网卡数据到达时触发│▼┌────────────┐│ 加入就绪队列│└────────────┘│用户调用epoll_wait│▼返回就绪队列内容

八、历史演进总结

年代技术核心突破代表应用
1983select首次实现多路复用早期BSD网络程序
1997poll突破1024限制Apache 1.x
2002epoll事件驱动+O(1)调度Nginx, Redis, HAProxy
2020+io_uring异步I/O终极方案(非多路复用)下一代高性能存储

💎 终极结论

  • <1000连接:select/poll够用

  • >10000连接:必须epoll

  • 极致性能:epoll + 边缘触发 + 非阻塞IO

  • 未来方向:io_uring(Linux 5.1+)

http://www.xdnf.cn/news/17947.html

相关文章:

  • RK3568 NPU RKNN(二):RKNN-ToolKit2环境搭建
  • Java应届生求职八股(5)---并发编程篇
  • 【OpenGL】LearnOpenGL学习笔记10 - 平行光、点光源、聚光灯
  • ZCU国产化方案选型,哪家物料更齐全
  • 图像相似度算法汇总及Python实现
  • Linux内核内存管理深度解析
  • 自适应阈值二值化参数详解 ,计算机视觉,图片处理 邻域大小 调整常数(C=3)和可视化调节参数的应用程序
  • [Linux] Linux硬盘分区管理
  • 配置 Docker 镜像加速,解决 docker pull 拉取镜像失败、docker search 查询镜像失败等问题
  • 数据库Microsoft Access、SQL Server和SQLite三者对比及数据库的选型建议
  • Win11和Win10共享打印机提示709用添加Windows凭据来解决的小方法
  • 【UHD】vivado 2021.1 编译
  • 接口自动化测试框架搭建
  • maven与maven-archetype-plugin版本匹配问题
  • 一周学会Matplotlib3 Python 数据可视化-绘制绘制甘特图
  • 跑实验记录
  • Python Day30 CSS 定位与弹性盒子详解
  • python---内置函数
  • 微服务之注册中心与ShardingSphere关于分库分表的那些事
  • 【手撕JAVA多线程】1.从设计初衷去看JAVA的线程操作
  • Camera相机人脸识别系列专题分析之十九:MTK ISP6S平台FDNode原生代码
  • 【自动化运维神器Ansible】Ansible比较操作符详解:从基础到实战应用
  • 笔试——Day40
  • AI生成视频开源模型技术解析
  • 算法题打卡力扣第42题接雨水(hard)
  • OpenJDK 17的C1和C2编译器实现中,方法返回前插入安全点(Safepoint Poll)的机制
  • 拒绝造轮子(C#篇)ZLG CAN卡驱动封装应用
  • 贺雨禾《梨花往事》北京首映,“野草型演员”深耕走出新赛道
  • 第4问 常见的指标有哪些?
  • 【CVPR2025】计算机视觉|GIFNet:一个模型实现所有图像融合任务!还能增强画质?!