一、epoll底层数据结构

在epoll的使用中,我们经常需要对文件描述符集合进行添加、删除等操作,同时对触发的事件类型进行处理,回调IO事件中的工作函数。这其中离不开两个数据结构的帮助------epitem与eventpoll。

1.1 eventpoll结构体

eventpoll是每一个 epoll所对应的,比如epoll_create()就是创建一个 eventpoll。

内核源码文件中eventpoll的定义如下:

// file:fs/eventpoll.c
struct eventpoll { 	
    //sys_epoll_wait用到的等待队列
    wait_queue_head_t wq;
    //接收就绪的描述符都会放到这里
    struct list_head rdllist;
    //每个epoll对象中都有一颗红黑树
    struct rb_root rbr;
    ......
}

其中wq为等待队列链表,软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。(epoll_wait若就绪队列中无数据,会将当前进程加入到等待队列中)

rdllist标识就绪的文件描述符链表,当有的连接就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。

rbr为一颗红黑树,采用红黑树的结构为epoll高效的处理海量数据的增删改查,这个红黑树用于管理用户进程添加进来的所有socket连接。

1.2 epitem结构体

epitem是每一个IO所对应的的事件,比如epoll_ctl()中使用EPOLL_CTL_ADD操作的时候,就需要创建一个epitem。

内核源码文件中epitem的定义如下:

// file:fs/eventpoll.c
struct epitem {
    //红黑树节点
    struct rb_node rbn;
    //socket文件描述符信息
    struct epoll_filefd ffd;
    //所归属的 eventpoll 对象
    struct eventpoll *ep;
    //等待队列
    struct list_head pwqlist;
}

1.3 epoll_ctl添加socketFd

eventpoll与epitem的联系如下图:

rrthread 消息队列 消息队列 epoll_linux


当我们使用epoll_ctl()函数注册一个socket时,内核将会做这些事情:

  1. 分配一个红黑树节点对象epitem
  2. 添加等待事件到socket的等待队列中
  3. 将epitem插入到epoll对象的红黑树中

1.4 epoll_wait等待数据

epoll_wait被调用时会观察 eventpoll->rdllist 链表里有没有数据,有数据就返回,没有数据就创建一个等待队列项,将其添加到 eventpoll 的等待队列上(1.1节中的wait_queue_head_t),然后把自己阻塞掉就完事。

二、epoll的锁机制

epoll在使用时也离不开锁机制的保护,主要的使用场景有:链表操作、红黑树操作、epoll_wait的等待。

2.1 链表操作

链表操作使用的是spinlock自旋锁,当没有竞争到锁资源时,不会睡眠,加快了链表操作的速度,添加和删除操作需要加锁。

2.2 红黑树操作

红黑树操作使用的是互斥锁,在添加和删除操作时需要加锁。

2.3 epoll_wait等待

采用pthread_cond_wait。

三、epoll的回调时机

在前面对epoll底层结构进行梳理之后,那么epoll是如何知道IO事件触发的呢?即epoll怎么知道有就绪fd的?

显然内核协议栈会在特点的时机通过回调函数通知咱们的epoll有IO事件到来,情况如下:

rrthread 消息队列 消息队列 epoll_linux_02

四、LT与ET

epoll的高效还与LT(水平)和ET(边缘)两种模式离不开,下面给出总结如下:

rrthread 消息队列 消息队列 epoll_rrthread 消息队列_03

五、epoll与select/poll的对比

  1. 对于select/poll来说,所有的文件描述符都是在用户态被加入集合中,每次调用需要将整个集合拷贝到内核态;epoll则将整个集合维护在内核态,但是每次添加文件描述符的时候需要执行一次系统调用,如果短时间内有大量活跃的连接时,epoll的性能可能不如select/poll。
  2. select使用线性表来描述文件描述符集合,有上限;poll使用链表来描述;epoll则使用红黑树来描述,同时还会维护一个就绪双向链表,用于存放已就绪的事件。
  3. select/poll的主要开销来自内核判断是否有文件描述符就绪的过程,每次执行select/poll调用时,会采用遍历整个集合的方法来判断是否有文件描述符就绪;epoll则不需要,当有事件发生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的描述符放到双向链表中,等待epoll_wait的调用处理。
  4. 当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select/poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll的性能会很好。