深入剖析 epoll 內核實現(Linux Kernel 2.6.11)

之前我們有講解過IO複用函數中epoll系列系統調用:
《Linux網絡編程 | IO複用 : epoll系列系統調用詳解》

本文基於 Linux Kernel 2.6.11 分析IO複用函數 epoll 的內核實現過程。

詳細分析了其中涉及到了四個重要的數據結構:struct eventpoll、struct epitem、struct epoll_event 和 struct eppoll_entry。

對epoll 內核實現機制做以簡單的框架概述,並分析各個函數的執行流程。接着詳細講解了epoll 內核實現過程,對eventpoll_init()、sys_epoll_create()、sys_epoll_ctl()、sys_epoll_wait()四個核心系統調用進行了全面而深入的剖析。



epoll 概述

epoll 是 Linux 內核爲處理大批句柄而作改進的poll,是Linux下多路複用IO接口select/poll的增強版本。它能顯著的減少程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率

因爲它會複用文件描述符集合來傳遞結果而不是迫使開發者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,另一個原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了

epoll除了提供select、poll所採用的LT模式外,還提供了ET模式,即當有事件就緒後,無論用戶此次是否將數據讀取完畢,只提醒用戶一次,這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait / epoll_pwait的調用,提高應用程序的效率。


epoll 內核實現機制綜述

epoll 使用過程中有幾個基本的函數:

  • sys_epoll_create()
  • sys_epoll_ctl()
  • sys_epoll_wait()

涉及到了四個重要的數據結構:

  • struct eventpoll
  • struct epitem
  • struct epoll_event
  • struct eppoll_entry

下面我們先來了解一下主要的數據結構,然後再對epoll內核的整體框架流程做以概述。


eventpoll 結構

struct eventpoll {
	rwlock_t lock;
	struct rw_semaphore sem;  //信號量

	/* 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;  /* 就緒隊列,存放就緒描述符*/

	/* RB-Tree root used to store monitored fd structs */
	struct rb_root rbr;
};
  • sem:該信號量用於確保在epoll使用文件時,這些文件不會被刪除
  • wq:sys_epoll_wait()使用的等待隊列 ,調用sys_epoll_wait()時,我們就是睡眠在了這個等待隊列上。
  • poll_wait:file-> poll()使用的等待隊列 , 這個用於 epollfd 被輪詢的時候。
  • rdllist:是一個雙向鏈表,其中保存着將要通過sys_epoll_wait()返回給用戶的、滿足條件的事件。具體來說,調用epoll_wait的時候,將rdllist中的epitem出列,將觸發的事件拷貝到用戶空間,之後判斷epitem是否需要重新添加回rdllist(ET/LT)。
  • rbr:紅黑樹的根節點,這棵樹中存儲着所有添加到epoll中的事件,也就是這個epoll監控的事件。具體來說,一個 fd 通過 EPOLL_ADD 添加進 epoll 後,內核會爲它生成一個對應的 epitem 結構對象(下面會講到)。epitem被添加到rbr中該結構保存了epoll監視的文件描述符

struct epitem 結構

首先來看一下 struct epitem 結構,每一個添加到 eventpoll 中的文件描述符都會對應一個 struct epitem 結構。我們的內核事件表(紅黑樹)上存放的就是此結構,也可以簡單理解爲,struct epitem即爲紅黑樹的節點。

struct epitem {
	struct rb_node rbn;
	struct list_head rdllink;
	struct epoll_filefd ffd;
	int nwait;
	struct list_head pwqlist;
	struct eventpoll *ep;
	struct epoll_event event;
	atomic_t usecnt;
	struct list_head fllink;
	struct list_head txlink;
	unsigned int revents;
};

我們介紹比較重要的幾個成員:

  • rbn:表示掛載到eventpoll 的紅黑樹節點。
  • rdllink:鏈表節點,所有已經就緒的epitem都會被鏈到 eventpoll 的 rdllist 中。
  • ffd:表示文件描述符信息,它是紅黑樹的key
  • pwqlist:list_head結構類型,也就是等待隊列鏈表。當前文件的等待隊列(eppoll_entry)列表,同一個文件上可能會監視多種事件, 這些事件可能屬於不同的wait_queue中 (取決於對應文件類型的實現),所以需要使用鏈表。
  • ep:指向其所屬的eventepoll對象
  • event:表示用戶註冊的感興趣的事件,也就是用戶空間的epoll_event。epoll_event 結構中包含要監聽的事件和對應的文件描述符

eppoll_entry 結構

