內(nèi)核視角看Epoll LT/ET:數(shù)據(jù)結(jié)構(gòu)與回調(diào)機(jī)制全解
在Linux網(wǎng)絡(luò)編程領(lǐng)域,Epoll 堪稱一顆璀璨明星,憑借其卓越性能,在高并發(fā)場景中大放異彩。想深度洞察 Epoll 的高效運(yùn)作奧秘,從內(nèi)核視角剖析其數(shù)據(jù)結(jié)構(gòu)與回調(diào)機(jī)制是不二之選。Epoll 有水平觸發(fā)(LT)和邊緣觸發(fā)(ET)兩種模式,二者在事件通知時機(jī)與處理方式上大相徑庭,這也使得它們適用于不同的應(yīng)用場景。而內(nèi)核中的數(shù)據(jù)結(jié)構(gòu),如紅黑樹、就緒鏈表等,宛如精密齒輪,協(xié)同運(yùn)作,支撐著 Epoll 精準(zhǔn)且高效地管理大量文件描述符。
同時,回調(diào)機(jī)制則如同靈動紐帶,將內(nèi)核與用戶空間緊密相連,確保事件能夠及時、準(zhǔn)確地傳遞,讓應(yīng)用程序迅速做出響應(yīng)。接下來,讓我們一同踏入內(nèi)核的奇妙世界,抽絲剝繭,深入探究 Epoll LT 和 ET 模式下的數(shù)據(jù)結(jié)構(gòu)精妙設(shè)計與回調(diào)機(jī)制的運(yùn)作邏輯,解鎖 Epoll 高效性能背后的神秘密碼 。
Part1Epoll核心工作原理
1.1 Epoll 是什么
Epoll是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本 ,誕生于 Linux 2.6 內(nèi)核。它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng) CPU 利用率。在傳統(tǒng)的 select/poll 模型中,當(dāng)需要處理大量的文件描述符時,每次調(diào)用都需要線性掃描全部的集合,導(dǎo)致效率隨著文件描述符數(shù)量的增加而呈現(xiàn)線性下降。
而 Epoll 采用了事件驅(qū)動機(jī)制,內(nèi)核會將活躍的文件描述符主動通知給應(yīng)用程序,應(yīng)用程序只需處理這些活躍的文件描述符即可,大大減少了無效的掃描操作。這就好比在一個大型圖書館中,select/poll 需要逐本書籍去查找是否有讀者需要借閱,而 Epoll 則是當(dāng)有讀者需要借閱某本書時,圖書館管理員主動將這本書找出來交給讀者,效率高下立判。
在 I/O 多路復(fù)用機(jī)制中,select 和 poll 是 epoll 的 “前輩”,但它們存在一些明顯的不足,而 epoll 正是為克服這些不足而出現(xiàn)的。
select 是最早被廣泛使用的 I/O 多路復(fù)用函數(shù),它允許一個進(jìn)程監(jiān)視多個文件描述符。然而,select 存在一個硬傷,即單個進(jìn)程可監(jiān)視的文件描述符數(shù)量被限制在 FD_SETSIZE(通常為 1024),這在高并發(fā)場景下遠(yuǎn)遠(yuǎn)不夠。例如,一個大型的在線游戲服務(wù)器,可能需要同時處理成千上萬的玩家連接,select 的這個限制就成為了性能瓶頸。此外,select 每次調(diào)用時,都需要將所有文件描述符從用戶空間拷貝到內(nèi)核空間,檢查完后再拷貝回用戶空間,并且返回后需要通過遍歷 fd_set 來找到就緒的文件描述符,時間復(fù)雜度為 O (n)。當(dāng)文件描述符數(shù)量較多時,這種無差別輪詢會導(dǎo)致效率急劇下降,大量的 CPU 時間浪費(fèi)在遍歷操作上。
poll 在一定程度上改進(jìn)了 select 的不足,它沒有了文件描述符數(shù)量的硬限制,使用 pollfd 結(jié)構(gòu)體數(shù)組來表示文件描述符集合,并且將監(jiān)聽事件和返回事件分開,簡化了編程操作。但 poll 本質(zhì)上和 select 沒有太大差別,它同樣需要將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個 fd 對應(yīng)的設(shè)備狀態(tài)。在處理大量文件描述符時,poll 每次調(diào)用仍需遍歷整個文件描述符數(shù)組,時間復(fù)雜度依然為 O (n),隨著文件描述符數(shù)量的增加,性能也會顯著下降。而且,poll 在用戶態(tài)與內(nèi)核態(tài)之間的數(shù)據(jù)拷貝開銷也不容忽視。
epoll 則在設(shè)計上有了質(zhì)的飛躍。它沒有文件描述符數(shù)量的上限,能輕松處理成千上萬的并發(fā)連接,這使得它非常適合高并發(fā)的網(wǎng)絡(luò)應(yīng)用場景。epoll 采用事件驅(qū)動模式,通過 epoll_ctl 函數(shù)將文件描述符和感興趣的事件注冊到內(nèi)核的事件表中,內(nèi)核使用紅黑樹來管理這些文件描述符,保證了插入、刪除和查找的高效性。當(dāng)有事件發(fā)生時,內(nèi)核會將就緒的文件描述符加入到就緒鏈表中,應(yīng)用程序通過 epoll_wait 函數(shù)獲取這些就緒的文件描述符,只需處理有狀態(tài)變化的文件描述符即可,避免了遍歷所有文件描述符的開銷,時間復(fù)雜度為 O (1)。這種高效的機(jī)制使得 epoll 在高并發(fā)情況下能夠保持良好的性能,大大提升了系統(tǒng)的吞吐量和響應(yīng)速度 。
1.2 Epoll 的核心接口
Epoll 提供了三個核心接口,它們是 Epoll 機(jī)制的關(guān)鍵所在,就像三把鑰匙,開啟了高效 I/O 處理的大門。下面我們詳細(xì)介紹這三個系統(tǒng)調(diào)用的功能、參數(shù)和返回值,并結(jié)合代碼示例展示它們的使用方法。
(1)epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
epoll_create用于創(chuàng)建一個 epoll 實(shí)例,返回一個文件描述符,后續(xù)對 epoll 的操作都將通過這個文件描述符進(jìn)行。在 Linux 2.6.8 之后,size參數(shù)被忽略,但仍需傳入一個大于 0 的值。epoll_create1是epoll_create的增強(qiáng)版本,flags參數(shù)可以設(shè)置為 0,功能與epoll_create相同;也可以設(shè)置為EPOLL_CLOEXEC,表示在執(zhí)行exec系列函數(shù)時自動關(guān)閉該文件描述符。
例如:
int epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
return 1;
}
上述代碼創(chuàng)建了一個 epoll 實(shí)例,并檢查創(chuàng)建是否成功。如果返回值為 - 1,說明創(chuàng)建失敗,通過perror打印錯誤信息。
(2)epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl用于控制 epoll 實(shí)例,對指定的文件描述符fd執(zhí)行操作op。epfd是epoll_create返回的 epoll 實(shí)例文件描述符;op有三個取值:EPOLL_CTL_ADD表示將文件描述符fd添加到 epoll 實(shí)例中,并監(jiān)聽event指定的事件;EPOLL_CTL_MOD用于修改已添加的文件描述符fd的監(jiān)聽事件;EPOLL_CTL_DEL則是將文件描述符fd從 epoll 實(shí)例中刪除,此時event參數(shù)可以為 NULL。
event是一個指向epoll_event結(jié)構(gòu)體的指針,該結(jié)構(gòu)體定義如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events字段表示要監(jiān)聽的事件類型,常見的有EPOLLIN(表示對應(yīng)的文件描述符可以讀)、EPOLLOUT(表示對應(yīng)的文件描述符可以寫)、EPOLLRDHUP(表示套接字的一端已經(jīng)關(guān)閉,或者半關(guān)閉)、EPOLLERR(表示對應(yīng)的文件描述符發(fā)生錯誤)、EPOLLHUP(表示對應(yīng)的文件描述符被掛起)等。data字段是一個聯(lián)合體,可用于存儲用戶自定義的數(shù)據(jù),通常會將fd存儲在這里,以便在事件觸發(fā)時識別是哪個文件描述符。
例如,將標(biāo)準(zhǔn)輸入(STDIN_FILENO)添加到 epoll 實(shí)例中,監(jiān)聽可讀事件:
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = STDIN_FILENO;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
perror("epoll_ctl");
close(epfd);
return 1;
}
上述代碼將標(biāo)準(zhǔn)輸入的文件描述符添加到 epoll 實(shí)例中,監(jiān)聽可讀事件EPOLLIN。如果epoll_ctl調(diào)用失敗,打印錯誤信息并關(guān)閉 epoll 實(shí)例。
(3)epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait用于等待 epoll 實(shí)例上的事件發(fā)生。epfd是 epoll 實(shí)例的文件描述符;events是一個指向epoll_event結(jié)構(gòu)體數(shù)組的指針,用于存儲發(fā)生的事件;maxevents表示events數(shù)組最多能容納的事件數(shù)量;timeout是超時時間,單位為毫秒。如果timeout為 - 1,表示無限期等待,直到有事件發(fā)生;如果為 0,則立即返回,不等待任何事件;如果為正數(shù),則等待指定的毫秒數(shù),超時后返回。
返回值為發(fā)生的事件數(shù)量,如果返回 0 表示超時且沒有事件發(fā)生;如果返回 - 1,表示發(fā)生錯誤,可通過errno獲取具體錯誤信息。
例如:
struct epoll_event events[10];
int nfds = epoll_wait(epfd, events, 10, -1);
if (nfds == -1) {
perror("epoll_wait");
close(epfd);
return 1;
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == STDIN_FILENO) {
char buffer[1024];
ssize_t count = read(STDIN_FILENO, buffer, sizeof(buffer));
if (count == -1) {
perror("read");
return 1;
}
printf("Read %zd bytes\n", count);
}
}
上述代碼使用epoll_wait等待 epoll 實(shí)例上的事件發(fā)生,最多等待 10 個事件,無限期等待。當(dāng)有事件發(fā)生時,遍歷events數(shù)組,檢查是否是標(biāo)準(zhǔn)輸入的可讀事件。如果是,讀取標(biāo)準(zhǔn)輸入的數(shù)據(jù)并打印讀取的字節(jié)數(shù)。
通過這三個系統(tǒng)調(diào)用,我們可以創(chuàng)建 epoll 實(shí)例,注冊文件描述符及其感興趣的事件,然后等待事件發(fā)生并處理,實(shí)現(xiàn)高效的 I/O 多路復(fù)用。
1.3 Epoll 的底層數(shù)據(jù)結(jié)構(gòu)
epoll之所以性能卓越,離不開其精心設(shè)計的數(shù)據(jù)結(jié)構(gòu)。epoll主要依賴紅黑樹和雙向鏈表這兩種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)高效的事件管理,再配合三個核心API,讓它在處理大量并發(fā)連接時游刃有余 。
- epoll工作在應(yīng)用程序和內(nèi)核協(xié)議棧之間。
- epoll是在內(nèi)核協(xié)議棧和vfs都有的情況下才有的。
epoll 的核心數(shù)據(jù)結(jié)構(gòu)是:1個紅黑樹和1個雙向鏈表。還有3個核心API。
可以看到,鏈表和紅黑樹使用的是同一個結(jié)點(diǎn)。實(shí)際上是紅黑樹管理所有的IO,當(dāng)內(nèi)部IO就緒的時候就會調(diào)用epoll的回調(diào)函數(shù),將相應(yīng)的IO添加到就緒鏈表上。數(shù)據(jù)結(jié)構(gòu)有epitm和eventpoll,分別代表紅黑樹和單個結(jié)點(diǎn),在單個結(jié)點(diǎn)上分別使用rbn和rblink使得結(jié)點(diǎn)同時指向兩個數(shù)據(jù)結(jié)構(gòu)。
(1)紅黑樹的巧妙運(yùn)用
epoll 使用紅黑樹來管理所有注冊的文件描述符。紅黑樹是一種自平衡的二叉搜索樹,它有著非常優(yōu)秀的性質(zhì):每個節(jié)點(diǎn)要么是紅色,要么是黑色;根節(jié)點(diǎn)是黑色;所有葉子節(jié)點(diǎn)(通常是 NULL 節(jié)點(diǎn))是黑色;如果一個節(jié)點(diǎn)是紅色,那么它的兩個子節(jié)點(diǎn)都是黑色;從任一節(jié)點(diǎn)到其每個葉子的所有路徑都包含相同數(shù)目的黑色節(jié)點(diǎn) 。這些性質(zhì)保證了紅黑樹的高度近似平衡,使得查找、插入和刪除操作的時間復(fù)雜度都穩(wěn)定在 O (log n),這里的 n 是紅黑樹中節(jié)點(diǎn)的數(shù)量。
- 因?yàn)殒湵碓诓樵?,刪除的時候毫無疑問時間復(fù)雜度是O(n);
- 數(shù)組查詢很快,但是刪除和新增時間復(fù)雜度是O(n);
- 二叉搜索樹雖然查詢效率是lgn,但是如果不是平衡的,那么就會退化為線性查找,復(fù)雜度直接來到O(n);
- B+樹是平衡多路查找樹,主要是通過降低樹的高度來存儲上億級別的數(shù)據(jù),但是它的應(yīng)用場景是內(nèi)存放不下的時候能夠用最少的IO訪問次數(shù)從磁盤獲取數(shù)據(jù)。比如數(shù)據(jù)庫聚簇索引,成百上千萬的數(shù)據(jù)內(nèi)存無法滿足查找就需要到內(nèi)存查找,而因?yàn)锽+樹層高很低,只需要幾次磁盤IO就能獲取數(shù)據(jù)到內(nèi)存,所以在這種磁盤到內(nèi)存訪問上B+樹更適合。
因?yàn)槲覀兲幚砩先f級的fd,它們本身的存儲空間并不會很大,所以傾向于在內(nèi)存中去實(shí)現(xiàn)管理,而紅黑樹是一種非常優(yōu)秀的平衡樹,它完全是在內(nèi)存中操作,而且查找,刪除和新增時間復(fù)雜度都是lgn,效率非常高,因此選擇用紅黑樹實(shí)現(xiàn)epoll是最佳的選擇。
當(dāng)然不選擇用AVL樹是因?yàn)榧t黑樹是不符合AVL樹的平衡條件的,紅黑樹用非嚴(yán)格的平衡來換取增刪節(jié)點(diǎn)時候旋轉(zhuǎn)次數(shù)的降低,任何不平衡都會在三次旋轉(zhuǎn)之內(nèi)解決;而AVL樹是嚴(yán)格平衡樹,在增加或者刪除節(jié)點(diǎn)的時候,根據(jù)不同情況,旋轉(zhuǎn)的次數(shù)比紅黑樹要多。所以紅黑樹的插入效率更高。
我們來具體分析一下。假如我們有一個服務(wù)器,需要監(jiān)聽 1000 個客戶端的連接,每個連接對應(yīng)一個文件描述符。如果使用普通的鏈表來管理這些文件描述符,當(dāng)我們要查找某個特定的文件描述符時,最壞情況下需要遍歷整個鏈表,時間復(fù)雜度是 O (n),也就是需要 1000 次比較操作。但如果使用紅黑樹,由于其平衡特性,即使在最壞情況下,查找一個文件描述符也只需要 O (log n) 次比較操作,對于 1000 個節(jié)點(diǎn)的紅黑樹,log?1000 約等于 10 次左右,相比鏈表效率大大提高。同樣,在插入新的文件描述符(比如有新的客戶端連接)和刪除文件描述符(比如客戶端斷開連接)時,紅黑樹的 O (log n) 時間復(fù)雜度也比鏈表的 O (n) 高效得多。
再對比一下其他數(shù)據(jù)結(jié)構(gòu)。數(shù)組雖然查詢效率高,時間復(fù)雜度為 O (1),但插入和刪除操作比較麻煩,平均時間復(fù)雜度為 O (n) 。二叉搜索樹在理想情況下查找、插入和刪除的時間復(fù)雜度是 O (log n),但如果樹的平衡性被破壞,比如節(jié)點(diǎn)插入順序不當(dāng),就可能退化為鏈表,時間復(fù)雜度變成 O (n)。
B + 樹主要用于磁盤存儲,適合處理大量數(shù)據(jù)且需要頻繁磁盤 I/O 的場景,在內(nèi)存中管理文件描述符不如紅黑樹高效。所以,綜合考慮,紅黑樹是 epoll 管理大量文件描述符的最佳選擇,它能夠快速地定位和操作文件描述符,大大提高了 epoll 的性能。
(2)就緒socket列表-雙向鏈表
除了紅黑樹,epoll 還使用雙向鏈表來存儲就緒的 socket。當(dāng)某個文件描述符上有事件發(fā)生(比如有數(shù)據(jù)可讀、可寫),對應(yīng)的 socket 就會被加入到這個雙向鏈表中。雙向鏈表的優(yōu)勢在于它可以快速地插入和刪除節(jié)點(diǎn),時間復(fù)雜度都是 O (1) 。這對于 epoll 來說非常重要,因?yàn)樵诟卟l(fā)場景下,就緒的 socket 可能隨時增加或減少。
就緒列表存儲的是就緒的socket,所以它應(yīng)能夠快速的插入數(shù)據(jù);程序可能隨時調(diào)用epoll_ctl添加監(jiān)視socket,也可能隨時刪除。當(dāng)刪除時,若該socket已經(jīng)存放在就緒列表中,它也應(yīng)該被移除。(事實(shí)上,每個epoll_item既是紅黑樹節(jié)點(diǎn),也是鏈表節(jié)點(diǎn),刪除紅黑樹節(jié)點(diǎn),自然刪除了鏈表節(jié)點(diǎn))所以就緒列表應(yīng)是一種能夠快速插入和刪除的數(shù)據(jù)結(jié)構(gòu)。雙向鏈表就是這樣一種數(shù)據(jù)結(jié)構(gòu),epoll使用雙向鏈表來實(shí)現(xiàn)就緒隊列(rdllist)。
想象一下,在一個繁忙的在線游戲服務(wù)器中,同時有大量玩家在線。每個玩家的連接都由一個 socket 表示,當(dāng)某個玩家發(fā)送了操作指令(比如移動、攻擊等),對應(yīng)的 socket 就有數(shù)據(jù)可讀,需要被加入到就緒列表中等待服務(wù)器處理。如果使用單向鏈表,插入節(jié)點(diǎn)時雖然也能實(shí)現(xiàn),但刪除節(jié)點(diǎn)時,由于單向鏈表只能從前往后遍歷,找到要刪除節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)比較麻煩,時間復(fù)雜度會達(dá)到 O (n) 。而雙向鏈表每個節(jié)點(diǎn)都有指向前驅(qū)和后繼節(jié)點(diǎn)的指針,無論是插入還是刪除節(jié)點(diǎn),都可以在 O (1) 時間內(nèi)完成。當(dāng)服務(wù)器處理完某個 socket 的事件后,如果該 socket 不再有就緒事件,就可以快速地從雙向鏈表中刪除,不會影響其他節(jié)點(diǎn)的操作。
雙向鏈表和紅黑樹在 epoll 中協(xié)同工作。紅黑樹負(fù)責(zé)管理所有注冊的文件描述符,保證文件描述符的增刪查操作高效進(jìn)行;而雙向鏈表則專注于存儲就緒的 socket,讓應(yīng)用程序能夠快速獲取到有事件發(fā)生的 socket 并進(jìn)行處理。當(dāng)一個 socket 的事件發(fā)生時,epoll 會先在紅黑樹中找到對應(yīng)的節(jié)點(diǎn),然后將其加入到雙向鏈表中。這樣,epoll_wait 函數(shù)只需要遍歷雙向鏈表,就能獲取到所有就緒的 socket,避免了對大量未就緒 socket 的無效遍歷,大大提高了事件處理的效率。
(3)紅黑樹和就緒隊列的關(guān)系
紅黑樹的結(jié)點(diǎn)和就緒隊列的結(jié)點(diǎn)的同一個節(jié)點(diǎn),所謂的加入就緒隊列,就是將結(jié)點(diǎn)的前后指針聯(lián)系到一起。所以就緒了不是將紅黑樹結(jié)點(diǎn)delete掉然后加入隊列。他們是同一個結(jié)點(diǎn),不需要delete。
struct epitem {
RB_ ENTRY(epitem) rbn;
LIST_ ENTRY(epitem) rdlink;
int rdy; //exist in List
int sockfd;
struct epoll_ event event ;
};
struct eventpoll {
ep_ _rb_ tree rbr;
int rbcnt ;
LIST_ HEAD( ,epitem) rdlist;
int rdnum;
int waiting;
pthread_ mutex_ t mtx; //rbtree update
pthread_ spinlock_ t 1ock; //rdList update
pthread_ cond_ _t cond; //bLock for event
pthread_ mutex_ t cdmtx; //mutex for cond
};|
Epoll 還利用了mmap機(jī)制來減少內(nèi)核態(tài)和用戶態(tài)之間的數(shù)據(jù)拷貝。在傳統(tǒng)的I/O模型中,內(nèi)核將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)時,需要進(jìn)行兩次數(shù)據(jù)拷貝,而Epoll通過mmap將內(nèi)核空間和用戶空間的一塊內(nèi)存映射到相同的物理地址,使得內(nèi)核可以直接將數(shù)據(jù)寫入用戶空間的內(nèi)存,減少了一次數(shù)據(jù)拷貝,提高了數(shù)據(jù)傳輸?shù)男?。
Part2LT與ET模式詳解
2.1 LT(水平觸發(fā))模式
(1)觸發(fā)原理:LT 模式就像是一個勤勞且執(zhí)著的快遞小哥,當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait 就會像收到通知的小哥一樣,立刻通知處理程序去讀寫。而且,如果一次沒處理完,下次調(diào)用 epoll_wait 它還會繼續(xù)通知,就如同小哥發(fā)現(xiàn)你沒取走快遞,會反復(fù)提醒你一樣 。這是因?yàn)樵?LT 模式下,只要文件描述符對應(yīng)的緩沖區(qū)中還有未處理的數(shù)據(jù),或者緩沖區(qū)還有可寫入的空間,對應(yīng)的事件就會一直被觸發(fā)。
(2)實(shí)際表現(xiàn):以網(wǎng)絡(luò)通信中的數(shù)據(jù)接收為例,當(dāng)一個 socket 接收到數(shù)據(jù)時,內(nèi)核會將數(shù)據(jù)放入接收緩沖區(qū)。在 LT 模式下,只要接收緩沖區(qū)中有數(shù)據(jù),每次調(diào)用 epoll_wait 都會返回該 socket 的可讀事件,通知應(yīng)用程序去讀取數(shù)據(jù)。哪怕應(yīng)用程序只讀取了部分?jǐn)?shù)據(jù),下次 epoll_wait 依然會返回該 socket 的可讀事件,直到接收緩沖區(qū)中的數(shù)據(jù)被全部讀完 。下面是一段簡單的代碼示例,展示了 LT 模式下數(shù)據(jù)讀取的過程:
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int sockfd, epfd, nfds;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen = sizeof(cliaddr);
char buffer[BUFFER_SIZE];
// 創(chuàng)建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 初始化服務(wù)器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8888);
// 綁定socket
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
// 監(jiān)聽socket
if (listen(sockfd, 5) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
// 創(chuàng)建epoll實(shí)例
epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
close(sockfd);
exit(EXIT_FAILURE);
}
// 將監(jiān)聽socket添加到epoll實(shí)例中
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
perror("epoll_ctl: add listen socket");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
while (1) {
// 等待事件發(fā)生
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == sockfd) {
// 處理新連接
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd == -1) {
perror("accept");
continue;
}
// 將新連接的socket添加到epoll實(shí)例中
event.events = EPOLLIN;
event.data.fd = connfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event) == -1) {
perror("epoll_ctl: add conn socket");
close(connfd);
}
} else {
// 處理已連接socket的讀事件
int connfd = events[i].data.fd;
int n = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (n == -1) {
perror("recv");
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
} else if (n == 0) {
// 對端關(guān)閉連接
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
} else {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
}
}
}
}
close(sockfd);
close(epfd);
return 0;
}
在這段代碼中,當(dāng)有新的連接到來時,將新連接的 socket 添加到 epoll 實(shí)例中。對于已連接的 socket,在 LT 模式下,只要有數(shù)據(jù)可讀,recv 函數(shù)就會被調(diào)用讀取數(shù)據(jù),即使一次沒有讀完,下次 epoll_wait 依然會觸發(fā)可讀事件 。
2.2 ET(邊緣觸發(fā))模式
(1)觸發(fā)原理:ET 模式則像是一個 “高冷” 的快遞小哥,只有當(dāng)被監(jiān)控的文件描述符上的事件狀態(tài)發(fā)生變化,即從無到有時才會觸發(fā)通知,而且只通知一次 。在 ET 模式下,對于讀事件,只有當(dāng) socket 的接收緩沖區(qū)由空變?yōu)榉强諘r才會觸發(fā);對于寫事件,只有當(dāng) socket 的發(fā)送緩沖區(qū)由滿變?yōu)榉菨M時才會觸發(fā) 。這就要求應(yīng)用程序在接收到 ET 模式的通知后,必須盡可能地一次性處理完所有相關(guān)數(shù)據(jù),因?yàn)楹罄m(xù)不會再收到重復(fù)的通知。
(2)實(shí)際表現(xiàn):在實(shí)際應(yīng)用中,ET 模式下的數(shù)據(jù)讀取需要特別注意。由于只通知一次,所以通常需要循環(huán)讀取數(shù)據(jù),直到返回 EAGAIN 錯誤,表示緩沖區(qū)中已經(jīng)沒有數(shù)據(jù)可讀了 。以 socket 接收數(shù)據(jù)為例,代碼示例如下:
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
// 設(shè)置文件描述符為非阻塞模式
void setnonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
}
}
int main() {
int sockfd, epfd, nfds;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen = sizeof(cliaddr);
char buffer[BUFFER_SIZE];
// 創(chuàng)建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 初始化服務(wù)器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8888);
// 綁定socket
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind");
close(sockfd);
exit(EXIT_FAILURE);
}
// 監(jiān)聽socket
if (listen(sockfd, 5) == -1) {
perror("listen");
close(sockfd);
exit(EXIT_FAILURE);
}
// 創(chuàng)建epoll實(shí)例
epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
close(sockfd);
exit(EXIT_FAILURE);
}
// 將監(jiān)聽socket添加到epoll實(shí)例中
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
perror("epoll_ctl: add listen socket");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
while (1) {
// 等待事件發(fā)生
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == sockfd) {
// 處理新連接
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd == -1) {
perror("accept");
continue;
}
// 設(shè)置新連接的socket為非阻塞模式
setnonblocking(connfd);
// 將新連接的socket添加到epoll實(shí)例中,使用ET模式
event.events = EPOLLIN | EPOLLET;
event.data.fd = connfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event) == -1) {
perror("epoll_ctl: add conn socket");
close(connfd);
}
} else {
// 處理已連接socket的讀事件
int connfd = events[i].data.fd;
while (1) {
int n = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 沒有數(shù)據(jù)可讀,退出循環(huán)
break;
} else {
perror("recv");
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
break;
}
} else if (n == 0) {
// 對端關(guān)閉連接
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
break;
} else {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
}
}
}
}
}
close(sockfd);
close(epfd);
return 0;
}
在這個示例中,首先將新連接的 socket 設(shè)置為非阻塞模式,然后以 ET 模式添加到 epoll 實(shí)例中。在處理讀事件時,通過循環(huán)調(diào)用 recv 函數(shù),直到返回 EAGAIN 錯誤,確保將緩沖區(qū)中的數(shù)據(jù)全部讀取出來 。
2.3 LT 與 ET 的對比
- 觸發(fā)次數(shù):從觸發(fā)次數(shù)上看,LT 模式會多次觸發(fā)事件,直到相關(guān)緩沖區(qū)中的數(shù)據(jù)處理完畢或者可寫空間被充分利用;而 ET 模式僅在事件狀態(tài)發(fā)生變化時觸發(fā)一次,后續(xù)不會再因相同事件而觸發(fā) 。這就像兩個快遞小哥,一個會反復(fù)通知你取快遞,直到你取走;另一個只通知你一次,取不取隨你。
- 數(shù)據(jù)處理方式:在數(shù)據(jù)處理方式上,LT 模式相對簡單,應(yīng)用程序可以根據(jù)自己的節(jié)奏逐步處理數(shù)據(jù),每次 epoll_wait 返回后處理一部分?jǐn)?shù)據(jù)即可;而 ET 模式要求應(yīng)用程序更加 “激進(jìn)”,一旦接收到事件通知,就需要盡可能一次性將緩沖區(qū)中的數(shù)據(jù)全部處理完,否則可能會丟失數(shù)據(jù) 。例如,在處理大量網(wǎng)絡(luò)數(shù)據(jù)包時,LT 模式可以每次讀取少量數(shù)據(jù),慢慢處理;而 ET 模式則需要在一次事件通知中讀取完所有到達(dá)的數(shù)據(jù)包。
- 效率:從效率角度來說,ET 模式在處理大量并發(fā)連接且每個連接數(shù)據(jù)量較小的場景下具有更高的效率,因?yàn)樗鼫p少了不必要的事件觸發(fā),降低了系統(tǒng)開銷;而 LT 模式雖然在某些情況下可能會產(chǎn)生一些冗余的觸發(fā),但它的編程復(fù)雜度較低,更易于理解和實(shí)現(xiàn),在一些對效率要求不是特別苛刻的場景中也能發(fā)揮很好的作用 。例如,在一個高并發(fā)的 Web 服務(wù)器中,如果每個請求的數(shù)據(jù)量較小,ET 模式可以更高效地處理請求;而在一個對穩(wěn)定性和開發(fā)效率要求較高的小型應(yīng)用中,LT 模式可能是更好的選擇 。
Part3回調(diào)機(jī)制詳解
epoll 的回調(diào)機(jī)制是其高效的關(guān)鍵所在 。當(dāng)一個文件描述符(比如 socket)就緒時(即有數(shù)據(jù)可讀、可寫或者發(fā)生錯誤等事件),內(nèi)核會調(diào)用預(yù)先注冊的回調(diào)函數(shù) 。這個回調(diào)函數(shù)的主要任務(wù)是將就緒的socket放入 epoll 的就緒鏈表中,然后喚醒正在等待的應(yīng)用程序(通過 epoll_wait 阻塞的應(yīng)用程序線程)。
3.1 回調(diào)函數(shù)的作用
在 Epoll 的世界里,回調(diào)函數(shù)就像是一個隱藏在幕后的 “幕后英雄”,默默地發(fā)揮著關(guān)鍵作用。具體來說,ep_poll_callback 回調(diào)函數(shù)在內(nèi)核中扮演著將事件添加到就緒鏈表 rdllist 的重要角色 。當(dāng)被監(jiān)聽的文件描述符上發(fā)生了對應(yīng)的事件(如可讀、可寫等),內(nèi)核就會調(diào)用 ep_poll_callback 函數(shù)。這個函數(shù)就像是一個 “快遞分揀員”,將發(fā)生事件的文件描述符及其對應(yīng)的事件信息,準(zhǔn)確無誤地添加到就緒鏈表 rdllist 中 。
這樣,當(dāng)應(yīng)用程序調(diào)用 epoll_wait 時,就可以直接從就緒鏈表中獲取到這些就緒的事件,而無需再去遍歷整個紅黑樹,大大提高了事件獲取的效率 。例如,在一個網(wǎng)絡(luò)服務(wù)器中,當(dāng)有新的數(shù)據(jù)到達(dá)某個 socket 時,內(nèi)核會調(diào)用 ep_poll_callback 將該 socket 的可讀事件添加到就緒鏈表,服務(wù)器程序通過 epoll_wait 就能及時獲取到這個事件,從而進(jìn)行數(shù)據(jù)讀取和處理 。
3.2 回調(diào)機(jī)制的工作流程
回調(diào)機(jī)制的工作流程是一個環(huán)環(huán)相扣的精密過程,從事件發(fā)生到最終被應(yīng)用程序處理,每一步都緊密相連。當(dāng)一個文件描述符上發(fā)生了感興趣的事件,比如一個 socket 接收到了數(shù)據(jù) 。內(nèi)核中的設(shè)備驅(qū)動程序會首先感知到這個事件。由于在調(diào)用 epoll_ctl 添加文件描述符時,已經(jīng)為該文件描述符注冊了 ep_poll_callback 回調(diào)函數(shù),所以設(shè)備驅(qū)動程序會調(diào)用這個回調(diào)函數(shù) 。ep_poll_callback 函數(shù)被調(diào)用后,會將包含該文件描述符和事件信息的 epitem 結(jié)構(gòu)體添加到 eventpoll 結(jié)構(gòu)體的就緒鏈表 rdllist 中 。這就好比將一封封 “快遞”(事件)放到了一個專門的 “收件箱”(就緒鏈表)里。
當(dāng)應(yīng)用程序調(diào)用 epoll_wait 時,它會檢查就緒鏈表 rdllist 是否有數(shù)據(jù)。如果有,就將鏈表中的事件復(fù)制到用戶空間的 epoll_event 數(shù)組中,并返回事件的數(shù)量 。應(yīng)用程序根據(jù)返回的事件,對相應(yīng)的文件描述符進(jìn)行處理,比如讀取 socket 中的數(shù)據(jù) 。下面是一個簡化的代碼示例,來展示這個過程:
// 假設(shè)已經(jīng)創(chuàng)建了epoll實(shí)例epfd
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds > 0) {
for (int i = 0; i < nfds; ++i) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
// 處理讀事件,這里可以讀取fd中的數(shù)據(jù)
char buffer[BUFFER_SIZE];
int n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
}
}
}
}
在這個示例中,epoll_wait 從就緒鏈表中獲取到就緒事件,應(yīng)用程序通過遍歷 events 數(shù)組,對發(fā)生讀事件的文件描述符進(jìn)行數(shù)據(jù)讀取操作 。
Part4應(yīng)用場景與選擇策略
4.1 LT模式的適用場景
LT 模式以其獨(dú)特的觸發(fā)特性,在一些特定的應(yīng)用場景中發(fā)揮著重要作用。由于它對數(shù)據(jù)處理及時性要求不高,邏輯處理相對簡單,所以非常適合一些簡單的網(wǎng)絡(luò)服務(wù)。比如小型的 Web 服務(wù)器,這類服務(wù)器通常處理的并發(fā)連接數(shù)較少,業(yè)務(wù)邏輯也不復(fù)雜,可能只是簡單地返回一些靜態(tài)頁面或者處理少量的動態(tài)請求 。
在這種情況下,使用 LT 模式可以降低開發(fā)的難度,開發(fā)者無需過多考慮數(shù)據(jù)一次性處理完的問題,可以按照常規(guī)的順序逐步處理數(shù)據(jù),減少出錯的概率 。再比如一些內(nèi)部系統(tǒng)的 API 服務(wù),這些服務(wù)往往只面向內(nèi)部的少量用戶,對性能的要求不是特別高,使用 LT 模式可以快速搭建起服務(wù),并且易于維護(hù)和調(diào)試 。
4.2 ET模式的適用場景
ET 模式則是高并發(fā)、對效率要求極高場景的 “寵兒”。以 Nginx 為例,作為一款高性能的 Web 服務(wù)器,Nginx 每天要處理海量的并發(fā)請求,在這種情況下,ET 模式的優(yōu)勢就凸顯出來了 。由于 ET 模式只在事件狀態(tài)發(fā)生變化時觸發(fā)一次,這就大大減少了不必要的事件觸發(fā),降低了系統(tǒng)開銷,使得 Nginx 能夠在高并發(fā)的環(huán)境下高效地處理大量請求 。
再比如一些實(shí)時系統(tǒng),如股票交易系統(tǒng)、實(shí)時通信系統(tǒng)等,這些系統(tǒng)對延遲和事件的精確控制要求極高,ET 模式可以確保在數(shù)據(jù)到達(dá)的第一時間觸發(fā)通知,并且通過一次性處理完數(shù)據(jù)的方式,保證系統(tǒng)的實(shí)時性和準(zhǔn)確性 。
4.3 如何根據(jù)需求選擇
在選擇 LT 或 ET 模式時,需要綜合考慮多個因素。從項目需求來看,如果項目的并發(fā)量較低,業(yè)務(wù)邏輯簡單,且開發(fā)周期較短,那么 LT 模式是一個不錯的選擇,它可以快速實(shí)現(xiàn)功能,降低開發(fā)成本 。如果項目面臨高并發(fā)的場景,對性能要求苛刻,那么 ET 模式更能滿足需求,雖然開發(fā)難度會有所增加,但可以獲得更高的效率和更好的性能表現(xiàn) 。
從開發(fā)難度來說,LT 模式編程相對簡單,易于理解和調(diào)試,適合初學(xué)者或者對性能要求不是特別高的項目;而 ET 模式需要開發(fā)者對非阻塞 I/O 和數(shù)據(jù)處理有更深入的理解,編程難度較大,適合有一定經(jīng)驗(yàn)的開發(fā)者 。在實(shí)際的項目中,也可以根據(jù)不同的業(yè)務(wù)模塊來選擇不同的模式,比如對一些核心的、高并發(fā)的業(yè)務(wù)模塊使用 ET 模式,而對一些輔助性的、并發(fā)量較低的模塊使用 LT 模式,從而達(dá)到性能和開發(fā)效率的平衡 。
Part5epoll使用中的注意事項
5.1 常見問題及解決方案
在使用 epoll 時,開發(fā)者常常會遇到一些棘手的問題,其中 ET 模式下數(shù)據(jù)讀取不完整以及 epoll 驚群問題較為典型。
在 ET 模式下,數(shù)據(jù)讀取不完整是一個常見的 “陷阱”。由于 ET 模式的特性,只有當(dāng)文件描述符的狀態(tài)發(fā)生變化時才會觸發(fā)事件通知。在讀取數(shù)據(jù)時,如果沒有一次性將緩沖區(qū)中的數(shù)據(jù)全部讀完,后續(xù)即使緩沖區(qū)中仍有剩余數(shù)據(jù),只要狀態(tài)不再變化,就不會再次觸發(fā)可讀事件通知。這就導(dǎo)致可能會遺漏部分?jǐn)?shù)據(jù),影響程序的正常運(yùn)行。
例如,在一個網(wǎng)絡(luò)通信程序中,客戶端向服務(wù)器發(fā)送了一個較大的數(shù)據(jù)包,服務(wù)器在 ET 模式下接收數(shù)據(jù)。如果服務(wù)器在第一次讀取時只讀取了部分?jǐn)?shù)據(jù),而沒有繼續(xù)讀取剩余數(shù)據(jù),那么剩余的數(shù)據(jù)就會被 “遺忘”,導(dǎo)致數(shù)據(jù)傳輸?shù)牟煌暾?。解決這個問題的關(guān)鍵在于,當(dāng)檢測到可讀事件時,要循環(huán)讀取數(shù)據(jù),直到read函數(shù)返回EAGAIN錯誤,表示緩沖區(qū)中已無數(shù)據(jù)可讀。這樣才能確保將緩沖區(qū)中的數(shù)據(jù)全部讀取完畢,避免數(shù)據(jù)丟失 。
epoll驚群問題也是使用epoll時需要關(guān)注的重點(diǎn)。epoll驚群通常發(fā)生在多個進(jìn)程或線程使用各自的epoll實(shí)例監(jiān)聽同一個socket的場景中。當(dāng)有事件發(fā)生時,所有阻塞在epoll_wait上的進(jìn)程或線程都會被喚醒,但實(shí)際上只有一個進(jìn)程或線程能夠成功處理該事件,其他進(jìn)程或線程在處理失敗后又會重新休眠。這會導(dǎo)致大量不必要的進(jìn)程或線程上下文切換,浪費(fèi)系統(tǒng)資源,降低程序性能。在一個多進(jìn)程的 Web 服務(wù)器中,多個工作進(jìn)程都使用 epoll 監(jiān)聽同一個端口。當(dāng)有新的 HTTP 請求到來時,所有工作進(jìn)程的epoll_wait都會被喚醒,但只有一個進(jìn)程能夠成功接受連接并處理請求,其他進(jìn)程的喚醒操作就成為了無效的開銷。
為了避免epoll 驚群問題,可以使用epoll的EPOLLEXCLUSIVE模式,該模式在 Linux 4.5 + 內(nèi)核版本中可用。當(dāng)設(shè)置了EPOLLEXCLUSIVE標(biāo)志后,epoll 在喚醒等待事件的進(jìn)程或線程時,只會喚醒一個,從而避免了多個進(jìn)程或線程同時被喚醒的情況,有效減少了系統(tǒng)資源的浪費(fèi) 。同時,也可以結(jié)合使用SO_REUSEPORT選項,每個進(jìn)程或線程都有自己獨(dú)立的 socket 綁定到同一個端口,內(nèi)核會根據(jù)四元組信息進(jìn)行負(fù)載均衡,將新的連接分配給不同的進(jìn)程或線程,進(jìn)一步優(yōu)化高并發(fā)場景下的性能 。
5.2 性能優(yōu)化建議
為了充分發(fā)揮 epoll 的優(yōu)勢,提升程序性能,我們可以從以下幾個方面進(jìn)行優(yōu)化:
合理設(shè)置epoll_wait的超時時間至關(guān)重要。epoll_wait的timeout參數(shù)決定了等待事件發(fā)生的最長時間。如果設(shè)置為 - 1,表示無限期等待,直到有事件發(fā)生;設(shè)置為 0,則立即返回,不等待任何事件;設(shè)置為正數(shù),則等待指定的毫秒數(shù)。在實(shí)際應(yīng)用中,需要根據(jù)具體業(yè)務(wù)場景來合理選擇。
在一些對實(shí)時性要求極高的場景,如在線游戲服務(wù)器,可能需要將超時時間設(shè)置為較短的值,以確保能夠及時響應(yīng)玩家的操作。但如果設(shè)置得過短,可能會導(dǎo)致頻繁的epoll_wait調(diào)用,增加系統(tǒng)開銷。因此,需要通過測試和調(diào)優(yōu),找到一個平衡點(diǎn),既能滿足實(shí)時性需求,又能降低系統(tǒng)開銷??梢愿鶕?jù)業(yè)務(wù)的平均響應(yīng)時間和事件發(fā)生的頻率來估算合適的超時時間,然后在實(shí)際運(yùn)行中根據(jù)性能指標(biāo)進(jìn)行調(diào)整 。
批量處理事件也是提高 epoll 性能的有效方法。當(dāng)epoll_wait返回多個就緒事件時,一次性處理多個事件可以減少函數(shù)調(diào)用和上下文切換的開銷。在一個高并發(fā)的文件服務(wù)器中,可能同時有多個客戶端請求讀取文件。當(dāng)epoll_wait返回多個可讀事件時,可以將這些事件對應(yīng)的文件描述符放入一個隊列中,然后批量讀取文件數(shù)據(jù)??梢允褂镁€程池或協(xié)程來并行處理這些事件,進(jìn)一步提高處理效率。通過批量處理事件,能夠充分利用系統(tǒng)資源,提高程序的吞吐量 。
使用EPOLLONESHOT事件可以避免重復(fù)觸發(fā)帶來的性能問題。對于注冊了EPOLLONESHOT的文件描述符,操作系統(tǒng)最多觸發(fā)其上注冊的一個可讀、可寫或者異常的事件,且只觸發(fā)一次,除非使用epoll_ctl函數(shù)重置該文件描述符上注冊的EPOLLONESHOT事件。這在多線程環(huán)境中尤為重要,它可以確保一個 socket 在同一時刻只被一個線程處理,避免多個線程同時操作同一個 socket 導(dǎo)致的競態(tài)條件。
在一個多線程的網(wǎng)絡(luò)爬蟲程序中,每個線程負(fù)責(zé)處理一個網(wǎng)頁的下載和解析。通過為每個socket設(shè)置EPOLLONESHOT事件,可以保證每個socket在下載過程中不會被其他線程干擾,提高程序的穩(wěn)定性和性能。在處理完事件后,要及時重置EPOLLONESHOT事件,以便該socket在后續(xù)有新事件發(fā)生時能夠再次被觸發(fā) 。