一文帶你了解Linux內(nèi)核epoll實(shí)現(xiàn)原理與機(jī)制!
一、epoll_create()
系統(tǒng)調(diào)用epoll_create()會創(chuàng)建一個epoll實(shí)例并返回該實(shí)例對應(yīng)的文件描述符fd。在內(nèi)核中,每個epoll實(shí)例會和一個struct eventpoll類型的對象一一對應(yīng),該對象是epoll的核心,其聲明在fs/eventpoll.c文件中.
epoll_create的接口定義在這里,主要源碼分析如下:
首先創(chuàng)建一個struct eventpoll對象:
然后創(chuàng)建一個struct file對象,將file中的struct file_operations *f_op設(shè)置為全局變量eventpoll_fops,將void *private指向剛創(chuàng)建的eventpoll對象ep:
然后設(shè)置eventpoll中的file指針:
ep->file = file;
最后將文件描述符添加到當(dāng)前進(jìn)程的文件描述符表中,并返回給用戶
fd_install(fd, file);<br data-filtered="filtered">return fd;
操作結(jié)束后主要結(jié)構(gòu)關(guān)系如下圖:

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。∏?00名進(jìn)群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)

二、epoll_ctl()
系統(tǒng)調(diào)用epoll_ctl()在內(nèi)核中的定義如下,各個參數(shù)的含義可參見epoll_ctl的man手冊
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event)
epoll_ctl()首先判斷op是不是刪除操作,如果不是則將event參數(shù)從用戶空間拷貝到內(nèi)核中:
接下來判斷用戶是否設(shè)置了EPOLLEXCLUSIVE標(biāo)志,這個標(biāo)志是4.5版本內(nèi)核才有的,主要是為了解決同一個文件描述符同時被添加到多個epoll實(shí)例中造成的“驚群”問題,詳細(xì)描述可以看這里。 這個標(biāo)志的設(shè)置有一些限制條件,比如只能是在EPOLL_CTL_ADD操作中設(shè)置,而且對應(yīng)的文件描述符本身不能是一個epoll實(shí)例,下面代碼就是對這些限制的檢查:
接下來從傳入的文件描述符開始,一步步獲得struct file對象,再從struct file中的private_data字段獲得struct eventpoll對象:
如果要添加的文件描述符本身也代表一個epoll實(shí)例,那么有可能會造成死循環(huán),內(nèi)核對此情況做了檢查,如果存在死循環(huán)則返回錯誤。這部分的代碼目前我還沒細(xì)看,這里不再貼出。
接下來會從epoll實(shí)例的紅黑樹里尋找和被監(jiān)控文件對應(yīng)的epollitem對象,如果不存在,也就是之前沒有添加過該文件,返回的會是NULL。
ep_find()函數(shù)本質(zhì)是一個紅黑樹查找過程,紅黑樹查找和插入使用的比較函數(shù)是ep_cmp_ffd(),先比較struct file對象的地址大小,相同的話再比較文件描述符大小。struct file對象地址相同的一種情況是通過dup()系統(tǒng)調(diào)用將不同的文件描述符指向同一個struct file對象。
接下來會根據(jù)操作符op的不同做不同的處理,這里我們只看op等于EPOLL_CTL_ADD時的添加操作。首先會判斷上一步操作中返回的epollitem對象地址是否為NULL,不是NULL說明該文件已經(jīng)添加過了,返回錯誤,否則調(diào)用ep_insert()函數(shù)進(jìn)行真正的添加操作。在添加文件之前內(nèi)核會自動為該文件增加POLLERR和POLLHUP事件。
三、ep_insert()
ep_insert()函數(shù)中,首先判斷epoll實(shí)例中監(jiān)視的文件數(shù)量是否已超過限制,沒問題則為待添加的文件創(chuàng)建一個epollitem對象:
接下來是對epollitem的初始化:
接下來是比較重要的操作:將epollitem對象添加到被監(jiān)視文件的等待隊列上去。等待隊列實(shí)際上就是一個回調(diào)函數(shù)鏈表,定義在/include/linux/wait.h文件中。因?yàn)椴煌募到y(tǒng)的實(shí)現(xiàn)不同,無法直接通過struct file對象獲取等待隊列,因此這里通過struct file的poll操作,以回調(diào)的方式返回對象的等待隊列,這里設(shè)置的回調(diào)函數(shù)是ep_ptable_queue_proc:
上面代碼中結(jié)構(gòu)體ep_queue的作用是能夠在poll的回調(diào)函數(shù)中取得對應(yīng)的epollitem對象,這種做法在Linux內(nèi)核里非常常見。
在回調(diào)函數(shù)ep_ptable_queue_proc中,內(nèi)核會創(chuàng)建一個struct eppoll_entry對象,然后將等待隊列中的回調(diào)函數(shù)設(shè)置為ep_poll_callback()。也就是說,當(dāng)被監(jiān)控文件有事件到來時,比如socker收到數(shù)據(jù)時,ep_poll_callback()會被回調(diào)。ep_ptable_queue_proc()代碼如下:
ppoll_entry和epitem等結(jié)構(gòu)關(guān)系如下圖:

