Skip to content

Latest commit

 

History

History
190 lines (89 loc) · 6.24 KB

31.彻底明白:epoll 底层系统调用的核心原理.md

File metadata and controls

190 lines (89 loc) · 6.24 KB

31.彻底明白:epoll 底层系统调用的核心原理

epoll是如何解决select和poll效率低的问题的呢?

之前select操作会有两部分,第一是进程加入到监听socket的等待队列, 第二是阻塞监听socket的读写事件。

多次调用select的流程如下:

添加等待队列 -> 阻塞 -> 添加等待队列 -> 阻塞 -> 添加等待队列 -> 阻塞 ...

大部分场景下,我们进程需要监听的socket都相对比较固定,不需要每次都加入socket等待队列,在每个socket再移除,如此反复。

epoll将这两个操作分开了,先用epoll_ctl维护等待队列,epoll_wait来阻塞进程。

多次调用epoll的流程如下:

添加等待队列 -> 阻塞 -> 阻塞 -> 阻塞 -> 阻塞 ...

epoll 系统调用的三个方法

epoll的监听队列

epoll调用之后,内核会创建一个eventpoll对象,它有一个监听队列,所有进程需要监听的socket的文件描述符都将添加到eventpoll对象的监听队列中,这样,就不需要每次都将文件描述符从用户空间拷贝到内核空间了。

epoll的事件就绪列表

和上面的图展示的一样,epoll再内核空间维护一个rdlist。 select效率低的一个原因是因为它不知道到底是哪些socket已经就绪状态了,因此,select必须遍历所有的socket,进行状态的判断。 rdlist中引用了已经就绪状态的socket的文件描述符,这样就不会像selext那样把eventpoll的监控列表整个都遍历一遍。

int epfd = epoll_create()
epoll_ctl(epfd, sockfd...)
while(){
	epoll_wait()
	for (socket s ) {
		...
	}
}

epoll例子

  • 监听的三个socket的文件描述符添加到eventpoll的监听队列
  • 进程A执行epoll_wait之后,进入到eventpoll的等待队列中
  • 如果某个socket就绪,则会进入到rdlist的就绪队列

当socket收到数据,中断程序做了两件事

  • 一方面修改rdlist
  • 另一方面唤醒eventpoll等待队列中的进程,进程A再次进入到CPU工作队列

也正是因为rdlist的存在,进程A知道是哪些socket发生了变化。

epoll的工作流程

下面过一下epoll的工作流程:

首次,应用进程调用epoll_create的同时,在内核中创建了event_poll对象,对应的文件描述符,同时还创建了一个数据结构: 红黑树 + 链表

红黑树用来存储所有需要监听的socket的文件描述符,采用红黑树的结构,相对于select采用fd_set的结构,性能要好的多。

同时epoll维护的rdlist,也就是链表结构,因为所有改变的socket都是需要进程区处理的,这里采用链表的结构,是非常合理的。

用户进程调用epoll_wait之后,当rdlist中有改变的socket之后,进程会被中断程序唤醒,重新进入到CPU的工作队列,进行socket通信等操作。

epoll_create

int epoll_create(int size) // 返回一个文件描述符 epfd

这里注意的是,在使用epoll完成之前,文件描述符会一直被占用,所以,**在使用完成之后,需要close**

当用户进程创建epoll_create时,内核会创建一个eventpoll对象(epfd文件描述符),和socket一样,他也是有等待队列的。

epoll_ctl

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发生改变时

当socket收到数据后,中断程序会操作eventpoll的就绪队列rdlist,而不是直接操作读取数据的进程,当socket收到数据时,中断程序会吧这两个socket放入rdlist。

epoll_wait

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之后,数据可读

当数据进入到socket缓冲区之后,eventpoll一方面将socket加入到rdlist,另一方面,唤醒等待队列中的应用进程,进程A再次进入到工作队列,进程 A进入到运行状态。