epoll详解——从功能到内核

        首先我们了解以下什么是I/O复用。I/O就是指网络中的I/O(即输入输出),多路是指多个TCP连接,复用是指一个或少量线程被重复使用。连起来理解就是,用少量的线程来处理网络上大量的TCP连接中的I/O。常见的I/O复用有以下三种:

  1. select
  2. poll
  3. epoll

为什么使用epoll?

这个问题也可以理解为epoll相比于select和poll有什么缺点。首先我们来分析一下select。

select函数声明:

#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdpl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

        函数第一个参数是被监听的描述符的最大值+1,select底层的数据结构是位数组,因此必须知道被监听的最大描述符才可以确定描述符的范围,否则就需要将整个数组遍历一遍。

       函数第二、三、四个参数是被监听的事件,分别是读、写、异常事件。

       函数的最后一个参数是监听的时间(NULL、0、正值)

selct缺点:

  1. 可以从函数参数列表上来看,select只能监听读、写、异常这三个事件
  2. selct监听的描述符是有最大值限制的,在Linux内核中是1024
  3. select的实现是每次将待检测的描述符放在位数组中,全部传给内核进行监听,内核监听之后会返回一个就绪描述符个数,并且修改了监听的事件值,以表示该事件就绪。内核再将修改后的数组传给用户空间。用户空间只能通过遍历所有描述符来处理就绪的描述符,之后再将描述符传给内核继续监听......很明显,这样在监听的描述符少的情况下并不影响效率,但是监听的描述符数量特别大的情况下,每次又只有少数描述符上有事件就绪,大量的换入换出会使得效率十分低下

第二步,我们接着分析以下poll

poll函数声明:

int poll(struct pollfd fdarray[],unsigned long nfds,int timeout);
struct pollfd{

    int fd;

    short events;

    short revents;

};

        第一个参数是个结构体数组,结构体中声明了被监听描述符和相应的事件,每个被监听的描述符对应一个结构体,数组表示可以监听多个描述符。

       第二个参数是被监听描述符的个数。

       第三个参数同select,只监听时间

poll缺点:

        从函数参数来看,poll解决了select前两个问题,监听的描述符数量没有严格限制,监听的事件不止读、写、异常,但是第三个缺点依然存在,存在大量的换入换出。

       在Linux2.4及以前的版本中使用的是select和poll,在2.6的时候使用的是epoll,正是因为epoll解决了select和poll共同存在的问题。接下来我们看一下epoll是怎么解决的。

epoll的相关函数

#include <sys/epoll.h>

int epoll_create(int size);

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

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

epoll内核剖析

  • epoll_create

       epoll_create的做法是创建出一个内核事件表,实际上就是创建文件,这其中包括文件描述符的分配、文件实体的分配等等,在这里我们记住文件描述符中有一个域很重要,就是:private_data域,该域才是epoll的核心,其中有内核时间表、就绪描述符队列等信息。如下图:

epoll详解——从功能到内核

  • epoll_ctl

       该函数主要是对内核事件表的操作,涉及插入(添加监听描述符)、删除(删除被监听的描述符)、修改(修改被监听的描述符)。主要有以下步骤:

  1. 遍历内核事件表,看该描述符是否在内核事件表中。
  2. 判断所要做的操作:插入、删除或是修改
  3. 根据操作做相应处理

epoll详解——从功能到内核

注意:此处注意的一点是在插入的时候,对相应的描述符注册了回调函数,即当该描述符上有数据就绪时,自动调用回调函数将该描述符加入就绪队列

  • epoll_wait

        在看epoll_wait之前,我们来以下内核事件表和组织就绪描述符的数据结构。内核事件表的底层数据结构是红黑树,就绪描述符的底层数据结构是链表。

       epoll_wait的功能就是不断查看就绪队列中有没有描述符,如果没有就一直检查、直到超时。如果有就绪描述符,就将就绪描述符通知给用户。此处有关ET和LT模式就是在给用户空间返回就绪描述符的时候体现的。

      回顾以下ET模式和LT模式:ET模式是高效模式,就绪描述符只通知用户一次,如果用户没做处理内核将不再进行通知;LT模式比较稳定,如果用户没有处理就绪的描述符,内核会不断通知。

     接下来我们看一下具体是怎么实现的。当为ET模式时,上边我们提到就绪描述符是用链表组织的,因此只需将就绪部分断链发给用户,而在LT模式下,用户没有处理就绪描述符时,内核会再次将未处理的就绪描述符加入到就绪队列中重复提醒用户空间。