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

Linux下的I/O复用技术之epoll

I/O多路复用

指在单个线程或进程中,同时处理多个I/O操作的技术。

旨在提高程序处理多个并发I/O操作的能力,避免程序因等待某个I/O操作而被阻塞。在传统的I/O模型中当程序进行I/O操作时(如读取文件、接受网路数据等),如果数据还未准备好,程序会被阻塞,直到I/O操作完成,这会导致效率低下,尤其是在需要处理大量并发连接的网络应用中。I/O复用技术的核心理念是允许一个进程或线程同时处理多个I/O操作,而不是等待某一个操作完成后再去处理其他任务。通过则何种方式,程序能够在多个I/O之间切换,充分利用系统资源,避免每个I/O事件都创建一个新的线程或进程,从而大大提高效率。在linux中相关技术有select()、poll()、epoll(),程序会通过上述机制同时监控多个I/O事件,并在其中某个文件描述符(或I/O操作)就绪时,进行相应的处理。这样即使有多个I/O操作正在进行,程序也可以及时响应,并继续进行其他任务,从而达到“非阻塞”的效果。

epoll涉及到的系统调用函数

epoll_create()

用于创建一个epoll示例,返回一个文件描述符,程序通过这个文件描述符与epoll实例进行交互。他为事件提供一个内核空间的数据结构,并为之后的epoll_ctl()和epoll_wait()调用提供一个有效的上下文。

int epoll_create(int size);

size:指定内核事件表的初始大小。这个参数在现代linux系统中已没有什么作用,通常   设置为1即可,因为内核会动态调整。

返回值:创建成功返回一个非负值,表示epoll实例的文件描述符,创建失败返回-1,      并设置errno为相应的错误代码。

int epoll_create1(int flags);

flags:创建时指定的额外选项

传入0代表不指定额外选项

EPOLL_CTL_ADD:添加新的文件描述符及其事件EPOLL_CTL_MOD:修改已经注册的文件描述符的事件EPOLL_CTL_DEL:删除文件描述符的注册

传入EPOLL_CLOEXEC:设置文件描述符为执行时关闭,以确保在exec系列函数调用后自动关闭该文件的文件描述符。

epoll_ctl()

用于控制epoll实例中的事件,包括向epoll注册、修改或删除文件                          描述符的I/O事件。

int epoll_ctl(int epfd, int op,int fd, struct epoll_event* event);

epfd:由epoll_create()返回的epoll文件描述符,用于标识epoll实例。

op:操作类型,指示所执行的操作。

EPOLL_CTL_ADD:添加新的文件描述符及其事件EPOLL_CTL_MOD:修改已经注册的文件描述符的事件EPOLL_CTL_DEL:删除文件描述符的注册

fd:需要注册、修改或删除的文件描述符

event:struct epoll_event类型的指针,表示与文件描述符关联的事件类型,这个结构体参数的作用不仅仅是保存文件描述符,他还包含了与该文件描述符关联的事件类型、以及其他用于标识和处理时间的数据

返回值:成功返回0,失败返回-1,并设置errno为相应的错误代码。

..................................................................................................................................................

补:epoll_event数据结构

该结构体定义了与文件描述符关联的事件类型及其他数据

struct epoll_event {uint32_t  events;  //事件类型epoll_data_t data;  //与文件描述符关联的数据,通常为文件描述符或指针
};

其中:

events:表示该文件描述符的事件类型,注意这些事件类型是可以a|b的混合注册的

data:是一个联合体epoll_data_t,用于存储与文件描述符相关的自定义数据,通常用于存储文件描述符本身或替他数据

epoll_data_t可以是:int fd(文件描述符);  void* ptr(指向用户数据的指针)等

..................................................................................................................................................

示例:

struct epoll_event ev;
ev.event = EPOLLIN;  //设置为读取事件
ev.data.fd = server_fd;  //设置文件描述符
int res = epoll_create(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);

epoll_wait()

用于等待已注册的文件描述符上的事件发生。该函数会阻塞直到至少一个事件发生或者超时。等待并返回已发生的事件。

