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