IO复用之——epoll

epoll是Linux下IO复用的一种高效实现,相比select和poll,epoll提供了epoll_create、epoll_ctl和epoll_wait三个函数,用于创建epoll实例、管理事件和等待事件就绪。epoll_wait返回就绪事件的数量,避免了遍历整个事件集的开销。此外,epoll支持边缘触发(ET)模式,提高效率,但也需要配合非阻塞IO使用以防止数据拥堵。文章通过一个TCP服务端栗子,详细介绍了epoll的工作流程和LT、ET模式的区别。

一. 关于epoll

    对于IO复用模型,前面谈论过了关于select和poll函数的使用,select提供给用户一个关于存储事件的数据结构fd_set来统一监测等待事件的就绪,分为读、写和异常事件集;而poll则是用一个个的pollfd类型的结构体管理事件的文件描述符和事件所关心的events,并通过结构体里面的输出型参数revents来通知用户事件的就绪状态;

    但是对于上述两种函数,都是需要用户遍历所有的事件集合来确定到底是哪一个或者是哪些事件已经就绪可以进行数据的处理了,因此当要处理等待的事件比较多时,就会有数据复制和系统遍历的开销导致效率并不高效;针对select和poll的缺点,另外一种相对高效的处理IO复用的函数就出现了,那就是epoll;



二. epoll相关函数的使用

    首先,和select及poll函数不同的是,epoll并没有直接的一个用epoll来命名的函数使用,而是分别提供出来三个函数:epoll_createepoll_ctlepoll_wait


  1. epoll_create

wKioL1dKc0OjGQjvAAAH_Pjisxk527.png

epoll_create函数创建一个epoll的“实例”,请求内核分配一个指定大小的空间用于事件的后台存储,函数参数size只是一个关于内核如何维护内部结构的提示,不过现在这个size已经被忽略并不需要在意了;

函数成功会返回一个引用新创建的epoll实例的一个文件描述符,用于随后调用其他的epoll函数的结构,如果不再需要的话,应当使用close函数关闭,这时内核会销毁该epoll实例并释放相关资源;如果函数失败会返回-1并置相应的错误码


2. epoll_ctl

wKioL1dKdxDxZc4NAAAIeYVCTEw897.png

函数参数中,

epfd是用epoll_create创建出来的epoll文件描述符,用来操纵epoll实例;

op是要对创建出的epoll实例进行操作,而op的操作选项有如下三种宏:

wKioL1dKdxDg7taeAAAcw24qBpg026.png

EPOLL_CTL_ADD用于在epfd标识的epoll实例中添加登记要处理的事件;

EPOLL_CTL_MOD用于更改特定的文件描述符所关心的事件;

EPOLL_CTL_DEL用于删除在epoll实例中登记的事件,标识并不需要再关心了;


fd是指要进行数据IO的事件的文件描述符,也就是用户需要进行操作的事件的文件描述符;

event是一个epoll_event的结构体,用于存放需要对fd进行操作的相关信息:

wKiom1dKdheg3AfeAAAG1FYwPjc281.png

结构体中,

events表示文件描述符fd所对应的事件所关心的操作,是相应的比特位的设置,有如下几种宏:

wKiom1dKdhjRqFC_AABc0X0ReFY488.png

如上的宏中,最主要使用的有如下几种:

EPOLLIN表示fd可以进行数据的读取;

EPOLLOUT表示fd可以进行数据的写入;

EPOLLPRI表示当前有紧急数据可供读取;

EPOLLERR表示当前事件发生错误;

EPOLLHUP表示当前事件被挂断;

EPOLLET将相关的文件描述符设置为边缘触发,因为默认是水平触发的;对于LT和ET模式下面会讨论;


对于结构体中的data则是一个联合,用于表示有关文件描述符操作的数据信息:

wKioL1dKdxKTCVYrAAAHRLOQKFY569.png

ptr是指向数据缓冲区的一个指针;

fd是相应操作的文件描述符;


epoll_ctl函数成功返回0,失败返回-1并置相应的错误码;


