图解IO多路复用之epoll实现原理
epoll和select和poll一样,都是Linux提供的多路复用的模型并且可以同时监听多个文件描述上的事件是否已经就绪。epoll可理解为是poll的扩展,epoll为了同时处理大量的文件描述符而改进了poll模型,也正是由于epoll的出现使得Linux得到了更广泛的使用。
1、认识epoll
epoll提供了三个很重要的函数,分别是epoll_create(int)、epoll_ctl以及epoll_wait()。下面使用一个生活中的小例子来帮助大家理解这三个函数的作用,如下图所示:
假设小区的所有住户的快递都统一放在一个快递驿站中,然后快递员在一个固定的时间中到快递驿站中拿走快递并且发送一条短信通知用户快递已经投递出去。
epoll_ cre ate函数负责创建快递驿站;epoll_ctl函数负责操作小区中添加、移除和更新住户在快递驿站中注册的信息;epoll_wait函数负责控制快递员多久去快递驿站取一次快递。
(1)epoll_ cre ate(size)
函数主要是用来创建epoll的模型(类似于创建驿站收取快递),函数中的参数size在早期的Linux版本上是初始化传入监听的文件描述符个数,在高版本的Linux中没有实际的意义了,只是为了兼容而保留下来了,实际中只需要传一个大于0的数字就可以了。
函数的返回结果是-1则表示创建模型失败;如果返回的结果大于0则表示创建模型成功,创建的模型如下所示:
函数返回值是4则表示epoll模型中用来存储全部事件的红黑树的根节点,这里红黑树的本质就是告诉内核需要监听哪些文件描述符上的哪些事件。如下图所示:
eventpoll模型中参数含义如下图所示:
(a)rbr是红黑树,用红黑树管理客户端Socket连接,如下列举示常见的epoll_ cre ate函数返回值含义:
返回值 | 含义 |
---|---|
1 | 标准输入 |
2 | 标准错误 |
3 | 需要监听的Socket |
4 | 表示成功创建eventpoll模型 |
(b)rdlist(已就绪的双端队列)
用来告诉内核哪些文件描述符上的哪些事件已经就绪了。
(c)wq(等待队列)
当某个进程需要关注的事件还未就绪的时候,就会把当前进程的描述符以及回调函数放到这个队列中,这样数据到达的时候就会通过检查阻塞队列来找到相应的阻塞进程去让他执行后续的业务。
(2)epoll_ctl()
epoll_ctl函数的完整形式是epoll_ctl(epfd, op, fd, *event),函数的作用是创建客户端和服务端的连接以及将连接注册到 e poll_create函数的红黑树上。 epoll_ctl函数参数如下整理如下表所示:
参数 | 含义 |
---|---|
epfd | 这个参数就是epoll_create函数的返回值(也就是4) |
op | 表示的是具体的动作,如EPOLL_ADD、MOD、DEL |
fd | 表示需要监听的文件描述符 |
*event | 具体的事件,如EPOLLIN(读事件) |
在调用 epoll_ctl函数的时候还会生成一个epitem结构体,这个结构体如下图所示:
其结构体中各个参数的函数如所示:
ffd指代的是每个文件描述符的指针,event表示的是监听的事件。
当调用epoll_create成功之后表示epoll的模型就创建出来了,此时就可以接受客户端的连接,假设此时有5个客户端连接上服务器,服务器就会调用epoll_ctl()函数,在红黑树上创建5个node节点,如下图所示:
此时每个红黑树上的节点多对应自己的一个epitem结构,客户端连接注册到黑红书上的结构如下图是所:
红黑树上的每个节点存储都是我们关注的文件描述符的编号、需要监听的事件。
(3)epoll_wait
epoll_wait(
# epoll_create()的返回值
int epfd,
# 内核就绪事件复制到events数组中
struct epoll_events *events,
# events数组的元素个数
int maxevents,
int timeout
)
timeout表示的epoll_wait等待的最大超时时间,其值的含义如下所示:
值 | 含义 |
---|---|
-1 | 如果没有就绪的文件描述符就一直等待 |
=0 | 无论是否有就绪的文件描述符,poll_wait方法一轮检测完成就直接返回 |
下图展示了epoll_wait的工作流程图:
epoll _wait检测当前的就绪队列中树否存在就绪的文件描述符,如果不存在就绪的文件描述符就当前的进程让出CPU的执行权和加入到阻塞的队列中等待。
如果epoll_wait检测到就绪的文件描述符,然后它会将就绪的文件描述符信息放在 events 数组中,通过 events 数组将数据传递到用户态做处理。其实就绪队列占用的空间是内核态和用户态都可共享的区域,如下图所示:
这也是为什么epoll不需要从内核态复制到用户态的原因,进而大大的提高了效率。
2、epoll的数据读取流程
其实epoll模型在调用epoll_ctl()函数的时候,在向内核中注册fd和相关事件的时候,他就注册了一个回调函数,当有客户端有数据到达内核缓冲区的时候,操作系统将将缓冲区的内容和对应的文件描述符都添加到已就绪的队列中。此时epoll_wait()方法来判断哪些文件描述符上的哪些事件已经就绪了,将就绪的数据通过唤醒对应的进程继续后续的逻辑动作执行。
当数据达到内核缓冲区的时候,epoll是如何通知就绪队列有数据达到的呢?这里epoll提供了两种触发机制,如下所示:
(1)水平触发
只要有就绪的事件,如果环形缓冲区的数据没有取走就会一直通知,类似于数字电路中的高水平触发一样的,如下图所示:
(2)边缘触发
只要有就绪事件,必须是从没有到有这个过程才会触发通知,并且只会通知一次。类似数字电路中上升沿触发机制一样,如下图所示:
水平触发支持阻塞读写和非阻塞读写,所以select、poll、epoll默认是水平触发。边缘触发通知用户态的次数比水平触发通知用户态的次数少,像Nginx采用的是边缘触发机制。
总结:
(1)epoll中引入了红黑树,不仅实现了让线程只关心自己注册的事件,而且事件就绪可以快速的通知到指定的线程
(2)epoll中就绪的文件描述符是不需要从内核态复制到用户态的
(3)epoll基于红黑树与双向链表实现的,没有最大的连接问题
(4)epoll的应用很广泛,常见的中间件如nginx、redis、netty都在使用