在回到ep_insert()函數(shù)中。ep_item_poll()調(diào)用完成之后,會將epitem中的fllink字段添加到struct file中的f_ep_links鏈表中,這樣就可以通過struct file找到所有對應(yīng)的struct epollitem對象,進(jìn)而通過struct epollitem找到所有的epoll實(shí)例對應(yīng)的struct eventpoll。
spin_lock(&tfile->f_lock);?
list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);?
spin_unlock(&tfile->f_lock);
然后就是將epollitem插入到紅黑樹中:1 ep_rbtree_insert(ep, epi) 最后再更新下狀態(tài)就返回了,插入操作也就完成了。 在返回之前還會判斷一次剛才添加的文件是不是當(dāng)前已經(jīng)有事件就緒了,如果是就將其加入到epoll的就緒鏈表中,關(guān)于就緒鏈表放到下一部分中講,這里略過。
最后是我畫的幾個結(jié)構(gòu)體之間的結(jié)構(gòu)圖。

四、分析如下
在通過epoll_ctl(2)向epoll中添加被監(jiān)視文件描述符時,會將ep_poll_callback()作為回調(diào)函數(shù)添加被監(jiān)視文件的等待隊列中。下面分析ep_poll_callback()函數(shù)
判斷返回的事件掩碼里是否設(shè)置了標(biāo)志位POLLFREE(什么時候會設(shè)置該標(biāo)志?),如果是則將當(dāng)前等待對象從文件描述符的等待隊列中刪除(疑問:注釋是什么意思?為什么不需要加鎖?)。
接下來對epoll的實(shí)例加鎖:
接下來判斷epitem中的事件掩碼是不是并沒有包括任何poll(2)事件,如果是的話,則解鎖后直接返回:
什么時候會出現(xiàn)上述情況呢?注釋里也說了,就是在設(shè)置了EPOLLONESHOT標(biāo)志的時候。對EPOLLONESHOT標(biāo)志的處理是在epoll_wait()的返回過程,調(diào)用ep_send_events_proc()的時候,如果設(shè)置了EPOLLONESHOT標(biāo)志則將EP_PRIVATE_BITS以外的標(biāo)志位全部清0:
接下來判斷返回的事件里是否有用戶真正感興趣的事件,沒有則解鎖后返回,否則繼續(xù)。
如果此時就緒鏈表rdllist沒有被其他進(jìn)程訪問,則直接將當(dāng)前文件描述符添加到rdllist鏈表中,否則的話添加到ovflist鏈表中。ovflist默認(rèn)值是EP_UNACTIVE_PTR,epoll_wait()遍歷rdllist之前會把ovflist設(shè)置為NULL,遍歷完再恢復(fù)為EP_UNACTIVE_PTR,因此通過判斷ovflist的值是不是EP_UNACTIVE_PTR可知此時rdllist是不是正在被訪問。
如果是描述符是添加到ovflist鏈表中,說明此時已經(jīng)有ep_wait()準(zhǔn)備返回了,因此不用再喚醒epoll實(shí)例的等待隊列,因此1062行直接跳到解鎖處;否則的話,則喚醒因?yàn)檎{(diào)用epoll_wait()而等待在epoll實(shí)例等待隊列上的進(jìn)程(這里最多只會喚醒一個進(jìn)程):
如果epoll實(shí)例的poll隊列非空,也會喚醒等待在poll隊列上的進(jìn)程,不過是在解鎖后才會進(jìn)行喚醒操作。
最后解鎖并返回:
注意到ep_poll_callback()的返回值和EPOLLEXCLUSIVE標(biāo)志有關(guān),該標(biāo)志是用來處理這種情況:當(dāng)多個進(jìn)程中的不同epoll實(shí)例在監(jiān)視同一個文件描述符時,如果該文件描述符上有事件發(fā)生,則所有的epoll實(shí)例所在進(jìn)程都將被喚醒,這樣有可能造成“驚群”(thundering herd)。
五、epoll驚群原因分析
考慮如下情況(實(shí)際一般不會做,這里只是舉個例子):
在主線程中創(chuàng)建一個socket、綁定到本地端口并監(jiān)聽?
在主線程中創(chuàng)建一個epoll實(shí)例(epoll_create(2))?
將監(jiān)聽socket添加到epoll中(epoll_ctl(2))?
創(chuàng)建多個子線程,每個子線程都共享步驟2里創(chuàng)建的同一個epoll文件描述符,然后調(diào)用epoll_wait(2)等待事件到來accept(2)?
請求到來,新連接建立?
這里的問題就是,在第5步的時候,會有多少個線程被喚醒而從epoll_wait()調(diào)用返回?答案是不一定,可能只有一個,也可能有部分,也可能是全部。當(dāng)然在多個線程都喚醒的情況下,只會有一個線程accept()調(diào)用會成功。
為何如此?從內(nèi)核代碼分析,原因如下:
因?yàn)開_wake_up_common()的調(diào)用是從wake_up_locked()開始的,__wake_up_common的各個參數(shù)值為:
局部變量curr的值可以通過epoll_wait()的源碼得到,具體為:
curr->flags: WQ_FLAG_EXCLUSIVE?
curr->func: default_wake_function?
default_wake_function調(diào)用的是try_to_wake_up。而try_to_wake_up只有在要喚醒的進(jìn)程狀態(tài)不是
TASK_NORMAL時才會返回0,TASK_NORMAL的定義是(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)。
因此__wake_up_common里的if條件會在第一次判斷的時候就滿足,喚醒一個進(jìn)程后便返回了,那為什么實(shí)際測試會發(fā)現(xiàn)有多個進(jìn)程被喚醒呢?
原因就在于這個唯一被喚醒的進(jìn)程。
當(dāng)某個等待在epoll實(shí)例上的進(jìn)程被喚醒后,最終會進(jìn)入到ep_scan_ready_list() 這個函數(shù)中,ep_scan_ready_list()會以回調(diào)方式調(diào)用ep_send_events_proc()來將數(shù)據(jù)復(fù)制到用戶空間。而ep_scan_ready_list()函數(shù)在返回之前會再次判斷epoll的就緒鏈表rdllist是否為空,如果不為空的話,就會再喚醒其他進(jìn)程!下面就是ep_scan_ready_list()返回之前的判斷操作:
而在水平觸發(fā)方式下,從就緒鏈表中移出來的文件描述符,如果當(dāng)前仍有事件就緒(可讀、可寫等),會在復(fù)制到用戶空間后被再次添加到就緒鏈表中:
因此在水平觸發(fā)模式下,被喚醒的進(jìn)程又會去喚醒其他進(jìn)程,除非當(dāng)前事件已經(jīng)被處理完或者所有進(jìn)程都已經(jīng)被喚醒(被喚醒的進(jìn)程會從epoll等待隊列上移除)。