3. epoll_wait

如果说上面的epoll_create和epoll_ctl是为了进行相关事件的操作而进行的准备工作,那么真正和select及poll函数一样用来进行多个事件的等待就绪则就是epoll_wait函数了:

wKioL1dKjOqSfqHJAAAM_vX-ny4647.png

函数参数中,

epfd是用epoll_create创建出的epoll实例的文件描述符;

events是上述的一个结构体的指针,这里一般是一个数组的首地址,是一个输入输出型参数,当作为输入时,是用户提供给系统一个用来存放就绪事件的地址空间,而作为输出型参数时,系统会将就绪的事件放入其中供用户提取,因此不可以为NULL

maxevents是events的大小;

timeout则是设置等待的超时时间,单位为毫秒


这里值得一提的是,既然epoll是select和poll的改进,那么其最主要的高效就是体现在epoll_wait的返回值:

  • 函数失败返回-1并置相应的错误码;

  • 函数返回0表示超时,预定时间内并没有事件就绪;

  • 当函数返回值大于0时,是告诉用户当前事件集中已经就绪的IO事件的个数,并且将其按序从头开始排列在了用户提供的空间events内,因此,不需要像select和poll那样遍历整个事件集找出就绪的事件,只需要在相应的数组中从头访问固定的返回值的个数就拿到了所有就绪的事件了;



三. 栗子时间

    同样的,使用epoll相关的接口函数,可以自主来编写一个基于TCP协议的服务端,其基本步骤如下:

  1. 首先,先要创建出一个监听socket,绑定好本地网络地址信息并将其处于监听状态,但是这里,为了使其更为高效,还需要调用setsockopt函数来将其属性设定为SO_REUSEADDR,使其地址信息可被重用;

  2. 调用epoll_create创建出一个关于epoll实例的文件描述符,用于以后操作epoll相关函数;

  3. 调用epoll_ctl函数,将监听socket登记添加到epoll实例中;

  4. 定义一个epoll_event结构体数组,用户指定大小,供系统存放就绪的IO事件;

  5. 调用epoll_wait进行事件的就绪等待,并接收其返回值;

  6. 当epoll_wait返回时,对返回的事件一一进行判断处理,如果是监听事件就绪,表明有连接请求需要处理,并将新的套接字添加进epoll实例中;如果是其他socket就绪,表明数据就绪可以进行读取和写入了;

  7. 当连接的一端关闭或者epoll实例使用完毕的时候,需要调用close函数关闭相应的文件描述符回收资源;