eppoll_entry 與一個文件上的一個 wait_queue_head 相關聯,因爲同一文件可能有多個等待的事件,這些事件可能使用不同的等待隊列

eppoll_entry 結構是在回調函數ep_ptable_queue_proc() 中,引入的一個非常重要的數據結構。eppoll_entry主要完成epitem和epitem事件發生時的callback(ep_poll_callback)函數之間的關聯

/* Wait structure used by the poll hooks */
struct eppoll_entry {
	struct list_head llink;

	/* The "base" pointer is set to the container "struct epitem"*/
	void *base;

	/*
	 * Wait queue item that will be linked to the target file wait
	 * queue head.
	 */
	wait_queue_t wait;

	/* The wait queue head that linked the "wait" wait queue item*/
	wait_queue_head_t *whead;
};

/*
	首先將eppoll_entry的whead指向fd的設備等待隊列,然後初始化
	eppoll_entry的base變量指向epitem,最後通過add_wait_queue
	將epoll_entry掛載到fd的設備等待隊列上。完成這個動作後,
	epoll_entry已經被掛載到fd的設備等待隊列。
*/

整體流程概述

接下來我們整體概述一下 epoll 的內核實現過程:

epoll 的實現主要依賴於一個迷你文件系統:eventpollfs。此文件系統通過eventpoll_init 初始化。在初始化的過程中,使用 slab 專用高速緩存初始化了兩個結構分別是:epitem 和 eppoll_entry


首先我們通過epoll_create方法創建內核事件表,準確的表述應該是eventpoll結構體

調用sys_epoll_create()時,Linux內核會創建一個eventpoll結構體。函數傳入一個size參數,size參數只要大於0即可,沒有任何意義

sys_epoll_create() 方法中:

  • 調用 ep_getfd() 函數創建新文件索引節點inode、新的文件結構體file、新的文件描述符fd
  • 調用ep_file_init() 函數創建eventpoll結構體,並將file->private_data指定爲指向前面生成的eventpoll,這樣就把eventpoll與file文件結構體關聯起來了。最後返回文件描述符fd。

通過sys_epoll_create()生成一個eventpoll後,就可以通過sys_epoll_ctl() 提供的相關操作對eventpoll 進行ADD添加,MOD修改和DEL刪除操作

sys_epoll_ctl() 方法中:

  • 首先通過 eventpoll 的 epfd 找到關聯的 file 結構體,再通過其 private_data 成員獲取需要到操作的eventpoll
  • 然後通過ep_find確認需要操作的fd是否已經在被監視的內核事件表中(eventpoll->rbr)。
  • 最後根據op的類型分別作添加、修改、刪除的操作

sys_epoll_wait()函數的作用是獲取就緒文件描述符

sys_epoll_ctl() 方法中:

  • 首先會檢測傳入參數的合法性,包括maxevents有沒有超過範圍,events指向的空間是否可寫,epfd是否合法等。
  • 參數合法性檢測都通過後,將通過fget函數獲取到epfd所對應的struct file,然後通過file->private_data獲取到eventpoll
  • 最後調用 ep_poll() 函數完成真正的sys_epoll_wait工作。 關於ep_poll() 函數的具體實現我們在後面進行詳細解析。

epoll 內核實現的具體分析

eventpoll_init()解析

首先我們來分析eventpoll_init()初始化函數,當系統啓動時,epoll進行初始化:

  • 調用 ep_poll_safewake_init() 函數初始化 poll_safewake 結構。
  • 使用kmem_cache_create調用爲 struct epitem 結構建立slab專用高速緩存注意這個函數僅僅是建立專用高速緩存,並沒有向緩存分配任何內存。
  • 使用kmem_cache_create調用爲 eppoll_entry 結構建立slab專用高速緩存
  • 調用 register_filesystem() 函數爲註冊一個名爲 eventpollfs 新的文件系統然後掛載此文件系統,eventpollfs 在eventpoll_fs_type 結構中定義。

sys_epoll_create()解析

sys_epoll_create()函數負責創建一個eventpollfs文件系統的inode節點由ep_getfd()函數完成

sys_epoll_create()的函數原型如下:

asmlinkage long sys_epoll_create(int size)

size只起參考作用,並沒有實際用到,函數中只是對size值做了一次判斷,我們只要保證size的值大於零即可

