Epoll是Linux IO多路复用的一种IO管理机制。内核的实现代码在Linux内核源码的fs/eventpoll.c
中。是比select和poll更高性能的一种IO管理机制。
前期准备
在实现epoll之前,要先了解内核epoll的运行原理。内核的epoll可以从四方面来理解。
- Epoll 的数据结构,rbtree 对<fd, event>的存储,ready 队列存储就绪 io。
- Epoll的线程安全,SMP的运行,以及防止死锁。
- Epoll内核回调
- Epoll的LT和ET
具体实现
Epoll数据结构
Epoll 主要由两个结构体:eventpoll 与 epitem。Epitem 是每一个 IO 所对应的的事件。比如
epoll_ctl EPOLL_CTL_ADD 操作的时候,就需要创建一个 epitem。Eventpoll 是每一个 epoll 所
对应的的。比如 epoll_create 就是创建一个 eventpoll。
- Epitem定义
- Eventpoll定义
数据结构示意图
List用来存储就绪的IO。当内核IO准备就绪的时候就会执行epoll_event_callback
的回调函数,将epitem添加到List中。当epoll_wait激活重新运行的时候,将list的epitem逐一copy到events参数中。
RBtree用来存储所有io数据,方便快速通过io_fd查找。也从insert与remove来讨论。
当 App 执行 epoll_ctl EPOLL_CTL_ADD 操作,将 epitem 添加到 rbtree
中。当 App 执行 epoll_ctl EPOLL_CTL_ADD 操作,将 epitem 从rbtree中删除。
Epoll锁机制
// 获取自旋锁 pthread_spin_lock(&ep->lock); // epitem的rdy置1,代表epitem在就绪队列重,后续触发相同事件只需要修改event epi->rdy = 1; // 添加到list中 LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink); // 将eventpoll的rdnum加1 ep->rdnum ++; // 释放spinlock pthread_spin_unlock(&ep->lock);
Epoll回调
Epoll 的回调函数何时执行,此部分需要与 Tcp 的协议栈一起来阐述。Tcp 协议栈的时序图如
下图所示,epoll 从协议栈回调的部分从下图的编号 1,2,3,4。具体 Tcp 协议栈的实现,后续
从另外的文章中表述出来。下面分别对四个步骤详细描述
编号 1:是 tcp 三次握手,对端反馈 ack 后,socket 进入 rcvd 状态。需要将监听 socket 的
event 置为 EPOLLIN,此时标识可以进入到 accept 读取 socket 数据。
编号 2:在 established 状态,收到数据以后,需要将 socket 的 event 置为 EPOLLIN 状态。
编号 3:在 established 状态,收到 fin 时,此时 socket 进入到 close_wait。需要 socket 的 event
置为 EPOLLIN。读取断开信息。
编号 4:检测 socket 的 send 状态,如果对端 cwnd>0 是可以,发送的数据。故需要将 socket
置为 EPOLLOUT。
所以在此四处添加 EPOLL 的回调函数,即可使得 epoll 正常接收到 io 事件。
有点尴尬,这个图等我有空了再重新画一遍,我不知道他要vip才能无水印。
LT和ET
LT(水平触发)与 ET(边沿触发)是电子信号里面的概念。不清楚可以 man epoll 查看的。
如下图所示:
比如:event = EPOLLIN | EPOLLLT,将 event 设置为 EPOLLIN 与水平触发。只要 event 为 EPOLLIN
时就能不断调用 epoll 回调函数。
比如: event = EPOLLIN | EPOLLET,event 如果从 EPOLLOUT 变化为 EPOLLIN 的时候,就会触
发。在此情形下,变化只发生一次,故只调用一次 epoll 回调函数。关于水平触发与边沿触
发放在 epoll 回调函数执行的时候,如果为 EPOLLET(边沿触发),与之前的 event 对比,如
果发生改变则调用 epoll 回调函数,如果为 EPOLLLT(水平触发),则查看 event 是否为 EPOLLIN,
即可调用 epoll 回调函数。