server客户端程序设计如下:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <sys/epoll.h>#include <fcntl.h>#include <assert.h>#include <errno.h>#define _BACKLOG_ 5  //网络中连接请求等待队列最大值#define _MAX_NUM_ 20 //事件就绪队列存储空间#define _DATA_SIZE_ 1024 //数据缓冲区大小//因为epoll_event结构体中的data成员是一个联合体,因此当需要同时使用联合中的fd和ptr的时候就会有问题//因此可以将其各自单独拿出存储typedef struct data_buf{    int _fd;    char _buf[_DATA_SIZE_];}data_buf_t, *data_buf_p;//命令行参数的格式判断void Usage(const char *argv){    assert(argv);    printf("Usage: %s  [ip]  [port]\n", argv);    exit(0);}//创建监听套接字static int CreateListenSock(int ip, int port){    int sock = socket(AF_INET, SOCK_STREAM, 0);//创建新socket    if(sock < 0)    {        perror("socket");        exit(1);    }    int opt = 1;//调用setsockopt函数使当server首先断开连接的时候避免进入一个TIME_WAIT的等待时间    if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)    {        perror("setsockopt");        exit(2);    }        //设置本地网络地址信息    struct sockaddr_in server;    server.sin_family = AF_INET;    server.sin_port = htons(port);    server.sin_addr.s_addr = ip;    //绑定套接字和本地网络信息    if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)    {        perror("bind");        exit(3);    }    //设定套接字为监听状态    if(listen(sock, _BACKLOG_) < 0)    {        perror("listen");        exit(4);    }    return sock;}//执行epollvoid epoll_server(int listen_sock){        //创建出一个epoll实例,获取其文件描述符,大小随意指定    int epoll_fd = epoll_create(256);   &n
### Linux I/O 复用机制:`select`、`poll` 和 `epoll` 的区别比较 #### 一、基本概念 I/O 多路复用是一种允许单个线程同时监视多个文件描述符的技术,以便在任意一个或多个文件描述符变为可读/写时得到通知。Linux 提供了三种主要的 I/O 多路复用机制:`select`、`poll` 和 `epoll`。 --- #### 二、具体特性对比 1. **`select`** - **特点**: - 支持的最大文件描述符数受限于系统常量(通常为 1024)。可以通过重新编译内核调整上限[^3]。 - 每次调用都需要将所有的文件描述符集合从用户空间拷贝到内核空间,并在线性扫描这些文件描述符以判断其是否准备好[^3]。 - 时间复杂度为 O(n),其中 n 表示要监控的文件描述符总数。 - **性能**: - 在少量文件描述符的情况下表现尚可,但在高并发场景下由于需要频繁地复制和扫描大量文件描述符,性能下降明显[^3]。 - **典型应用场景**: - 小规模网络应用或者嵌入式设备上的简单通信程序[^3]。 2. **`poll`** - **特点**: - 解决了 `select` 中最大文件描述符数量限制的问题,理论上可以支持无限多的文件描述符。 - 类似于 `select`,仍然存在每次调用都要将所有感兴趣的文件描述符列表传送到内核的空间开销问题[^3]。 - 同样采用轮询方式检测哪些文件描述符已准备就绪,时间复杂度仍为 O(n)[^3]。 - **性能**: - 相较于 `select` 略有改进,但由于底层实现原理相似,在大规模并发情况下依然不够高效[^3]。 - **典型应用场景**: - 较大但非极高的并发环境下的服务器端编程。 3. **`epoll`** - **特点**: - 基于事件驱动模型设计,专门针对高性能需求而优化[^1]。 - 使用红黑树存储注册过的文件描述符及其关联的状态信息;利用双向链表管理活跃的事件队列[^4]。 - 只需一次性的将文件描述符加入到 epoll 实例中即可,后续无需重复传输整个集合至内核层。 - 支持两种触发模式——边缘触发(Edge Triggered, ET)与水平触发(Level Triggered, LT),提供了更大的灵活性[^1]。 - **性能**: - 即使面对海量连接也能维持较高的效率,因为它只会关注那些确实发生了变化的少数几个文件描述符而不是全部检查一遍[^1]。 - 数据结构的设计使得无论是添加还是查询操作都非常迅速,整体时间复杂度接近于 O(1)[^4]。 - **典型应用场景**: - Web 服务、数据库代理等超高并发的服务端架构。 --- #### 三、总结表格 | 属性 | Select | Poll | Epoll | |--------------|---------------------------------|--------------------------------|-------------------------------| | 文件描述符限 | 默认最多 1024 | 无固定限制 | 几乎不受限 | | 效率 | O(n) | O(n) | 接近 O(1) | | 用户态<->内核态交互频率 | 每次都需传递完整集合 | 每次也都需传递完整集合 | 初始化阶段一次性设置 | | 是否支持多种触发模式 | 否 | 否 | 是 | --- #### 四、代码示例 以下是使用 `epoll` 的一段简化版 C++ 示例代码: ```c++ #include <sys/epoll.h> #include <unistd.h> #define MAX_EVENTS 10 int main() { int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); return 1; } struct epoll_event event; event.events = EPOLLIN; event.data.fd = STDIN_FILENO; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) { perror("epoll_ctl: add"); close(epoll_fd); return 1; } struct epoll_event events[MAX_EVENTS]; while (true) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); break; } for(int i = 0;i < nfds; ++i){ printf("Event on fd %d\n",events[i].data.fd ); } } close(epoll_fd); } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值