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更好