int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout);

epfd:由epoll_create返回的epoll文件描述符

events:指向epoll_event结构体数组的指针,用于接收已发生的事件。

event是一个数组,用于存储epoll_event类型的结构体,即用于接收发生的事件event。这里的事件是指,在epoll_ctl阶段注册到epoll中的事件event,并且他得已经发生,已经发生的意思是指已注册的文件描述符的状态满足了你注册的事件条件(如:在epoll_ctl阶段为某个文件描述符fd注册了EPOLLIN,当fd上有数据可读时,例如socket接收到网络数据了,那么这个fd上就有数据可以读取了,这个事件就被认为已经发///或者为某个文件描述符注册的EPOLLOUT,当fd可以用来写数据时,比如socket的发送缓冲区有空闲空间时,那么这个fd就可以进行写,这个事件被认为已经发生了)。

epoll_wait()的作用可以这样理解,它的作用就是监听在epoll_ctl注册到epfd中的事件集合,然后将发生的事件传入events数组中,通过遍历这个events可以得到event,通过event的fd和events成员可以得知该fd可以进行events对应操作了(比如:struct epoll_event ev;  ev.fd = fd1;  ev.events = EPOLLOUT,那么我们可以对fd1进行写操作了,write(fd, ....))。

maxevents:指定events数组的大小,即一次最多返回的事件数

timeout:指定等待的超时时间(单位:毫秒)。设置为-1时表示无限等待,设置为0表示                非阻塞,其他正值表示最大等待时间。

返回值:成功则返回已就绪的事件数,即发生的事件数量,失败则返回-1,并设置errno          为相应的错误代码。

示例:

struct epoll_event events[10];
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; ++i) {if (events[i].events & EPOLLIN) {// 处理可读事件}if (events[i].events & EPOLLOUT) {// 处理可写事件}
}

 综合使用简单示例:

// 创建一个 epoll 实例,返回 epoll 文件描述符
int epfd = epoll_create(1);// 定义一个 epoll_event 结构体变量,用于描述要监听的事件
struct epoll_event ev;// 设置事件类型为 EPOLLOUT,表示监听 "可写" 事件
ev.events = EPOLLOUT;// 绑定要监听的文件描述符(socketfd)到 ev 的 data.fd 字段
ev.data.fd = socketfd;// 将指定的文件描述符(socketfd)注册到 epoll 实例 epfd 中,监听可写事件
epoll_ctl(epfd, EPOLL_CTL_ADD, socketfd, &ev);// 定义一个数组,用来接收 epoll_wait 返回的就绪事件
struct epoll_event events[10];// 调用 epoll_wait,阻塞等待内核检测 epfd 中注册的事件
// -1 表示永远等待,直到有事件发生
int nfds = epoll_wait(epfd, events, 10, -1);// 遍历所有返回的就绪事件
for (int i = 0; i < nfds; ++i) {// 检查当前事件是否包含 EPOLLOUT,可写事件if (events[i].events & EPOLLOUT) {// 取出就绪的文件描述符int fd = events[i].data.fd;// 定义要发送的消息内容const char* message = "hello";// 使用 write 将消息写入到对应的文件描述符ssize_t bytes_written = write(fd, message, strlen(message));// 这里没有做错误检查,实际项目中最好检查 bytes_written 是否出错}
}
// 创建 epoll 实例,返回 epoll 文件描述符
int epfd = epoll_create1(0);// 注意:这里通过一个 open 打开了一个文件,得到一个文件描述符,但它不一定满足下面的 EPOLLIN 事件,
// 只有当 example 文件中有数据时,它才可以被读,才满足该 fd 监听的 / 感兴趣的事件 EPOLLIN,
// 它才可以在 epoll_wait 的时候被添加到 events 中。
int fd = open("example.txt", O_RDONLY);  // 注意:这里应该是 O_RDONLY,不是 0_RDONLY// 定义一个 epoll_event 结构体变量
struct epoll_event ev;// 设置监听的事件类型为 EPOLLIN(可读事件)
ev.events = EPOLLIN;// 将文件描述符 fd 保存到 ev.data.fd 中
ev.data.fd = fd;// 调用 epoll_ctl,将 fd 注册到 epoll 实例 epfd 中,关注可读事件
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);// 定义一个数组,用来存放 epoll_wait 返回的就绪事件
struct epoll_event events[10];// 调用 epoll_wait,阻塞等待 epfd 中注册的文件描述符上有事件发生
int nfds = epoll_wait(epfd, events, 10, -1);// 遍历所有返回的就绪事件
for (int i = 0; i < nfds; ++i) {// 如果当前事件是 EPOLLIN(可读事件)if (events[i].events & EPOLLIN) {// 在这里处理对应的可读文件描述符}
}