函數執行流程:

  • 首先定義文件描述符fd、inode文件節點指針、struct file 結構指針
  • 調用 ep_getfd() 函數創建新文件索引節點inode、新的文件結構體file、新的文件描述符fd
  • 調用ep_file_init() 創建eventpoll結構體,並將file->private_data指定爲指向前面生成的eventpoll,這樣就把eventpoll與file文件結構體關聯起來了
  • 最後返回文件描述符fd

接下來我們分別來分析一下 ep_getfd()函數ep_file_init()函數


ep_getfd() 函數原型如下:

static int ep_getfd(int *efd, 
struct inode **einode, struct file **efile)

在 ep_getfd() 函數中:

  • 通過 get_empty_filp() 函數 獲得一個未使用的文件緩存空間即file結構體
  • 通過 ep_eventpoll_inode()函數 分配inode節點空間以及初始化
  • 通過 get_unused_fd() 函數 獲得一個未使用的文件描述符
  • 接下來是對 file 結構體的一系列初始化工作
  • 調用 fd_install() 函數,它以文件描述符fd爲索引,將當前文件描述符和上述的 file結構體關聯在一起
  • 最後將上述創建的文件描述符fd、inode文件節點指針、struct file 結構指針賦值給傳遞進來的參數,也就是在sys_epoll_create()的三個文件參數,即通過此函數將它們進行了修改

ep_file_init() 函數原型如下:

static int ep_file_init(struct file *file)
  • 函數首先創建一個eventpoll結構類型指針ep
  • 使用 kmalloc 通用高速緩存爲 eventpoll 分配空間,因爲 kmalloc參數指定從內核開闢空間的大小,這裏參數爲 sizeof(struct eventpoll) 。
  • 接下來是對eventpoll結構進行一系列的初始化工作
  • 最後使用 file->private_data = ep; 則將 eventpoll 與 file文件結構體關聯起來

上面則是在sys_epoll_create()中最爲重要的兩個函數調用,結束後,返回創建好的文件描述符fd。


sys_epoll_ctl()解析

通過sys_epoll_create()生成一個eventpoll後,就可以通過sys_epoll_ctl() 提供的相關操作對 eventpoll 進行ADD添加,MOD修改和DEL刪除操作

函數的原型如下:

asmlinkage long
sys_epoll_ctl(int epfd, int op, int fd, 
struct epoll_event __user *event)
  • epfd:該參數就是sys_epoll_create()的創建的fd,可以通過該fd找到對應的文件結構體file,然後通過file->private_data便可找到我們要操作的eventpoll
  • op:表示操作類型,函數根據其值進行匹配執行相應的操作,op值對應的操作如下:
#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2
#define EPOLL_CTL_MOD 3
  • fd:這個 fd 表示的是需要被監視的文件描述符,即我們要操作的文件描述符。
  • event:表示被監視描述符上的相關event

程序執行流程:

  • 定義file結構體指針 file、tfile
  • 通過 fget() 函數 獲取描述符 epfd 所對應的 file 結構體並賦值給 file
  • 通過 fget() 函數 獲取描述符 fd 所對應的 file 結構體並賦值給 tfile
  • ep = file->private_data; 執行後 ep 便得到了 eventpoll
  • 通過 ep_find() 函數確認需要操作的 fd 是否已經在被監視的內核事件表中(eventpoll->rbr)
  • 最後根據 op的值,進行不同的操作
    EPOLL_CTL_ADD -> ep_insert()
    EPOLL_CTL_DEL -> ep_remove()
    EPOLL_CTL_MOD -> ep_modify()

接下來我們主要分析一下 ep_insert() 函數的實現。

ep_insert() 的作用是將用戶關心的文件描述符fd插入到內核事件表中,內核事件表即爲 eventpoll->rbr 所對應的紅黑樹。rbr爲紅黑樹的根節點,這棵樹中存儲着所有添加到epoll中的事件,也就是這個epoll監控的事件

執行的整體流程如下:

  • 把當前文件描述符及其對應的事件(fd,epoll_event)加入紅黑樹,便於內核管理。
  • 註冊設備驅動poll的回調函數ep_ptable_queue_proc,當調用f_op->poll()時,最終會調用該回調函數ep_ptable_queue_proc()。
  • 在ep_ptable_queue_proc回調函數中,註冊回調函數ep_poll_callbackep_poll_callback表示當描述符fd上相應的事件發生時該如何告知進程
  • 在ep_ptable_queue_proc回調函數中,檢測是文件描述符fd對應的設備的epoll_event事件是否發生,如果發生則把fd及其epoll_event加入上面提到的就緒隊列rdlist中

ep_insert() 函數原型如下:

