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