基本數據結構
先看一下 epoll 中使用的數據結構,分別是 eventpoll、epitem 和 eppoll_entry。
eventpoll 這個數據結構,這個數據結構是我們在調用 epoll_create 之後內核側創建的一個句柄,表示了一個 epoll 實例。後續如果我們再調用 epoll_ctl 和 epoll_wait 等,都是對這個 eventpoll 數據進行操作,這部分數據會被保存在 epoll_create 創建的匿名文件 file 的 private_data 字段中。
/*
* This structure is stored inside the "private_data" member of the file
* structure and represents the main data structure for the eventpoll
* interface.
*/
struct eventpoll {
/* Protect the access to this structure */
spinlock_t lock;
/*
* This mutex is used to ensure that files are not removed
* while epoll is using them. This is held during the event
* collection loop, the file cleanup path, the epoll file exit
* code and the ctl operations.
*/
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
//這個隊列裏存放的是執行epoll_wait從而等待的進程隊列
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
//這個隊列裏存放的是該eventloop作爲poll對象的一個實例,加入到等待的隊列
//這是因爲eventpoll本身也是一個file, 所以也會有poll操作
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
//這裏存放的是事件就緒的fd列表,鏈表的每個元素是下面的epitem
struct list_head rdllist;
/* RB tree root used to store monitored fd structs */
//這是用來快速查找fd的紅黑樹
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;
//這是eventloop對應的匿名文件,充分體現了Linux下一切皆文件的思想
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
};
代碼中我提到了 epitem,這個 epitem 結構是幹什麼用的呢?
每當我們調用 epoll_ctl 增加一個 fd 時,內核就會爲我們創建出一個 epitem 實例,並且把這個實例作爲紅黑樹的一個子節點,增加到 eventpoll 結構體中的紅黑樹中,對應的字段是 rbr。這之後,查找每一個 fd 上是否有事件發生都是通過紅黑樹上的 epitem 來操作。
/*
* Each file descriptor added to the eventpoll interface will
* have an entry of this type linked to the "rbr" RB tree.
* Avoid increasing the size of this struct, there can be many thousands
* of these on a server and we do not want this to take another cache line.
*/
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 */
//將這個epitem連接到eventpoll 裏面的rdllist的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 */
//epoll監聽的fd
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
//一個文件可以被多個epoll實例所監聽,這裏就記錄了當前文件被監聽的次數
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
//當前epollitem所屬的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;
/* The structure that describe the interested events and the source fd */
struct epoll_event event;
};
每次當一個 fd 關聯到一個 epoll 實例,就會有一個 eppoll_entry 產生。eppoll_entry 的結構如下:
/* Wait structure used by the poll hooks */
struct eppoll_entry {
/* List header used to link this structure to the "struct epitem" */
struct list_head llink;
/* The "base" pointer is set to the container "struct epitem" */
struct epitem *base;
/*
* Wait queue item that will be linked to the target file wait
* queue head.
*/
wait_queue_entry_t wait;
/* The wait queue head that linked the "wait" wait queue item */
wait_queue_head_t *whead;
};
epoll_create
我們在使用 epoll 的時候,首先會調用 epoll_create 來創建一個 epoll 實例。
首先,epoll_create 會對傳入的 flags 參數做簡單的驗證。
/* Check the EPOLL_* constant for consistency. */
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
/*
內核申請分配 eventpoll 需要的內存空間。
/* Create the internal data structure ("struct eventpoll").
*/
error = ep_alloc(&ep);
if (error < 0)
return error;
在接下來,epoll_create 爲 epoll 實例分配了匿名文件和文件描述字,其中 fd 是文件描述字,file 是一個匿名文件。這裏充分體現了 UNIX 下一切都是文件的思想。注意,eventpoll 的實例會保存一份匿名文件的引用,通過調用 fd_install 函數將匿名文件和文件描述字完成了綁定。
這裏還有一個特別需要注意的地方,在調用 anon_inode_getfile 的時候,epoll_create 將 eventpoll 作爲匿名文件 file 的 private_data 保存了起來,這樣,在之後通過 epoll 實例的文件描述字來查找時,就可以快速地定位到 eventpoll 對象了。
最後,這個文件描述字作爲 epoll 的文件句柄,被返回給 epoll_create 的調用者。
/*
* 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;
epoll_ctl
接下來,看一下套接字是如何被添加到 epoll 實例中的。
查找epoll實例:
首先,epoll_ctl 函數通過 epoll 實例句柄來獲得對應的匿名文件,這一點很好理解,UNIX 下一切都是文件,epoll 的實例也是一個匿名文件。
//獲得epoll實例對應的匿名文件
f = fdget(epfd);
if (!f.file)
goto error_return;
接下來,獲得添加的套接字對應的文件,這裏 tf 表示的是 target file,即待處理的目標文件。
/* Get the "struct file *" for the target file */
//獲得真正的文件,如監聽套接字、讀寫套接字
tf = fdget(fd);
if (!tf.file)
goto error_fput;
再接下來,進行了一系列的數據驗證,以保證用戶傳入的參數是合法的,比如 epfd 真的是一個 epoll 實例句柄,而不是一個普通文件描述符。
/* The target file descriptor must support poll */
//如果不支持poll,那麼該文件描述字是無效的
error = -EPERM;
if (!tf.file->f_op->poll)
goto error_tgt_fput;
...
如果獲得了一個真正的 epoll 實例句柄,就可以通過 private_data 獲取之前創建的 eventpoll 實例了。
/*
* At this point it is safe to assume that the "private_data" contains
* our own data structure.
*/
ep = f.file->private_data;
紅黑樹查找:
epoll_ctl 通過目標文件和對應描述字,在紅黑樹中查找是否存在該套接字,這也是 epoll 爲什麼高效的地方。紅黑樹(RB-tree)是一種常見的數據結構,這裏 eventpoll 通過紅黑樹跟蹤了當前監聽的所有文件描述字,而這棵樹的根就保存在 eventpoll 數據結構中。
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
對於每個被監聽的文件描述字,都有一個對應的 epitem 與之對應,epitem 作爲紅黑樹中的節點就保存在紅黑樹中。
/*
* Try to lookup the file inside our RB tree, Since we grabbed "mtx"
* above, we can be sure to be able to use the item looked up by
* ep_find() till we release the mutex.
*/
epi = ep_find(ep, tf.file, fd);
紅黑樹是一棵二叉樹,作爲二叉樹上的節點,epitem 必須提供比較能力,以便可以按大小順序構建出一棵有序的二叉樹。其排序能力是依靠 epoll_filefd 結構體來完成的,epoll_filefd 可以簡單理解爲需要監聽的文件描述字,它對應到二叉樹上的節點。
可以看到這個還是比較好理解的,按照文件的地址大小排序。如果兩個相同,就按照文件文件描述字來排序。
struct epoll_filefd {
struct file *file; // pointer to the target file struct corresponding to the fd
int fd; // target file descriptor number
} __packed;
/* Compare RB tree keys */
static inline int ep_cmp_ffd(struct epoll_filefd *p1,
struct epoll_filefd *p2)
{
return (p1->file > p2->file ? +1:
(p1->file < p2->file ? -1 : p1->fd - p2->fd));
}
在進行完紅黑樹查找之後,如果發現是一個 ADD 操作,並且在樹中沒有找到對應的二叉樹節點,就會調用 ep_insert 進行二叉樹節點的增加。
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
error = -EEXIST;
if (full_check)
clear_tfile_check_list();
break;
ep_insert
首先判斷當前監控的文件值是否超過了 /proc/sys/fs/epoll/max_user_watches 的預設最大值,如果超過了則直接返回錯誤。
user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
接下來是分配資源和初始化動作。
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;
再接下來的事情非常重要,ep_insert 會爲加入的每個文件描述字設置回調函數。這個回調函數是通過函數 ep_ptable_queue_proc 來進行設置的。這個回調函數是幹什麼的呢?其實,對應的文件描述字上如果有事件發生,就會調用這個函數,比如套接字緩衝區有數據了,就會回調這個函數。這個函數就是 ep_poll_callback。這裏你會發現,原來內核設計也是充滿了事件回調的原理。
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.
*/
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;
}
}
ep_poll_callback
ep_poll_callback 函數的作用非常重要,它將內核事件真正地和 epoll 對象聯繫了起來。它又是怎麼實現的呢?
首先,通過這個文件的 wait_queue_entry_t 對象找到對應的 epitem 對象,因爲 eppoll_entry 對象裏保存了 wait_quue_entry_t,根據 wait_quue_entry_t 這個對象的地址就可以簡單計算出 eppoll_entry 對象的地址,從而可以獲得 epitem 對象的地址。這部分工作在 ep_item_from_wait 函數中完成。一旦獲得 epitem 對象,就可以尋跡找到 eventpoll 實例。
/*
* This is the callback that is passed to the wait queue wakeup
* mechanism. It is called by the stored file descriptors when they
* have events to report.
*/
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
接下來,進行一個加鎖操作。
spin_lock_irqsave(&ep->lock, flags);
下面對發生的事件進行過濾,爲什麼需要過濾呢?爲了性能考慮,ep_insert 向對應監控文件註冊的是所有的事件,而實際用戶側訂閱的事件未必和內核事件對應。比如,用戶向內核訂閱了一個套接字的可讀事件,在某個時刻套接字的可寫事件發生時,並不需要向用戶空間傳遞這個事件。
/*
* Check the events coming with the callback. At this stage, not
* every device reports the events in the "key" parameter of the
* callback. We need to be able to handle both cases here, hence the
* test for "key" != NULL before the event match test.
*/
if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
接下來,判斷是否需要把該事件傳遞給用戶空間。
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist;
ep->ovflist = epi;
if (epi->ws) {
/*
* Activate ep->ws since epi->ws may get
* deactivated at any time.
*/
__pm_stay_awake(ep->ws);
}
}
goto out_unlock;
}
如果需要,而且該事件對應的 event_item 不在 eventpoll 對應的已完成隊列中,就把它放入該隊列,以便將該事件傳遞給用戶空間。
/* If this file is already in the ready list we exit soon */
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
}
我們知道,當我們調用 epoll_wait 的時候,調用進程被掛起,在內核看來調用進程陷入休眠。如果該 epoll 實例上對應描述字有事件發生,這個休眠進程應該被喚醒,以便及時處理事件。下面的代碼就是起這個作用的,wake_up_locked 函數喚醒當前 eventpoll 上的等待進程。
/*
* Wake up ( if active ) both the eventpoll wait list and the ->poll()
* wait list.
*/
if (waitqueue_active(&ep->wq)) {
if ((epi->event.events & EPOLLEXCLUSIVE) &&
!((unsigned long)key & POLLFREE)) {
switch ((unsigned long)key & EPOLLINOUT_BITS) {
case POLLIN:
if (epi->event.events & POLLIN)
ewake = 1;
break;
case POLLOUT:
if (epi->event.events & POLLOUT)
ewake = 1;
break;
case 0:
ewake = 1;
break;
}
}
wake_up_locked(&ep->wq);
}
查找epoll實例
epoll_wait 函數首先進行一系列的檢查,例如傳入的 maxevents 應該大於 0。
/* The maximum number of event must be greater than zero */
if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
return -EINVAL;
/* Verify that the area passed by the user is writeable */
if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event)))
return -EFAULT;
和前面介紹的 epoll_ctl 一樣,通過 epoll 實例找到對應的匿名文件和描述字,並且進行檢查和驗證。
/* Get the "struct file *" for the eventpoll file */
f = fdget(epfd);
if (!f.file)
return -EBADF;
/*
* We have to check that the file structure underneath the fd
* the user passed to us _is_ an eventpoll file.
*/
error = -EINVAL;
if (!is_file_epoll(f.file))
goto error_fput;
還是通過讀取 epoll 實例對應匿名文件的 private_data 得到 eventpoll 實例。
/*
* At this point it is safe to assume that the "private_data" contains
* our own data structure.
*/
ep = f.file->private_data;
接下來調用 ep_poll 來完成對應的事件收集並傳遞到用戶空間。
/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout);
ep_poll
前面介紹 epoll 函數的時候,對應的 timeout 值可以是大於 0,等於 0 和小於 0 麼?這裏 ep_poll 就分別對 timeout 不同值的場景進行了處理。如果大於 0 則產生了一個超時時間,如果等於 0 則立即檢查是否有事件發生。
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
u64 slack = 0;
wait_queue_entry_t wait;
ktime_t expires, *to = NULL;
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) {
/*
* Avoid the unnecessary trip to the wait queue loop, if the
* caller specified a non blocking operation.
*/
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
goto check_events;
}
接下來嘗試獲得 eventpoll 上的鎖:
spin_lock_irqsave(&ep->lock, flags);
獲得這把鎖之後,檢查當前是否有事件發生,如果沒有,就把當前進程加入到 eventpoll 的等待隊列 wq 中,這樣做的目的是當事件發生時,ep_poll_callback 函數可以把該等待進程喚醒。
if (!ep_events_available(ep)) {
/*
* Busy poll timed out. Drop NAPI ID for now, we can add
* it back in when we have moved a socket with a valid NAPI
* ID onto the ready list.
*/
ep_reset_busy_poll_napi_id(ep);
/*
* We don't have any available event to return to the caller.
* We need to sleep here, and we will be wake up by
* ep_poll_callback() when events will become available.
*/
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
緊接着是一個無限循環, 這個循環中通過調用 schedule_hrtimeout_range,將當前進程陷入休眠,CPU 時間被調度器調度給其他進程使用,當然,當前進程可能會被喚醒,喚醒的條件包括有以下四種:
- 當前進程超時;
- 當前進程收到一個 signal 信號;
- 某個描述字上有事件發生;
- 當前進程被 CPU 重新調度,進入 for 循環重新判斷,如果沒有滿足前三個條件,就又重新進入休眠。
//這個循環裏,當前進程可能會被喚醒,喚醒的途徑包括
//1.當前進程超時
//2.當前進行收到一個signal信號
//3.某個描述字上有事件發生
//對應的1.2.3都會通過break跳出循環
//第4個可能是當前進程被CPU重新調度,進入for循環的判斷,如果沒有滿足1.2.3的條件,就又重新進入休眠
for (;;) {
/*
* We don't want to sleep if the ep_poll_callback() sends us
* a wakeup in between. That's why we set the task state
* to TASK_INTERRUPTIBLE before doing the checks.
*/
set_current_state(TASK_INTERRUPTIBLE);
/*
* Always short-circuit for fatal signals to allow
* threads to make a timely exit without the chance of
* finding more events available and fetching
* repeatedly.
*/
if (fatal_signal_pending(current)) {
res = -EINTR;
break;
}
if (ep_events_available(ep) || timed_out)
break;
if (signal_pending(current)) {
res = -EINTR;
break;
}
spin_unlock_irqrestore(&ep->lock, flags);
//通過調用schedule_hrtimeout_range,當前進程進入休眠,CPU時間被調度器調度給其他進程使用
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
}
如果進程從休眠中返回,則將當前進程從 eventpoll 的等待隊列中刪除,並且設置當前進程爲 TASK_RUNNING 狀態。
//從休眠中結束,將當前進程從wait隊列中刪除,設置狀態爲TASK_RUNNING,接下來進入check_events,來判斷是否是有事件發生
__remove_wait_queue(&ep->wq, &wait);
__set_current_state(TASK_RUNNING);
最後,調用 ep_send_events 將事件拷貝到用戶空間。
//ep_send_events將事件拷貝到用戶空間
/*
* Try to transfer events to user space. In case we get 0 events and
* there's still timeout left over, we go trying again in search of
* more luck.
*/
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;
return res;
ep_send_event
ep_send_events 這個函數會將 ep_send_events_proc 作爲回調函數並調用 ep_scan_ready_list 函數,ep_scan_ready_list 函數調用 ep_send_events_proc 對每個已經就緒的事件循環處理。
ep_send_events_proc 循環處理就緒事件時,會再次調用每個文件描述符的 poll 方法,以便確定確實有事件發生。爲什麼這樣做呢?這是爲了確定註冊的事件在這個時刻還是有效的。
可以看到,儘管 ep_send_events_proc 已經儘可能的考慮周全,使得用戶空間獲得的事件通知都是真實有效的,但還是有一定的概率,當 ep_send_events_proc 再次調用文件上的 poll 函數之後,用戶空間獲得的事件通知已經不再有效,這可能是用戶空間已經處理掉了,或者其他什麼情形。在這種情況下,如果套接字不是非阻塞的,整個進程將會被阻塞,這也是爲什麼將非阻塞套接字配合 epoll 使用作爲最佳實踐的原因。
在進行簡單的事件掩碼校驗之後,ep_send_events_proc 將事件結構體拷貝到用戶空間需要的數據結構中。這是通過 __put_user 方法完成的。
Level-triggered VS Edge-triggered
前面,我們一直都在強調 level-triggered 和 edge-triggered 之間的區別。
從實現角度來看其實非常簡單,在 ep_send_events_proc 函數的最後,針對 level-triggered 情況,當前的 epoll_item 對象被重新加到 eventpoll 的就緒列表中,這樣在下一次 epoll_wait 調用時,這些 epoll_item 對象就會被重新處理。
在前面我們提到,在最終拷貝到用戶空間有效事件列表中之前,會調用對應文件的 poll 方法,以確定這個事件是不是依然有效。所以,如果用戶空間程序已經處理掉該事件,就不會被再次通知;如果沒有處理,意味着該事件依然有效,就會被再次通知。
//這裏是Level-triggered的處理,可以看到,在Level-triggered的情況下,這個事件被重新加回到ready list裏面
//這樣,下一輪epoll_wait的時候,這個事件會被重新check
else if (!(epi->event.events & EPOLLET)) {
/*
* If this file has been added with Level
* Trigger mode, we need to insert back inside
* the ready list, so that the next call to
* epoll_wait() will check again the events
* availability. At this point, no one can insert
* into ep->rdllist besides us. The epoll_ctl()
* callers are locked out by
* ep_scan_ready_list() holding "mtx" and the
* poll callback will queue them in ep->ovflist.
*/
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
}
epoll VS poll/select
我們從實現角度來說明一下爲什麼 epoll 的效率要遠遠高於 poll/select。
首先,poll/select 先將要監聽的 fd 從用戶空間拷貝到內核空間, 然後在內核空間裏面進行處理之後,再拷貝給用戶空間。這裏就涉及到內核空間申請內存,釋放內存等等過程,這在大量 fd 情況下,是非常耗時的。而 epoll 維護了一個紅黑樹,通過對這棵黑紅樹進行操作,可以避免大量的內存申請和釋放的操作,而且查找速度非常快。
下面的代碼就是 poll/select 在內核空間申請內存的展示。可以看到 select 是先嚐試申請棧上資源, 如果需要監聽的 fd 比較多, 就會去申請堆空間的資源。
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timespec64 *end_time)
{
fd_set_bits fds;
void *bits;
int ret, max_fds;
size_t size, alloc_size;
struct fdtable *fdt;
/* Allocate small arguments on the stack to save memory and be faster */
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds)
n = max_fds;
/*
* We need 6 bitmaps (in/out/ex for both incoming and outgoing),
* since we used fdset we need to allocate memory in units of
* long-words.
*/
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
if (size > (SIZE_MAX / 6))
goto out_nofds;
alloc_size = 6 * size;
bits = kvmalloc(alloc_size, GFP_KERNEL);
if (!bits)
goto out_nofds;
}
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
...
第二,select/poll 從休眠中被喚醒時,如果監聽多個 fd,只要其中有一個 fd 有事件發生,內核就會遍歷內部的 list 去檢查到底是哪一個事件到達,並沒有像 epoll 一樣, 通過 fd 直接關聯 eventpoll 對象,快速地把 fd 直接加入到 eventpoll 的就緒列表中。
static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
...
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
bool can_busy_loop = false;
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += BITS_PER_LONG;
continue;
}
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1;
...
總之
epoll 維護了一棵紅黑樹來跟蹤所有待檢測的文件描述字,黑紅樹的使用減少了內核和用戶空間大量的數據拷貝和內存分配,大大提高了性能。同時,epoll 維護了一個鏈表來記錄就緒事件,內核在每個文件有事件發生時將自己登記到這個就緒事件列表中,通過內核自身的文件 file-eventpoll 之間的回調和喚醒機制,減少了對內核描述字的遍歷,大大加速了事件通知和檢測的效率,這也爲 level-triggered 和 edge-triggered 的實現帶來了便利。
通過對比 poll/select 的實現,我們發現 epoll 確實克服了 poll/select 的種種弊端,不愧是 Linux 下高性能網絡編程的皇冠。我們應該感謝 Linux 社區的大神們設計了這麼強大的事件分發機制,讓我們在 Linux 下可以享受高性能網絡服務器帶來的種種技術紅利。
溫故而知新 !