之前select操作会有两部分,第一是进程加入到监听socket的等待队列, 第二是阻塞监听socket的读写事件。
多次调用select的流程如下:
添加等待队列 -> 阻塞 -> 添加等待队列 -> 阻塞 -> 添加等待队列 -> 阻塞 ...
大部分场景下,我们进程需要监听的socket都相对比较固定,不需要每次都加入socket等待队列,在每个socket再移除,如此反复。
epoll将这两个操作分开了,先用epoll_ctl维护等待队列,epoll_wait来阻塞进程。
多次调用epoll的流程如下:
添加等待队列 -> 阻塞 -> 阻塞 -> 阻塞 -> 阻塞 ...
epoll调用之后,内核会创建一个eventpoll对象,它有一个监听队列,所有进程需要监听的socket的文件描述符都将添加到eventpoll对象的监听队列中,这样,就不需要每次都将文件描述符从用户空间拷贝到内核空间了。
和上面的图展示的一样,epoll再内核空间维护一个rdlist。 select效率低的一个原因是因为它不知道到底是哪些socket已经就绪状态了,因此,select必须遍历所有的socket,进行状态的判断。 rdlist中引用了已经就绪状态的socket的文件描述符,这样就不会像selext那样把eventpoll的监控列表整个都遍历一遍。
int epfd = epoll_create()
epoll_ctl(epfd, sockfd...)
while(){
epoll_wait()
for (socket s ) {
...
}
}
- 监听的三个socket的文件描述符添加到eventpoll的监听队列
- 进程A执行epoll_wait之后,进入到eventpoll的等待队列中
- 如果某个socket就绪,则会进入到rdlist的就绪队列
- 一方面修改rdlist
- 另一方面唤醒eventpoll等待队列中的进程,进程A再次进入到CPU工作队列
也正是因为rdlist的存在,进程A知道是哪些socket发生了变化。
下面过一下epoll的工作流程:
首次,应用进程调用epoll_create的同时,在内核中创建了event_poll对象,对应的文件描述符,同时还创建了一个数据结构: 红黑树 + 链表
红黑树用来存储所有需要监听的socket的文件描述符,采用红黑树的结构,相对于select采用fd_set的结构,性能要好的多。
同时epoll维护的rdlist,也就是链表结构,因为所有改变的socket都是需要进程区处理的,这里采用链表的结构,是非常合理的。
用户进程调用epoll_wait之后,当rdlist中有改变的socket之后,进程会被中断程序唤醒,重新进入到CPU的工作队列,进行socket通信等操作。
int epoll_create(int size) // 返回一个文件描述符 epfd
这里注意的是,在使用epoll完成之前,文件描述符会一直被占用,所以,**在使用完成之后,需要close**
当用户进程创建epoll_create时,内核会创建一个eventpoll对象(epfd文件描述符),和socket一样,他也是有等待队列的。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event)
第二个参数:操作类型
- EPOLL_CTL_ADD: 注册新的fd到epdf中
- EPOLL_CTL_MOD: 修改已经注册到epfd的监听事件
- EPOLL_CTL_DEL: 从epfd中删除fd
第三个参数: 需要监听的socket的文件描述符
第四个参数: 告诉内核需要监听什么类型的事件,使用epoll_event结构体表示
struct epoll_event {
int events;
epoll_data data;
}
具体的事件类型:
这里有一个触发方式非常重要
‼️ 默认水平触发。还有一个边缘触发。
通过epoll_ctl 可以添加或删除需要监听的socket。
被监听的socket的文件描述符会添加到eventpoll的监听队列中,其底层数据结构是一个红黑树。
当socket收到数据后,中断程序会操作eventpoll的就绪队列rdlist,而不是直接操作读取数据的进程,当socket收到数据时,中断程序会吧这两个socket放入rdlist。
int epoll_wait(int epfd, struct epoll_enent * events, int maxevnets, int timeout)
**第二个参数: ** 用来从内核得到事件的集合,epoll把将要发生的事件赋值到events数组中。 events不可以是空指针,内核只负责吧数据复制到这个events数组中,不会帮助我们在用户态分配内存。
第三个参数: 告知内核这个events有多大,这个maxevnets不能超过epoll_create创建时候的size
第四个参数: 设置超时时间,毫秒,
当用户进程调用epoll_wait时,如果rdlist不是空的,则返回,如果rdlist是空的,则进程阻塞。
当数据进入到socket缓冲区之后,eventpoll一方面将socket加入到rdlist,另一方面,唤醒等待队列中的应用进程,进程A再次进入到工作队列,进程 A进入到运行状态。