为什么要了解epoll的实现原理?由于在某些环境中项目需要用户态实现tcp协议栈,而内核的epoll对内核的tcp套接字做了管理,而用户态的tcp协议栈得到的fd是设备文件系统的fd并不是内核的tcp套接字,因此不包含对tcp四元组的管理。因此需要了解epoll实现原理来自己实现一个用户态的epoll,来对tcp的四元组做管理。
epoll的数据结构
epoll的接口有三个,epoll_create、epoll_ctl和epoll_wait。
epoll_create创建一个IO多路复用的管理对象。epoll_ctl对epoll_create创建的管理对象的操作,增加、删除和修改需要IO事件检测的fd。epoll_wait对epoll_create创建的管理对象中的fd进行IO事件检测,并把触发事件的fd对应的数据copy出来。
epoll_create成功创建一个管理对象会返回一个ep_fd索引,epoll_ctl和epoll_wait只需要对ep_fd操作就是对创建的管理对象的操作了。
管理对象中,包含了一个存储所有向管理对象增加IO事件检测的fd集合和一个就绪集合。fd集合是个红黑树,而就绪集合是个就绪链表队列。hash表和btree/b+tree相对于红黑树,hash表一开始创建的空间比较多,但是数据量大的时候查询速度快。btree/b+tree主要是多叉树降低层高,适用于硬盘存储。
fd集合和就绪集合的节点是同一个节点,加入就绪队列,其实就是把就绪队列最后一个节点的后继指针指向加入的节点,把加入节点的前置指针指向就绪队列最后一个节点。
协议栈如何与epoll模块通信
IO事件检测主要有EPOLLIN事件的触发与EPOLLOUT事件的触发。
EPOLLIN事件触发有三种,一种是listen后然后完成三次握手后建立连接加入到全连接队列的时候触发,一种是读缓冲区可读的时候触发,一种是接收到对端发送的FIN标识的包的时候触发。EPOLLOUT事件的触发是写缓冲区可写的时候触发。
协议栈通过把触发事件的fd与触发的事件作为参数调用回调函数查找对应的节点,然后把节点加入到就绪队列。用户通过epoll_wait读取就绪队列的节点内容。
以上的事件触发条件都是LT(水平触发)的时候的表现。ET(边缘触发)的时候,EPOLLIN事件触发有一点不一样,把读缓冲区可读的时候触发改为有数据加入读缓冲区的时候触发。EPOLLOUT时间的触发也不一样,把写缓冲区可写的时候触发改为写缓冲区从不可写变为可写的时候触发。
另外注意的是,在et模式下,同时注册了EPOLLIN与EPOLLOUT事件,只要EPOLLIN(或者EPOLLOUT)触发了,那么如果写缓冲区可写(或者读缓冲区可读),那么触发的事件也有EPOLLOUT(或者EPOLLIN)。
epoll如何加锁
epoll_create创建管理对象不用加锁。
epoll_ctl对红黑树加互斥锁。
epoll_wait与加入就绪队列的时候加自旋锁,因为锁住的操作就是push到就绪队列或者从就绪队列pop出来,这个操作的指令并不复杂,时间耗费上比线程切换上下文耗费的时间更加小。