Epoll原理详解

527 阅读2分钟

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP)\color{red}{而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包)},也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本以前,那时的select或者poll事件驱动方式是这样做的。

这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的\color{green}{其实这100万连接中的大部分都是没有事件发生的}。因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这首先是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然后selectpoll就是这样做的,因此它们最多只能处理几千个并发连接\color{blue}{而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然后select和poll就是这样做的,因此它们最多只能处理几千个并发连接}。而epoll不这样做,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分:

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_create建立一个epoll对象(在epoll文件系统中给这个句柄分配资源);

  • 调用epoll_ctl向epoll对象中添加这100万个连接的套接字;

  • 调用epoll_wait收集发生事件的连接。

  这样只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epollwait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。\color{red}{因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。}

Epoll流程

 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};
  • 我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树\color{red}{红黑树}用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表\color{red}{rdllist双向链表},用于存储准备就绪的事件
  • 执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据\color{red}{然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据}
  • 当epoll_wait调用时,仅仅观察rdllist双向链表\color{red}{rdllist双向链表}里有没有数据即可。有数据就返回,并将事件复制到用户态内存(使用共享内存提高效率)。没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。\color{green}{一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。}