epoll 相关的系统调用有以下三个,这里简述下当调用对应函数后,内核的具体实现
epoll_creat( )
1. 在内核注册文件系统 eventpollfs,挂载此文件系统
(linux一切皆文件,便于处理)
若返回指针,指针如果出错则无法判断,而 fd 可以通过 current -> files -> fd_array[] 找到其真伪
epoll_creat 为什么返回一个 fd?因为它对应的就是这个文件系统中创建的新文件
2. 创建两个内核 cache(频繁分配小块内存,应该创建 kmem_cahe 来做内存池),分别存放 struct epitem(事件信息) 和 eppoll_entry (用于挂在设备等待队列下)
创建struct eventpoll(红黑树根/就绪链表)结构,放入 file -> private data
(一个新创建的epoll文件带有一个struct eventpoll 结构,这个结构上再挂一个红黑树,而这个红黑树就是每次 epoll_ctl 时 fd 存放的地方)
epoll_ctl( )
1. 检测红黑树中有没有当前 fd 有则返回,没有则插入树中:
ep_insert( )
创建 struct eppoll_entry(为了放入设备等待队列)
设置其唤醒回调函数为 ep_poll_callback
加入设备等待队列 (设备驱动)
(当设备就绪,唤醒等待队列上的等待者时,ep_poll_callback就会被调用,将 epitem 放入 rdlist,每次调用 epoll_wait 就只收集 rdlist 里 的 fd 就可以了)
ET/LT 工作模式的区别
来自百度百科的解释:
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block
socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。ET (edge-triggered)是高速工作方式,只支持non-block
socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK
错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only
once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
对应的具体实现如下
在内核中除了上面提到的就绪队列 rdlist 外,还另外维护了一个队列 txlist,用于内核空间与用户空间的缓冲
在 LT 模式下,当我们调用 epoll_wait() 时:
-
睡眠 & 检查 rdlist 是否为空
-
调用 ep_poll()
-
把 txlist 里的 fd 拷给用户空间,然后 ep_reinject_items 把一部分 fd 从 txlist 里返还给
rdlist 以便下次还能从 rdlist 中发现它 调用 ep_reinject_items() -
将 txlist 中 没有标注 EPOLLET 且事件被关注的 fd 重新放回 rdlist,下一次 epoll_wait()
时会再次响应