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

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.lryc.cn/news/623210.html

相关文章:

  • InfluxDB 数据迁移工具:跨数据库同步方案(二)
  • 【大模型核心技术】Dify 入门教程
  • 制作 Windows 11 启动U盘
  • Linux-Vim编辑器最简美化配置
  • 全排列问题回溯解法
  • Linux软件编程(六)(exec 函数族、system 实现、进程回收与线程通信)
  • 基于动捕实现Epuck2的轨迹跟踪
  • 数据结构:迭代方法(Iteration)实现树的遍历
  • 记录一下第一次patch kernel的经历
  • 【UHD】vivado 2021.1 编译
  • 解决 Microsoft Edge 显示“由你的组织管理”问题
  • c#Blazor WebAssembly在网页中多线程计算1000万次求余
  • Spring Framework:Java 开发的基石与 Spring 生态的起点
  • Agent中的memory
  • 西湖大学新国立,多模态大语言模型能指引我回家吗?ReasonMap:基于交通地图的细粒度视觉推理基准研究
  • imx6ull-驱动开发篇27——Linux阻塞和非阻塞 IO(上)
  • pdf合并代码
  • 杂记 03
  • 链表。。。
  • 全面解析Tomcat生命周期原理及其关键实现细节
  • 【论文笔记】STORYWRITER: A Multi-Agent Framework for Long Story Generation
  • 云原生俱乐部-RH124知识点总结(3)
  • 如何解决C盘存储空间被占的问题,请看本文
  • 异构数据库兼容力测评:KingbaseES 与 MySQL 的语法・功能・性能全场景验证解析
  • 后量子密码算法SLH-DSA介绍及开源代码实现
  • huggingface TRL中的对齐算法: KTO
  • 嵌入式硬件篇---BuckBoost电路
  • GPIO初始化及调用
  • AI杀死的第一个仪式:“hello world”
  • CentOS 7 一键部署 上Maria Database(MariaDB)10.3.38 安装手册(避开 Oracle 19c 路径)