select、poll、epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。但select、poll、epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间;
当某一个进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表里没数据也返回。
同时,所有添加到epoll中的事件都会与设备驱动程序建立回调关系,也就是说相应事件的发生会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事放在上面的rdllist双向链表中。
当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高,可以轻易地处理百万级别的并发连接。
当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所表示的对象)。eventpoll对象也是文件系统的一员,和socket一样,它也会有等待队列。
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。中断程序会给eventpoll的”就绪列表“添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdllist引用这两个socket。
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
当程序执行到epoll_wait时,如果rdlist已经引用到了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll在select和poll(poll和select基本一样,有少量改进)的基础引入了eventpoll作为中间层,使用了先进的数据结构,是一种高效的多路复用技术。