IO多路转接:epoll

IO多路转接:epoll

什么是EPOLL?

本质是一颗红黑树。监听事件的文件描述符则是红黑树的子节点,提高程序在大量并发连接中只有少量活跃的情况下 CPU的利用率

(比如我有1000个并发连接,但只有[3, 500, 1000] 有数据传输,此时使用select()轮询则效率过低,而使用红黑树的epoll则可以快速返回响应的fd

epoll是select和poll的升级版,相较于这两个前辈,epoll改进了工作方式,因此它更加高效。

  1. select和poll是基于线性方式处理的,epoll是基于**红黑树**来管理待检测集合的。
  2. select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高
  3. select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
  4. select和poll需要对返回的集合进行判断才能知道哪些文件描述符是就绪的,epoll可以直接得到已就绪的文件描述符集合,无需再次检测

select/poll低效的原因之一是将“添加/维护待检测任务”和“阻塞进程/线程”两个步骤合二为一。每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket个数相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl()维护等待队列,再调用epoll_wait()阻塞进程(解耦)。

如下图所示,每次select/poll的操作都是将添加待检测任务(green)和阻塞(yellow)合在一起

img

一、 API函数

  • int epoll_create(int size); 返回一个文件描述符,作为红黑树的根节点

    • size:创建的红黑树的监听节点数量。(Linux内核2.6.8版本以后,这个参数是被忽略,大于0即可)
    • 返回值:指向新创建的红黑树的根节点的 fd。
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 管理红黑树上的文件描述符(添加、修改、删除)

    • epfd:epoll_create()的返回值 -> 代表红黑树的根节点

    • op:对该监听红黑树所做的操作

      • EPOLL_CTL_ADD:添加fd到监听红黑树
      • EPOLL_CTL_MOD:修改fd在监听红黑树的监听事件(读、写、异常事件)
      • EPOLL_CTL_DEL:从监听红黑树上删除fd(取消监听)
    • fd:待监听的fd (监听连接事件->lfd, 监听读写事件->cfd)

    • event:epoll事件,用来修饰第三个参数对应的文件描述符,指定检测这个文件描述符的什么事件,struct epoll_event结构体如下:

      struct epoll_event {
                     uint32_t     events;      
                     epoll_data_t data;        
      };
      /*events:委托epoll检测的事件
      					EPOLLIN		读
             				EPOLLOUT	写
                    		EPOLLERR	异常
      ☆data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。
      */
      typedef union epoll_data {
      	void        *ptr;			//反应堆模型->用作回调函数
      	int          fd;			//对应监听事件的fd,与epoll_ctl第三参数对应
      } epoll_data_t;
      
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 检测epoll树中是否有就绪的文件描述符

    • epfd:epoll_create()的返回值 -> 代表红黑树的根节点
    • events传出参数,这是一个结构体数组的地址,,存储满足监听条件的fd结构体struct epoll_event)。
      • 只要是在数组里的全部都是满足监听条件的fd。则无需轮询查询fd
    • maxevents:第二个参数中结构体数组的大小(元素个数)。
    • timeout:超时阻塞
    • 返回值: 满足监听事件的总个数,可用作循环上限

二、 epoll的使用

操作步骤:

  • 创建监听的套接字,绑定,监听三件套

    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    //设置端口复用(可选)
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    listen(lfd, 128);
    
  • 创建epoll实例对象int epfd = epoll_create(100);

  • 将用于监听的套接字添加到epoll实例中

    struct epoll_event ev;
    ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
    ev.data.fd = lfd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    
  • 检测添加到epoll实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理

    int num = epoll_wait(epfd, evs, size, -1);
    
    • 如果是监听文件描述符lfd,和新客户端建立连接,将得到的文件描述符添加到epoll实例中。

       for(int i=0; i<num; ++i) {
      	// 取出当前的文件描述符,其中evs是epoll_wait的第二个参数
      	int curfd = evs[i].data.fd;
      	if(curfd == lfd){
      		int cfd = accept(curfd, NULL, NULL);
      		ev.events = EPOLLIN;
      		ev.data.fd = cfd;
      		// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
      		epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
          }
      
    • 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除

      	int len = recv(curfd, buf, sizeof(buf), 0);
      	if(len == 0){
          	// 将这个文件描述符从epoll模型中删除
      	    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
          	close(curfd);
      	}
      

三、epoll的工作模式

3.1 水平模式(LT模式)

LT(level triggered)是默认的工作方式,缓冲区剩余未读尽的数据会导致epoll_wait()的返回。并且同时支持block和no-block socket

  • 触发条件:只要文件描述符(fd)上有未处理的数据,就会触发事件通知。

  • 处理逻辑:可以多次触发通知,直到数据被完全读取或写入。

  • 特性

    • 默认模式epoll 默认工作在 LT 模式。

    • 适用场景:适合对事件不敏感的场景,编程简单,容易调试。

    • 可能的缺点:容易出现性能问题,因为可能反复触发同一事件。

3.2 边沿模式(ET模式)

高速工作方式,缓冲区剩余未读尽的数据不会导致epoll_wait()的返回。只支持no-block socket

  • 触发条件:只有文件描述符状态从未就绪变为就绪时,才会触发一次事件通知。

    • ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
  • 处理逻辑:事件不会重复触发,程序必须一次性将数据全部处理完。

  • 特性

    • 更高性能:减少了事件触发的次数,适合高性能场景。

    • 更复杂:需要使用非阻塞 I/O,并确保数据被完整处理,容易出现问题。

    • 适用场景:对事件处理要求严格、注重性能的场景。

epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。

epoll在边沿模式下,必须要将套接字设置为非阻塞模式。 -> 需要处理读完的异常(当缓冲区数据被读完了,调用的read()/recv()函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1)

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;	// 设置边沿模式、

// 将文件描述符设置为非阻塞
// 得到文件描述符的属性
int cfd = accept(curfd, NULL, NULL);
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

//...
int len = recv(curfd, buf, sizeof(buf), 0);
if(len < 0){
	if(errno == EAGAIN){
		printf("数据读完了...\n");
        break;
	}
	else{
 		perror("recv");
        exit(0);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值