static int ep_insert(struct eventpoll *ep, 
struct epoll_event *event, struct file *tfile, int fd)

ep_insert() 的參數中的ep、event、fd 和 sys_epoll_ctl()函數所對應的參數相同tfile 即爲sys_epoll_ctl()中所定義的tfile,即爲 fd 所對應的文件結構體

  • ep_insert() 首先調用EPI_MEM_ALLOC使用kmem_cache_alloc()從slab中分配一個 epitem 結構的對象epi
  • 接下來對 epi 進行一些初始化工作,例如初始化rdllink指向eventpoll的rdlist,初始化 pwqlist 指向包含此 epitem 的所有 poll wait queue等。
  • 接下來是一些賦值操作,將epitem的ep指針,指向傳入的eventpoll,並通過傳入參數event對ep內部變量event賦值。
  • 通過 EP_SET_FFD() 函數將目標文件和 epitem 關聯。這樣epitem本身就完成了和eventpoll以及被監視文件的關聯。
  • 接下來使用init_poll_funcptr()函數將epitem插入目標文件的polllist並註冊回調函數,這需要藉助一個新的數據結構ep_pqueue,定義如下:
/* Wrapper struct used by poll queueing */
struct ep_pqueue {
	poll_table pt;
	struct epitem *epi;
};

// poll_table 結構定義如下
typedef struct poll_table_struct {
	poll_queue_proc qproc;
} poll_table;

// 上述poll_queue_proc 函數指針的定義如下
typedef void (*poll_queue_proc)(struct file *, 
wait_queue_head_t *, struct poll_table_struct *);

// 程序中通過下述語句將ep_pqueue 結構與 epitem關聯起來
epq.epi = epi;

根據定義我們知道ep_pqueue主要包含兩個變量:一個是epitem另一個是callback函數(ep_ptable_queue_proc)相關的一個數據結構poll_table

init_poll_funcptr()函數的定義及相關結構如下:

// init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

static inline void 
init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
	pt->qproc = qproc;
}

ep_pqueue主要完成epitem和callback函數的關聯。然後通過目標文件的poll函數調用callback函數ep_ptable_queue_proc。poll函數一般由設備驅動提供

ep_ptable_queue_proc()函數將epitem 加入到特定文件的等待隊列

函數原型如下:

static void ep_ptable_queue_proc
(struct file *file, wait_queue_head_t *whead, poll_table *pt)

ep_ptable_queue_proc有三個參數:

  • file:目標文件的file結構體
  • whead:目標文件的waitlist等待隊列
  • pt:前面生成的poll_table

在函數中,引入了另外一個非常重要的數據結構eppoll_entry。eppoll_entry主要用於處理 epitem 和 epitem 事件發生時的callback函數之間的關聯。

  • 首先將eppoll_entry的whead指向fd的設備等待隊列
  • 然後初始化eppoll_entry的base變量指向epitem
  • 通過add_wait_queue()函數將epoll_entry掛載到目標文件fd的設備等待隊列waitlist上
  • 最後通過list_add_tail()函數將eppoll_entry掛載到epitem的pwqlist上面。

接下來我們繼續回到ep_insert()函數中:

revents = tfile->f_op->poll(tfile, &epq.pt);

上述語句其實就是調用被監控文件的poll方法,而這個poll其實就是調用poll_wait(每個支持poll的設備驅動程序都要調用的),最後就是調用ep_ptable_queue_proc

接下來就是將epitem 的 fllink 鏈接到目標文件的 f_ep_links上,這部分工作將在poll函數返回後在ep_insert()中完成

然後將epitem插入到eventpoll的rbtree中:

ep_rbtree_insert(ep, epi);

完成以上動作後,將還會判斷當前插入的event是否剛好發生,如果是,那麼做一個ready動作,將epitem加入到rdlist中,並對epoll上的wait隊列調用wakeup


上面我們介紹了epoll_ctl()以及ep_insert()函數的基本實現。

下面介紹epoll_wait()函數。


sys_epoll_wait()解析

sys_epoll_wait() 是 epoll_wait() 對應的系統調用,主要用來獲取文件狀態已經就緒的事件

函數的原型如下:

asmlinkage long sys_epoll_wait(int epfd, 
struct epoll_event __user *events,int maxevents, int timeout)
  • maxevents:指定最多監聽多少個事件
  • events:檢測到事件,將所有就緒的事件從內核事件表中複製到events指向的數組中
  • timeout:指定超時時間,單位是毫秒。當timeout爲-1是,sys_epoll_wait調用將永遠阻塞,直到某個事件發生。當timeout爲0時,sys_epoll_wait調用將立即返回。

