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

Linux I/O多路复用:深入浅出poll与epoll

一、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数组的指针,每个元素描述一个待监视的文件描述符及其感兴趣的事件。

    • nfdsfds数组中的元素个数。

    • timeout:超时时间(毫秒)。

      • -1:阻塞直到有事件发生;

      • 0:立即返回,不阻塞;

      • >0:等待指定的毫秒数。

  • 返回值

    • 成功:返回就绪(有事件发生)的文件描述符个数。

    • 超时:返回0。

    • 出错:返回-1,并设置errno。

struct pollfd定义如下:

struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件掩码 */ short revents; /* 实际发生的事件掩码 */ };

eventsrevents是由以下标志按位或组成的位掩码:

  • 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:用户提供的数组,用于存放内核返回的就绪事件。

    • maxeventsevents数组的大小(即最多返回多少个事件)。

    • 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对比总结

特性selectpollepoll
底层数据结构位数组(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等封装库)。

http://www.jsqmd.com/news/449483/

相关文章:

  • StructBERT中文相似度模型保姆级教程:Sentence Transformers环境配置
  • 开发者一站式效率工具站,JSON 处理 + 开发调试全搞定
  • 性价比高的预制果茶包机构
  • 专业讲解:IRS2381C Real3™ 飞行时间图像传感器
  • 【Linux内核源码分析】进程管理
  • PyTorch 2.5镜像开箱实测:4.5GB磁盘空间够用吗?
  • 使用gte-base-zh进行文本数据清洗与去重:提升数据集质量
  • 提醒一下,金三银四前端面试别太老实…
  • 面试实录:互联网大厂Java岗位三轮技术问答及详细解析
  • 大模型学习笔记 self attention
  • 美国真的要崩了?别被情绪骗了!它的三张底牌,至今无人能破
  • 【计算机二级MSoffice题库软件】小黑课堂下载安装教程(2026年3月最新版)
  • 本科生收藏!千笔,最受欢迎的降AI率工具
  • 博途S7 - 1200采用MODBUS_TCP与第三方设备通讯教程
  • 被告警吵醒太多次,我做了个让告警自动修复的监控工具
  • STL容器——std::vector
  • 智慧物流已成标配:2026年主流AMR搬运机器人厂家市场竞争力与行业格局全景解析 - 品牌推荐
  • 告别繁琐查询:一键整合企业工商、司法、经营数据的API方案
  • 2026全国靠谱运输车厂家挑选攻略,速来了解,自卸履带运输车/矿山履带运输车/高速除雪设备,运输车厂家直供排名 - 品牌推荐师
  • OpenClaw 安装避坑指南:工具权限配置详解
  • $emit自定义组件发数据本组件
  • 选一种颜色,出门走走
  • DRAM内存访问协议核心解析:全场景命令时序约束汇总表(内存控制器设计核心参考)
  • 英飞凌 IRS2381C Real3™ 飞行时间(ToF)图像传感器
  • 正面交锋:Gemini 3.1 Pro与GPT-5.4的技术分野与选择逻辑
  • 从加载状态看提示界面设计:提升等待体验
  • 计算机毕业设计java基于Java的自动化网站设计与实现 基于B/S架构的教学自动化管理平台设计与实现 面向师生互动的作业提交与课程测评系统开发
  • 程序化树木生成器(ThreeJS EZ-Tree 开源项目)
  • 同样画CAD,别人2小时搞定,你却卡半天?问题出在这3处
  • 全国可实时在线监控的压力变送器品牌有哪些推荐 - 工业品网