epoll底层实现原理

核心组件

①红黑树

epoll内部维护了一颗红黑树,通过红黑树来管理(增加、删除、修改)所有被监控的文件描述符;当调用epoll_ctl增加、删除或修改监控事件时,会在红黑树中插入、移除或更新相应的节点;红黑树的高效查找和插入特性(时间复杂度为O(Logn))使得epoll在管理大量文件描述符时性能优越。

②就绪链表(双向链表)

epoll使用一个就绪链表来保存当前已经触发事件的文件描述符;当某个文件描述符变为就绪状态时(例如数据可读或可写),其对应的事件会被添加到就绪链表中;这使得epoll_wait调用只需直接扫描这个链表,从而避免了向poll或select那样逐个遍历所有的文件描述符。

与内核的交互

epoll是依赖内核中的事件通知机制来工作的,通常通过文件系统(如proc文件系统等)监控文件描述符的状态;每个文件描述符在内核中都有一个对应的事件回调函数,当事件发生时(即通过epoll_ctl添加到红黑树中的event对应的fd满足注册的事件状态时),会触发这个回调函数,将事件添加到就绪链表中,当调用epoll_wait时,内核将扫描就绪链表,并返回链表中的数据。

触发机制

在epoll中,触发机制决定了epoll_wait如何返回文件描述符的事件。这直接影响事件的通知方式和应用程序对文件描述符的处理策略,epoll支持两种触发机制:水平触发和边缘触发。

