Epoll是Linux IO的多路复用的机制,是select/poll的增强版本,在Linux内核fs/eventpoll.c中可以查看epoll的具体的实现。
一、epoll数据结构
学习任何组件,首先得知道它有什么数据结构或者数据类型,epoll主要有两个结构体:eventpoll和epitem。epitem是每一个IO对应的事件,比如EPOLL_CTL_ADD操作时,就需要创建一个epitem;eventpoll是每一个epoll所对应的,比如epoll_create就是创建一个eventpoll。
struct epitem {
union {
/* RB tree node links this structure to the eventpoll RB tree */
struct rb_node rbn;
/* Used to free the struct epitem */
struct rcu_head rcu;
};
/* List header used to link this structure to the eventpoll ready list */
struct list_head rdllink;
/*
* Works together "struct eventpoll"->ovflist in keeping the
* single linked chain of items.
*/
struct epitem *next;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;//sockfd
/* List containing poll wait queues */
struct eppoll_entry *pwqlist;
/* The "container" of this item */
struct eventpoll *ep;
/* List header used to link this item to the "struct file" items list */
struct hlist_node fllink;
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu *ws;
/* The structure that describe the interested events and the source fd */
struct epoll_event event;
};
struct eventpoll {
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
struct list_head rdllist;
/* Lock which protects rdllist and ovflist */
rwlock_t lock;
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
/*
* This is a single linked list that chains all the "struct epitem" that
* happened while transferring ready events to userspace w/out
* holding ->lock.
*/
struct epitem *ovflist;
/* wakeup_source used when ep_scan_ready_list is running */
struct wakeup_source *ws;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
struct file *file;
/* used to optimize loop detection check */
u64 gen;
struct hlist_head refs;
#ifdef CONFIG_NET_RX_BUSY_POLL
/* used to track busy poll napi_id */
unsigned int napi_id;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/* tracks wakeup nests for lockdep validation */
u8 nests;
#endif
};
数据结构如下图所示。
list用来存储就绪的IO,rbtree用来存储所有IO,方便快速查找fd,这两种数据结构我们都从inster和remove来讨论。对于list,当内核IO准备就绪时,则执行epoll_event_callback的回调函数,将epitem添加到list中;当epoll_wait激活重新运行时,将list的epitem 逐一拷贝到events中,并删除list中被拷贝出来的epitem。
对于rbtree又该何时添加何时删除呢?当app执行epoll_ctl(EPOLL_CTL_ADD)操作,将epitem添加到rbtree中;当app执行epoll_ctl(EPOLL_CTL_DEL)操作,将对应的epitem从rbtree中删除。那么list和rbtree又如何做到线程安全呢?
二、epoll锁的机制
list使用最小粒度的锁spinlock,便于在SMP下添加操作的时候,能够快速操作list。避免SMP体系下,多核竞争,此处采用自旋锁,不适合采用睡眠锁;添加操作如下。
(1)获取spinlock
(2)epitem的rdy置为1,代表epitem已在就绪队列中
(3)添加到list
(4)将eventpoll的rdnum加1
(5)释放spinlock
删除则与添加类似。
对于rbtree的操作使用互斥锁,过程如下:
(1)获取互斥锁
(2)查找sockid的epitem是否存在,不存在可以添加
(3)分配epitem
(4)sockid赋值
(5)设置event添加到epitem的event域
(6)将epitem添加到rbtree
(7)释放互斥锁
这里的互斥锁,锁的是整颗树,而不是节点;删除则与之类似操作。
三、epoll回调
首先要知道回调函数何时执行,此部分需要与tcp的协议栈联系起来理解。
(1)三次握手完成时,把fd加入到就绪队列,把event置为EPOLLIN可读,此时标识可以进入到accept读取socket数据;
(2)recvbuffer有数据的时候(可读数据),找到对应的fd加入到就绪队列,把event置EPOLLIN为可读;
(3)sendbuffer有空隙的时候(可发数据),找到对应的fd加入到就绪队列,把event置EPOLLOUT为可写;
(4)接收到fin的时候(断开连接),找到对应的fd加入到就绪队列,把event置EPOLLIN为可读;
四、LT与ET
(1)LT是水平出发,有数据就一直触发,只要recvBuffer里面有数据就一直触发,直到数据读取完,适用于大块;
(2)ET是边沿触发,从没有数据到有数据,才触发,只触发一次,即使没读完数据,也不会再触发去读取剩余的数据,剩余的数据等待下一次触发再读,适用于小块;
思考:
就绪集合为啥使用队列而不适用栈?
首先就绪集合的数据本身就需要遍历所有,肯定使用链式的数据结构,如果使用栈,就会存在就绪节点一次那不完的情况,导致上一次没被取出的节点,在下一次epoll_wait再拿的时候也可能拿不到,导致出现一些就绪节点永远都不被处理。
epoll的误区:
(1)epoll性能高,里面有内存映射,mmap
(2)epoll比select/poll要高,在fd很少时select/poll比epoll更好
C/C++Linux服务器开发高级架构师/C++后台开发架构师免费学习地址
另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,详情看以下视频
Linux C/C++后台开发学习资源整理,包括教学视频,文档,面试题,学习路线图