前言
epoll
同樣是linux上的IO多路復用的一種實現,內核在實現時使用的數據結構相比select
要復雜,但原理上并不復雜,我們力求在下面的描述里抽出主干,理清思路。
epoll
也利用了上文中介紹過的Linux中的重要數據結構 wait queue
, 有了上面select
的基礎,其實epoll
就沒那么復雜了。
通過閱讀本文 ,你除了可以了解到epoll的原理外,還可以搞清epoll存不存在驚群問題,LT和 ET模式在實現上有什么 區別,epoll和select相比有什么不同, epoll是如何處理多核并發的等等問題 。當然內容難免有疏漏之處,請大家多多指證。
主要數據結構
eventpoll
epoll操作最重要的數據結構,封裝了所有epoll操作涉及到的數據結構:
struct eventpoll {
// 用于鎖定這個eventpoll數據結構,
// 在用戶空間多線程操作這個epoll結構,比如調用epoll_ctl作add, mod, del時,用戶空間不需要加鎖保護
// 內核用這個mutex幫你搞定
struct mutex mtx;
// 等待隊列,epoll_wait時如果當前沒有拿到有效的事件,將當前task加入這個等待隊列后作進程切換,等待被喚醒
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
// eventpoll對象在使用時都會對應一個struct file對象,賦值到其private_data,
// 其本身也可以被 poll, 那也就需要一個wait queue
wait_queue_head_t poll_wait;
// 所有有事件觸發的被監控的fd都會加入到這個列表
struct list_head rdllist;
/* Lock which protects rdllist and ovflist */
rwlock_t lock;
// 所有被監控的fd使用紅黑樹來存儲
struct rb_root_cached rbr;
// 當將ready的fd復制到用戶進程中,會使用上面的 lock鎖鎖定rdllist,
// 此時如果有新的ready狀態fd, 則臨時加入到 ovflist表示的單鏈表中
struct epitem *ovflist;
// 會autosleep準備的喚醒源
struct wakeup_source *ws;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
// linux下一切皆文件,epoll實例被創建時,同時會創建一個file, file的private_data
// 指向當前這個eventpoll結構
struct file *file;
/* used to optimize loop detection check */
int visited;
struct list_head visited_list_link;
#ifdef CONFIG_NET_RX_BUSY_POLL
/* used to track busy poll napi_id */
unsigned int napi_id;
#endif
};
我們將 上面結構體中的 poll_wait
單提出來說一下,正如注釋中所說的,有了這個成員變量,那這個eventpoll對應的struct file也可以被poll,那我們也就可以將這個 epoll fd 加入到另一個epoll fd中,也就是實現了epoll的嵌套。
另外,在下面的講解中我們暫時不涉及epoll嵌套的問題。
epitem
由上面的介紹我們知道每一個被 epoll
監控的句柄都會保存在eventpoll
內部的紅黑樹上(eventpoll->rbr
),ready狀態的句柄也會保存在eventpoll
內部的一個鏈表上(eventpoll->rdllist
), 實現時會將每個句柄封裝在一個結構中,即epitem
:
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;
};
// 用于將當前epitem鏈接到eventpoll->rdllist中
struct list_head rdllink;
//用于將當前epitem鏈接到"struct eventpoll"->ovflist這個單鏈表中
struct epitem *next;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
// 對應的eventpoll對象
struct eventpoll *ep;
/* List header used to link this item to the "struct file" items list */
struct list_head fllink;
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu *ws;
// 需要關注的讀,寫事件等
struct epoll_event event;
};
epoll_event
調用epoll_ctl
時傳入的最后一個參數,主要是用來告訴內核需要其監控哪些事件。我們先來看其定義
-
在kernel源碼中的定義:
struct epoll_event { __poll_t events; __u64 data; } EPOLL_PACKED;
-
在glic中的定義:
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ } __EPOLL_PACKED;
乍一看,為什么這兩種定義不一樣,這怎么調用啊?
我們先來看下glic中的定義,它將
epoll_event.data
定義為epoll_data_t
類型,而epoll_data_t
被定義為union
類型,其能表示的最大值類型為uinit64_t
,這與kernel源碼中的定義__u64 data
是一致的,其實這個data
成員變量部分kernel在實現時根本不會用到,它作為user data在epoll_wait
返回時通過epoll_event
原樣返回到用戶空間,聲明成union
對使用者來說自由發揮的空間就大多了,如果使用fd
,你可以把當前要監控的socket fd賦值給它,如果使用void* ptr
,那你可以將任意類型指針給它......
主要函數
epoll_create
創建一個epoll的實例,Linux里一切皆文件,這里也不例外,返回一個表示當前epoll實例的文件描述符,后續的epoll相關操作,都需要傳入這個文件描述符。
其實現位于 fs/eventpoll.c
里 SYSCALL_DEFINE1(epoll_create, int, size)
, 具體實現 static int do_epoll_create(int flags)
:
static int do_epoll_create(int flags)
{
int error, fd;
struct eventpoll *ep = NULL;
struct file *file;
/* Check the EPOLL_* constant for consistency. */
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
// 目前flags只支持 EPOLL_CLOEXEC 這一種,如果傳入了其他的,返回錯誤
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
/*
* Create the internal data structure ("struct eventpoll").
*/
error = ep_alloc(&ep);
if (error < 0)
return error;
/*
* Creates all the items needed to setup an eventpoll file. That is,
* a file structure and a free file descriptor.
*/
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
if (fd < 0) {
error = fd;
goto out_free_ep;
}
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
O_RDWR | (flags & O_CLOEXEC));
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto out_free_fd;
}
ep->file = file;
fd_install(fd, file);
return fd;
out_free_fd:
put_unused_fd(fd);
out_free_ep:
ep_free(ep);
return error;
}
主要分以下幾步:
校驗傳入參數
flags
, 目前僅支持EPOLL_CLOEXEC
一種,如果是其他的,立即返回失敗;調用
ep_alloc
, 創建eventpoll
結構體;在當前task的打開文件打描述符表中獲取一個fd;
-
使用
anon_inode_getfile
創建一個 匿名inode的struct file
, 其中會使用file->private_data = priv
將第二步創建的eventpoll
對象賦值給struct file
的private_data
成員變量。關于
匿名inode
作者也沒有找到太多的資料,可以簡單理解為其沒有對應的dentry
, 在目錄下ls
看不到這類文件 ,其被close
后會自動刪除,比如 使用O_TMPFILE
選項來打開的就是這類文件; 將第三步中的
fd
和第四步中的struct file
結合起來,放入當前task的打開文件描述符表中;
epoll_ctl
從一個fd添加到一個eventpoll中,或從中刪除,或如果此fd已經在eventpoll中,可以更改其監控事件。
我們在下面的源碼中添加了必要的注釋:
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
int error;
int full_check = 0;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi;
struct epoll_event epds;
struct eventpoll *tep = NULL;
error = -EFAULT;
// ep_op_has_event() 其實就是判斷當前的op不是 EPOLL_CTL_DEL操作,
// 如果 是EPOLL_CTL_ADD 或 EPOLL_CTL_MOD,
// 將event由用戶態復制到內核態
//
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
goto error_return;
error = -EBADF;
f = fdget(epfd);
if (!f.file)
goto error_return;
/* Get the "struct file *" for the target file */
tf = fdget(fd);
if (!tf.file)
goto error_fput;
// 被添加的fd必須支持poll方法
error = -EPERM;
if (!file_can_poll(tf.file))
goto error_tgt_fput;
/*
Linux提供了autosleep的電源管理功能
如果當前系統支持 autosleep功能,支持休眠,
那么我們 允許用戶傳入EPOLLWAKEUP標志;
如果當前系統不支持這樣的電源管理功能,但用戶還是傳入了EPOLLWAKEUP標志,
那么我們將此標志從flags中去掉
*/
if (ep_op_has_event(op))
ep_take_care_of_epollwakeup(&epds);
error = -EINVAL;
// epoll不能自己監控自己
if (f.file == tf.file || !is_file_epoll(f.file))
goto error_tgt_fput;
/*
EPOLLEXCLUSIVE是為了解決某個socket有事件發生時的驚群問題
所謂驚群,簡單講就是把一個socket fd加入到多個epoll中時,如果此socket有事件發生,
會同時喚醒多個在此socket上等待的task
目 前僅允許在EPOLL_CTL_ADD操作時傳入EPOLLEXCLUSIVE標志,且傳入此標志時不允許
epoll嵌套監聽
*/
if (ep_op_has_event(op) && (epds.events & EPOLLEXCLUSIVE)) {
if (op == EPOLL_CTL_MOD)
goto error_tgt_fput;
if (op == EPOLL_CTL_ADD && (is_file_epoll(tf.file) ||
(epds.events & ~EPOLLEXCLUSIVE_OK_BITS)))
goto error_tgt_fput;
}
/*
* At this point it is safe to assume that the "private_data" contains
* our own data structure.
*/
ep = f.file->private_data;
/*
這里處理將一個epoll fd添加到當前epoll的嵌套情況,
特別是要檢測是否有環形epoll監聽情況,類似于A監聽B, B又監聽A
我們先略過
*/
// 查看對應的epitme是否已經在紅黑樹上存在,即是否已經添加過
epi = ep_find(ep, tf.file, fd);
error = -EINVAL;
switch (op) {
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= EPOLLERR | EPOLLHUP;
// 將當前fd加入紅黑樹,我們在下面重點講
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
error = -EEXIST;
if (full_check)
clear_tfile_check_list();
break;
case EPOLL_CTL_DEL:
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD:
if (epi) {
if (!(epi->event.events & EPOLLEXCLUSIVE)) {
epds.events |= EPOLLERR | EPOLLHUP;
error = ep_modify(ep, epi, &epds);
}
} else
error = -ENOENT;
break;
}
......
return error;
}
這個函數主要作以下幾件事:
-
先將epoll_event(上面已有介紹,保存著需要監控的事件)從用戶空間復制到內核空間。
我們看來針對某個socket, 這種用戶空間到內核空間的復制只需一次,不像
select
,每次調用都要復制; 先由傳入的epoll fd和被監聽的socket fd獲取到其對應的文件句柄
struct file
,針對文件句柄和傳入的flags作邊界條件檢測;針對epoll嵌套用法,作單獨檢測,檢測是否有環形epoll監聽情況,類似于A監聽B, B又監聽A, 這部分我們先略過;
針對
EPOLL_CTL_ADD
EPOLL_CTL_DEL
EPOLL_CTL_MOD
分別作處理。
ep_insert
這個函數是真正將待監聽的fd加入到epoll中去。下面我們將這個函數的實現拆解,分段來看一下其是如何實現的。
-
作max_user_watches檢驗
user_watches = atomic_long_read(&ep->user->epoll_watches); if (unlikely(user_watches >= max_user_watches)) return -ENOSPC;
內核對系統中所有(是所有,所有使用了epoll的進程)使用
epoll
監聽fd所消耗的內存作了限制, 且這個限制是針對當前linux user id的。32位系統為了監控注冊的每個文件描述符大概占90字節,64位系統上占160字節。可以通過
/proc/sys/fs/epoll/max_user_watches
來查看和設置 。默認情況下每個用戶下
epoll
為注冊文件描述符可用的內存是內核可使用內存的1/25。 -
初始化
epitem
這個
epitem
前面說過,它會被掛在epoll的紅黑樹上。if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL))) return -ENOMEM; /* Item initialization follow here ... */ INIT_LIST_HEAD(&epi->rdllink); INIT_LIST_HEAD(&epi->fllink); INIT_LIST_HEAD(&epi->pwqlist); epi->ep = ep; ep_set_ffd(&epi->ffd, tfile, fd); epi->event = *event; epi->nwait = 0; epi->next = EP_UNACTIVE_PTR; if (epi->event.events & EPOLLWAKEUP) { error = ep_create_wakeup_source(epi); if (error) goto error_create_wakeup_source; } else { RCU_INIT_POINTER(epi->ws, NULL); }
a. 這個epitem里會保存監控的fd及其事件,所屬的eventpoll等;
b. 如果events里設置了EPOLLWAKEUP, 還需要為autosleep創建一個喚醒源
ep_create_wakeup_source
。 -
獲取當前被監聽的fd上是否有感 興趣的事件發生,同時生成新的
eppoll_entry
對象并添加到被監聽的socket fd的等待隊列中epq.epi = epi; init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); revents = ep_item_poll(epi, &epq.pt, 1);
下面是
ep_item_poll
的實現:static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt, int depth) { struct eventpoll *ep; bool locked; pt->_key = epi->event.events; if (!is_file_epoll(epi->ffd.file)) return vfs_poll(epi->ffd.file, pt) & epi->event.events; }
如果你讀過上面的
select
分析部分,就會看到一個熟悉的身影vfs_poll
, 它會調用ep_ptable_queue_proc
將當前被監聽的socket fd加入到等待隊列中:static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) { struct epitem *epi = ep_item_from_epqueue(pt); struct eppoll_entry *pwq; if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) { init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); pwq->whead = whead; pwq->base = epi; if (epi->event.events & EPOLLEXCLUSIVE) add_wait_queue_exclusive(whead, &pwq->wait); else add_wait_queue(whead, &pwq->wait); list_add_tail(&pwq->llink, &epi->pwqlist); epi->nwait++; } else { /* We have to signal that an error occurred */ epi->nwait = -1; } }
這里有兩點比較重要:
a.
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
如果這個被監聽的socket上有事件發生,這個回調ep_poll_callback
將被調用, 我們后面會講這個回調里作了哪些事情, 這個回調很重要;b. 如果設置了
EPOLLEXCLUSIVE
, 將使用add_wait_queue_exclusive
添加到等待隊列。意思是說,如果一個socket fd被添加到了多個epoll中進行監控,設置了這個參數后,這個fd上有事件發生時,只會喚醒被添加到的第一個epoll里,避免驚群。 -
添加到 epoll的紅黑樹上
ep_rbtree_insert(ep, epi);
-
如果上面調用
ep_item_poll
時,立即返回了準備好的事件,我們這里要作喚醒的操作if (revents && !ep_is_linked(epi)) { list_add_tail(&epi->rdllink, &ep->rdllist); ep_pm_stay_awake(epi); /* Notify waiting tasks that events are available */ if (waitqueue_active(&ep->wq)) wake_up(&ep->wq); if (waitqueue_active(&ep->poll_wait)) pwake++; } ..... if (pwake) ep_poll_safewake(&ep->poll_wait);
a. 將當前 epi加入到eventpoll的rdllist中;
b. 如果當前eventpoll處于wait狀態,就喚醒它;
c. 如果當前的eventpoll被嵌套地加入到了另外的poll中,且處于wait狀態,就喚醒它。
ep_poll_callback
被監聽的socket fd上有事件發生時,這個回調被觸發, 然后喚醒epoll_wait
被調用時加入到eventpoll等待隊列中的task,下面會放張圖來解釋其功能。
ep_events_available
我們首先來看一下函數ep_events_available
,它的功能是檢測當前epoll上是否已經收集了有效的事件:
static inline int ep_events_available(struct eventpoll *ep)
{
return !list_empty_careful(&ep->rdllist) ||
READ_ONCE(ep->ovflist) != EP_UNACTIVE_PTR;
}
按這個邏輯只有rdllist不為空或者ovflist != EP_UNACTIVE_PTR,那么就有有效的事件,前一個條件好理解,ovflist這個我們先在這里埋個坑,后面我們來填它~
ep_poll
這個函數是epoll_wait
在內核里的具體實現。我們把它的實現分解來看。
-
準備好超時時間
if (timeout > 0) { struct timespec64 end_time = ep_set_mstimeout(timeout); slack = select_estimate_accuracy(&end_time); to = &expires; *to = timespec64_to_ktime(end_time); } else if (timeout == 0) { timed_out = 1; write_lock_irq(&ep->lock); eavail = ep_events_available(ep); write_unlock_irq(&ep->lock); goto send_events; }
a. 如果用戶設置了超時時間, 作相應的初始化;
b. 如果timeout == 0, 表時此次調用立即返回, 此時首先獲取當前是否已有有效的事件ready, 然后goto 到send_events, 這部分是將有效的events復制到用戶空間,我們后面會詳述。
-
將當前task加入到此eventpoll的等待隊列中
if (!waiter) { waiter = true; init_waitqueue_entry(&wait, current); spin_lock_irq(&ep->wq.lock); __add_wait_queue_exclusive(&ep->wq, &wait); spin_unlock_irq(&ep->wq.lock); }
我們前面在
select
部分已經介紹過wait queue, 這里就是將當前task加入到eventpoll的等待隊列,接下來當前task將會被調度走,然后等待從eventpoll的等待隊列中被喚醒。這里用了__add_wait_queue_exclusive
, 是說針對同一個eventpoll
, 可能在不同的進程(線程)調用epoll_wait
, 此時eventpoll的等待隊列里將會有多個task, 為避免驚群,我們每次只喚醒一個task。 -
無限循環體
for (;;) { set_current_state(TASK_INTERRUPTIBLE); if (fatal_signal_pending(current)) { res = -EINTR; break; } eavail = ep_events_available(ep); if (eavail) break; if (signal_pending(current)) { res = -EINTR; break; } if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) { timed_out = 1; break; } }
這個無限循環體退出的條件:
a. 有signal發生,被中斷會退出;
b. 有ready的事件,會退出;
c. 用戶設置的超時時間到達,會退出;
否則當前 task將被
schedule_hrtimeout_range
調度走。 -
有ready的事件復制到用戶空間
if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && !timed_out) goto fetch_events;
ep_poll
和ep_poll_callback
的處理有些復雜,我下面用張圖來說明一下:
幾點說明如下:
實際中可能同時有多個socket有事件到來,此時
ep_poll_callback
會并發被調用,因此將epi添中到eventpoll->rdllik時,均采用原子操作;-
ep_scan_ready_list
中一旦開始向用戶空間復制events, eventpoll->rdllink就不能再有新的添加,此時如果ep_poll_callback
被調用,當前的epi會被添加到eventpoll->ovflist中, ovflist是個單鏈表,這個添加操作很有意思,每次新的epi都被原子添加到鏈接頭:static inline bool chain_epi_lockless(struct epitem *epi) { struct eventpoll *ep = epi->ep; /* Check that the same epi has not been just chained from another CPU */ if (cmpxchg(&epi->next, EP_UNACTIVE_PTR, NULL) != EP_UNACTIVE_PTR) return false; /* Atomically exchange tail */ epi->next = xchg(&ep->ovflist, epi); return true; }
-
ep_send_events_proc
才是真正實現將events復制到用戶空間。雖然當socket fd有事件到來時,會通過
ep_poll_callback
來喚醒epoll_wait所在的task, 后者遍歷rdllist即可,但在遍歷時,還是通過ep_item_poll
(內部會調用vs_poll, 最終調用到tcp_poll)來獲取關注的事件是否發生,所有poll機制很重要; -
對于水平觸發方式,在首次調用
ep_item_poll
后,會再次將這個epi
加入到eventpoll->rdllist這個就緒列表中,這會導致兩種情況出現:a. 如果針對同一個eventpoll同時調用了多個 epoll_wait, 此時另一個調用epoll_wait的task將被喚醒,這不能被稱之為
epoll_wait
的驚群,反而是并發處理的體現;b. 如果只有一個epoll_wait, 那下次這個epoll_wait再次被調用時,不會進入到上面的
無限循化
邏輯,也不會被調度走,而是直接又一次進入到ep_send_events
中,直到在這個socket fd上poll不到關注的事情,它就不會再被加入到rdllist中。你可以將這個水平觸發方式理解成是完全輪詢的一種實現;聰明的你讀到這里一定會發現對于水平觸發,即使是socket fd上已經沒有關注的事件發生了,它還是要多用一次poll來確認,這是一處性能損失的點,但監聽的socket少的話這也不是什么大問題。
總結
這里先講上點上面沒有提及的內容
- epoll模型中ep_poll執行時如果當前沒有有效的events,當前task會被調度走,后續有socket fd有事件發生,ep_poll_callback被調用,將當前的socket fd 添加到rdllist中,再喚醒前面的task, 然后ep_poll再一次被調度執行,鎖定住rdllist后開始向用戶空間復制,由次可以看出來每次epoll_wait返回的events就是從第一次ep_poll_callback調用執行喚醒到ep_poll所有task被真正喚醒開始執行這段時間內,所收集中的socket fd。如果同時有大量的socket fd是活躍狀態,那么這里可能需要多次調用epoll_wait,效率上是個問題;