①水平触发(level triggered

这是epoll默认的触发方式。文件描述符只要处于就绪状态(可读或可写),epoll_wait就会一直返回该事件(调用该方法返回的int值大小中有它一席,并且传入给epoll_wait的events数组也会一直存入这个事件event);无论文件描述符的状态是否变化,只要其仍然满足条件(如缓冲区有数据可读),事件都会重复触发,直到应用程序对其处理完成。

优点:使用简单,适合大多数场景。不容易遗漏事件,即使处理稍有延迟,也可以通过多次调用读取剩余数据。

缺点:对于大量文件描述符,就绪事件可能被重新触发,导致处理效率较低。

struct epoll_event ev;
ev.events = EPOLLIN;  //水平触发是默认的

②边沿触发(edge triggered

事件只会在状态变化时触发(如从不可读变为可读,或从不可写变成可写);如果应用程序没有在事件触发时处理完数据,则不会再次触发,可能导致数据遗漏。

优点:减少了重复通知,提高了系统效率,适合大规模并发场景,支持高性能的非阻塞模式。

缺点:复杂性高,必须一次性读取或写入尽可能多的数据,否则可能会遗漏数据。

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;	//设置为边沿触发

epoll工作流程

首先,用户通过调用epoll_create创建一个epoll实例,用于管理所有需要监听的文件描述符(每个epoll实例中都有一个独立的evetnepoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加的事件)。接着,使用epoll_ctl向epoll内部的红黑树中添加、修改或删除需要监听的文件描述符及事件;最后,通过调用epoll_wait等待事件发生,内核会将已经触发的事件加入就绪链表,并将链表中的就绪文件描述符返回给用户程序,从而实现高效的事件驱动模型。

相较于selectpollepoll为什么高效?

以下几个差异导致epoll更高效

1.事件监听和管理方式的不同

select、poll:每次调用select、poll,都需要将所有文件描述符表传递给内核,内   核会对这些文件描述符逐一查其状态,这种查找是线性查找,时间复杂度是O(n), 导致性能开销大,尤其是在文件描述符数量较多时。

epoll使用一个红黑树来存储用户注册的文件描述符事件,文件描述符只需通过   epoll_ctl注册一次;每次epoll_wait时,内核只需检查红黑树上的事件,并通过 一个就绪链表直接返回有事件发生的文件描述符;事件分发是基于回调的机制,无 需线性扫描。

2.数据拷贝的效率

select、poll:每次调用select、poll都需要将文件描述符列表从用户态拷贝到内核 态,再从内核态返回结果到用户态,如果文件描述符很多,这个过程会占用大量的 cpu和内存带宽。

epoll采用共享内存机制,文件描述符只在epoll_ctl注册时传递给内核;内核和用 户空间之间通过共享的就绪链表来传递数据,避免每次调用时的大量拷贝。

3.支持更大的文件描述符集合

select文件描述符数量受到系统常量FD_SETSIZE的限制(通常是1024个)。超过限 制后无法使用。

poll支持更多的文件描述符,但依然需要遍历整个文件描述符列表

epoll支持的文件描述符数量只受限于系统的最大文件描述符数量,理论上可以达到   数十万甚至更多。即使文件描述符数量庞大,只关注有事件发生的文件描述符,效 率依然很高。

4.触发机制的差异

select、poll:只支持水平触发,即只要文件描述符的状态满足条件,每次都会返回, 可能会导致重复处理。

epoll支持水平触发和边缘触发,边缘触发模式下,只有当文件描述符的状态从未满   足到满足时,才会触发事件,进一步减少系统调用的次数,提高性能。

5.线程安全性

epoll是线程安全的(epoll_wait内存实现对共享的就绪事件列表有锁机制保活,确 保线程安全),多个线程可以同时调用epoll_wait,充分利用多核CPU提高并   发  能力。

select、poll:通常需要额外的同步机制来确保多线程访问同一个文件描述符集合时的 线程安全性。

相关文章:

  • 模型 隐含前提
  • MyBatis缓存配置的完整示例,包含一级缓存、二级缓存、自定义缓存策略等核心场景,并附详细注释和总结表格
  • Python部署Docker报错:curl: (56) Recv failure: Connection reset by peer
  • 强化学习:高级策略梯度理论与优化方法
  • leetcode110 平衡二叉树
  • 在QML中获取当前时间、IP和位置(基于网络请求)
  • Simple-BEV论文解析
  • module.noParse(跳过指定文件的依赖解析)
  • [贪心_8] 跳跃游戏 | 单调递增的数字 | 坏了的计算器
  • GitOps进化:深入探讨 Argo CD 及其对持续部署的影响
  • 青少年编程与数学 02-018 C++数据结构与算法 12课题、递归
  • 多模态大语言模型arxiv论文略读(四十二)
  • Dify框架面试内容整理-Dify如何实现模型调用与管理?
  • 【OSG学习笔记】Day 10: 字体与文字渲染(osgText)
  • 两台没有网络的电脑如何通过网线共享传输文件
  • Compose笔记(十八)--rememberLazyListState
  • 【第11节 嵌入式软件的组成】
  • 从后端研发角度出发,使用k8s部署业务系统
  • ARP协议【复习篇】
  • Tortoise-ORM级联查询与预加载性能优化
  • 印度媒体称印巴在克什米尔再次交火
  • 偷拍拷贝某轨道车技术信息后撰写论文发表,工程师被判一年有期徒刑
  • 巴黎奥运后红土首秀落败,郑钦文止步马德里站次轮
  • 鞍钢矿业党委书记、董事长刘炳宇调任中铝集团副总经理
  • 铁线礁、牛轭礁珊瑚礁“体检”报告首次发布,专家:菲非法活动产生胁迫性影响
  • 马上评|起名“朱雀玄武敕令”?姓名权别滥用