一、epoll的基礎數據結構
在開始研究源代碼之前,我們先看一下 epoll 中使用的數據結構,分別是 eventpoll、epitem 和 eppoll_entry。
1、eventpoll
我們先看一下 eventpoll 這個數據結構,這個數據結構是我們在調用 epoll_create 之后內核創建的一個句柄,表示了一個 epoll 實例。后續如果我們再調用 epoll_ctl 和 epoll_wait 等,都是對這個 eventpoll 數據進行操作,這部分數據會被保存在 epoll_create 創建的匿名文件 file 的 private_data 字段中。
* 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
};
2、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;
};
3、eppoll_entry
每次當一個 fd 關聯到一個 epoll 實例,就會有一個 eppoll_entry 產生。eppoll_entry 的結構如下:
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底層原理
在高并發場景下,如果有100萬用戶同時與一個進程保持著TCP連接,而每一時刻只有幾十個或幾百個TCP連接是活躍的(接收TCP包),也就是說在每一時刻進程只需要處理這100萬連接中的一小部分連接。
對于這種場景,select或者poll事件驅動方式采用了輪詢的方式操作系統收集有事件發生的TCP連接,把這100萬個連接告訴操作系統。但這里有一個明顯的問題,在某一時刻,進程收集有事件的連接時,其實這100萬連接中的大部分都是沒有事件發生的。因此如果每次收集事件時,都把100萬連接的套接字傳給操作系統,從用戶態內存到內核態內存的大量復制,這無疑會產生巨大的開銷。而由操作系統內核尋找這些連接上有沒有未處理的事件,將會是巨大的資源浪費,然后select和poll就是這樣做的,因此它們最多只能處理幾千個并發連接。
而epoll不這樣做,它在Linux內核中申請了一個簡易的文件系統,把原先的一個select或poll調用分成了3部分:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
- 調用epoll_create建立一個epoll對象(在epoll文件系統中給這個句柄分配資源);
- 調用epoll_ctl向epoll對象中添加這100萬個連接的套接字;
- 調用epoll_wait收集發生事件的連接。
這樣只需要在進程啟動時建立1個epoll對象,并在需要的時候向它添加或刪除連接就可以了,因此,在實際收集事件時,epoll_wait的效率就會非常高,因為調用epoll_wait時并沒有向它傳遞這100萬個連接,內核也不需要去遍歷全部的連接。
1、epoll_create
我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用于存儲以后epoll_ctl傳來的socket外,還會再建立一個rdllist雙向鏈表,用于存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個rdllist雙向鏈表里有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到后即使鏈表沒數據也返回。所以,epoll_wait非常高效。
紅黑樹操作使用的是互斥鎖,在添加和刪除操作時需要加鎖。
雙向鏈表操作使用的是spinlock自旋鎖,當沒有競爭到鎖資源時,不會睡眠,加快了鏈表操作的速度,添加和刪除操作需要加鎖。
總之,紅黑樹存儲所監控的文件描述符的節點數據,就緒鏈表存儲就緒的文件描述符的節點數據
epoll_create工作流程
首先,epoll_create 會對傳入的 flags 參數做簡單的驗證。
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
/*
接下來,內核申請分配 eventpoll 需要的內存空間。
*/
error = ep_alloc(&ep);
if (error < 0)
return error;
在接下來,epoll_create 為 epoll 實例分配了匿名文件和文件描述字,其中 fd 是文件描述字,file 是一個匿名文件。這里充分體現了 UNIX 下一切都是文件的思想。注意,eventpoll 的實例會保存一份匿名文件的引用,通過調用 fd_install 函數將匿名文件和文件描述字完成了綁定。
這里還有一個特別需要注意的地方,在調用 anon_inode_get_file 的時候,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;
2、epoll_ctl
接下來,我們看一下一個套接字是如何被添加到 epoll 實例中的。這就要解析一下 epoll_ctl 函數實現了。
查找 epoll 實例
首先,epoll_ctl 函數通過 epoll 實例句柄來獲得對應的匿名文件,這一點很好理解,UNIX 下一切都是文件,epoll 的實例也是一個匿名文件。
f = fdget(epfd);
if (!f.file)
goto error_return;
接下來,獲得添加的套接字對應的文件,這里 tf 表示的是 target file,即待處理的目標文件。
// 獲得真正的文件,如監聽套接字、讀寫套接字
tf = fdget(fd);
if (!tf.file)
goto error_fput;
再接下來,進行了一系列的數據驗證,以保證用戶傳入的參數是合法的,比如 epfd 真的是一個 epoll 實例句柄,而不是一個普通文件描述符。
// 如果不支持 poll,那么該文件描述字是無效的
error = -EPERM;
if (!tf.file->f_op->poll)
goto error_tgt_fput;
...
紅黑樹查找
接下來 epoll_ctl 通過目標文件和對應描述字,在紅黑樹中查找是否存在該套接字,這也是 epoll 為什么高效的地方。紅黑樹(RB-tree)是一種常見的數據結構,這里 eventpoll 通過紅黑樹跟蹤了當前監聽的所有文件描述字,而這棵樹的根就保存在 eventpoll 數據結構中。
對于每個被監聽的文件描述字,都有一個對應的 epitem 與之對應,epitem 作為紅黑樹中的節點就保存在紅黑樹中。
紅黑樹是一棵二叉樹,作為二叉樹上的節點,epitem 必須提供比較能力,以便可以按大小順序構建出一棵有序的二叉樹。其排序能力是依靠 epoll_filefd 結構體來完成的,epoll_filefd 可以簡單理解為需要監聽的文件描述字,它對應到二叉樹上的節點。
ep_insert
ep_insert 首先判斷當前監控的文件值是否超過了 /proc/sys/fs/epoll/max_user_watches 的預設最大值,如果超過了則直接返回錯誤。接下來是分配資源和初始化動作。
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;
}
}
總而言之,當我們使用epoll_ctl()函數注冊一個socket時,內核將會做這些事情:
- 分配一個紅黑樹節點對象epitem
- 添加等待事件到socket的等待隊列中
- 將epitem插入到epoll對象的紅黑樹中
3、epoll_wait
epoll_wait被調用時會觀察 eventpoll->rdllist 鏈表里有沒有數據,有數據就返回,沒有數據就創建一個等待隊列項,將其添加到 eventpoll 的等待隊列上(1.1節中的wait_queue_head_t),然后把自己阻塞掉就結束。
查找 epoll 實例
epoll_wait 函數首先進行一系列的檢查,例如傳入的 maxevents 應該大于 0。和前面介紹的 epoll_ctl 一樣,通過 epoll 實例找到對應的匿名文件和描述字,并且進行檢查和驗證。
還是通過讀取 epoll 實例對應匿名文件的 private_data 得到 eventpoll 實例。
/* 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;
/* 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;
4、總結
- 執行epoll_create()時,創建了紅黑樹和就緒鏈表;
- 執行epoll_ctl()時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用于當中斷事件來臨時向準備就緒鏈表中插入數據;
- 執行epoll_wait()時立刻返回準備就緒鏈表里的數據即可;
三、epoll的兩種觸發模式
epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。
LT(水平觸發)模式下,只要這個文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作;
LT比ET多了一個開關EPOLLOUT事件(系統調用消耗,上下文切換)的步驟;對于監聽的sockfd,最好使用水平觸發模式(參考nginx),邊緣觸發模式會導致高并發情況下,有的客戶端會連接不上,LT適合處理緊急事件;對于讀寫的connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,不過為了防止特殊情況,還是建議設置非阻塞;LT的編程與poll/select接近,符合一直以來的習慣,不易出錯;
ET(邊緣觸發)模式下,在它檢測到有 I/O 事件時,通過 epoll_wait 調用會得到有事件通知的文件描述符,對于每一個被通知的文件描述符,如可讀,則必須將該文件描述符一直讀到空,讓 errno 返回 EAGAIN 為止,否則下次的 epoll_wait 不會返回余下的數據,會丟掉事件。如果ET模式不是非阻塞的,那這個一直讀或一直寫勢必會在最后一次阻塞。
邊沿觸發模式很大程度上降低了同一個epoll事件被重復觸發的次數,所以效率更高;對于讀寫的connfd,邊緣觸發模式下,必須使用非阻塞IO,并要一次性全部讀寫完數據。ET的編程可以做到更加簡潔,某些場景下更加高效,但另一方面容易遺漏事件,容易產生bug;
總之,ET和LT各有優缺點,需要根據業務場景選擇最合適的模式。
四、epoll的不足之處
1.定時的精度不夠,只到5ms級別,select可以到0.1ms;
2.當連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好;
3.epoll_ctrl每次只能夠修改一個fd(kevent可以一次改多個,每次修改,epoll需要一個系統調用,不能 batch 操作,可能會影響性能);
4.可能會在定時到期之前返回,導致還需要下一個epoll_wait調用;
-
TCP
+關注
關注
8文章
1353瀏覽量
79078 -
文件
+關注
關注
1文章
566瀏覽量
24746 -
數據結構
+關注
關注
3文章
573瀏覽量
40132 -
epoll
+關注
關注
0文章
28瀏覽量
2956
發布評論請先 登錄
相關推薦
評論