函數執行流程:

  • 首先會檢測傳入參數的合法性,包括maxevents有沒有超過範圍,events指向的空間是否可寫,epfd是否合法等。
  • 參數合法性檢測都通過後,將通過fget函數獲取到epfd所對應的struct file,然後通過file->private_data獲取到eventpoll
  • 獲取到後調用 ep_poll() 函數完成真正的epoll_wait工作。 關於ep_poll() 函數我們接下來進行詳細解析。

接下來我們重點分析一下ep_poll() 函數

static int ep_poll(struct eventpoll *ep,
struct epoll_event __user *events,int maxevents, long timeout)

函數的參數與sys_epoll_wait()是一一對應、完全相同的

ep_poll() 執行流程如下【概述】:

  • 當rdlist爲空(無就緒fd)時掛起當前進程,直到rdlist不空時進程才被喚醒
  • 文件fd狀態改變導致相應fd上的回調函數ep_poll_callback()被調用
  • ep_poll_callback 將相應fd對應epitem加入rdlist,導致rdlist不空,進程被喚醒,epoll_wait得以繼續執行
  • ep_events_transfer 函數將rdlist中的epitem拷貝到txlist中,並將rdlist清空
  • ep_send_events函數非常重要,它掃描txlist中的每個epitem,調用其關聯fd對用的poll方法。此時對poll的調用僅僅是取得fd上較新的events(防止之前events被更新),之後將取得的events和相應的fd發送到用戶空間(封裝在struct epoll_event,從epoll_wait返回)。

ep_poll() 具體分析:

ep_poll首先根據timeout的值判斷是否是無限等待

然後判斷eventpoll的rdlist是否爲空

如果爲空:

  • 調用 init_waitqueue_entry() 函數將current 進程關聯到 wait_queue_t 類型
  • 調用 add_wait_queue() 函數將 current 進程加入到 eventpoll 的 waitlist(wq) 等待隊列中
  • 接着在一個死循環中重複檢測rdlist上是否有數據或者超時
  • 這個循環中進行狀態設置、數據檢測等工作,通過set_current_state()將task的狀態設置爲TASK_INTERRUPTIBLE,表示可中斷的,並通過schedule_timeout讓出處理器。
  • 如果檢測到rdlist上有數據了或者超時時間到了,那麼結束循環,設置狀態爲TASK_RUNNING,並將current進程通過remove_wait_queue()移除出等待隊列。

如果非空:

  • 這部分語句沒有寫在else中,因此有可能是剛開始rdlist就不爲空,也有可能是剛開始爲空,然後在上面的for循環中跳出了
  • 因此需要再次判斷rdlist是否爲空,因爲若是從上面for循環跳出的,那麼也有可能是超時時間到了,返回 0
  • 調用ep_events_transfer()函數執行了將數據拷貝到用戶空間等操作,最後返回拷貝到用戶空間文件描述符的數量

接下來我們分析一下ep_events_transfer()函數:

static int ep_events_transfer(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)

ep_events_transfer函數將rdlist中的epitem拷貝到txlist中,並將rdlist清空

  • 調用 ep_collect_ready_items()函數 從rdlist中收集最多maxevent個元素到txlist。每次向txlist拷貝一個元素,便從rdlist中將其刪除,最終rdlist被清空。
  • 調用 ep_send_events()函數 將有事件就緒的描述符拷貝給用戶空間,實際返回給用戶實際拷貝的數目
  • 調用 ep_reinject_items()函數判斷epitem->event.events是否設置了ET模式,若沒有設置ET模式,就將epitem返還給rdlist(LT模式)
if (EP_RB_LINKED(&epi->rbn) && 
!(epi->event.events & EPOLLET) &&
(epi->revents & epi->event.events) 
&& !EP_IS_LINKED(&epi->rdllink)) 
{
	list_add_tail(&epi->rdllink, &ep->rdllist);
	ricnt++;
}

接下來我們分析ep_send_events()函數:

static int ep_send_events(struct eventpoll *ep,
struct list_head *txlist,struct epoll_event __user *events)
  • 通過list_for_each()來遍歷txlist
  • 檢查該描述符上所有的數據就緒事件
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL); 
  • 接下來只拿到用戶關心的事件(註冊事件)
epi->revents = revents & epi->event.events; 
  • 通過判斷epi->revents,若存在就緒事件,則向用戶空間進行拷貝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章