epoll 是Linux IO 多路复用的管理机制。作为现在Linux平台高性能网络IO必要的组件。内核的实现可以参考: fs/eventpoll.c
在实现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
数据结构如下:
List 用来存储准备就绪的IO,对于数据结构主要讨论两方面:insert与remove。同样如此,对于list我们也讨论insert与remove。何时将数据插入到list中呢?当内核io准备就绪的时候,则就会执行epoll_event_callback的回调函数,将epitem添加到list中。
那何时删除list中的元素呢?当epoll_wait 激活重新运行的时候,将list的epitem逐一copy到events参数中。
Rbtree用来存储所有io的数据,方便快速通io_fd查找。也从insert与remove来讨论。对于rbtree何时添加:当App执行epoll_ctl EPOLL_CTL_ADD操作,当epitem添加到rbtree中。何时删除呢?当App执行epoll_ctl EPOLL_CTL_DEL操作,将epitem添加到rbtree中。List 与rbtree的操作又如何做到线程安全,SMP ,防止死锁呢?
Epoll锁机制
Epoll从以下几个方面是需要加锁保护的。List的操作,rbtree的操作,epoll_wait的等待。List使用最小粒度的锁spinlock,便于在SMP下添加操作的时候,能够快速操作list。list添加
346 行:获取 spinlock。
347 行:epitem 的 rdy 置为 1,代表 epitem 已经在就绪队列中,后续再触发相同事件就只需
更改 event。
348 行:添加到 list 中。
349 行:将 eventpoll 的 rdnum 域 加 1。
350 行:释放 spinlock
301 行:获取 spinlock
304 行:判读 rdnum 与 maxevents 的大小,避免 event 溢出。
307 行:循环遍历 list,判断添加 list 不能为空
309 行:获取 list 首个结点
310 行:移除 list 首个结点。
311 行:将 epitem 的 rdy 域置为 0,标识 epitem 不再就绪队列中。
313 行:copy epitem 的 event 到用户空间的 events。
316 行:copy 数量加 1
317 行:eventpoll 中 rdnum 减一。
避免 SMP 体系下,多核竞争。此处采用自旋锁,不适合采用睡眠锁。
149 行:获取互斥锁。
153 行:查找 sockid 的 epitem 是否存在。存在则不能添加,不存在则可以添加。
160 行:分配 epitem。
167 行:sockid 赋值
168 行:将设置的 event 添加到 epitem 的 event 域。
170 行:将 epitem 添加到 rbrtree 中。
173 行:释放互斥锁。
177 行:获取互斥锁。
181 行:删除 sockid 的结点,如果不存在,则 rbtree 返回-1。
188 行:释放 epitem
190 行:释放互斥锁。
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 事件。
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 回调函数。