接上文poll機制:Linux 系統 poll 與 epoll 機制1。
3. epoll 機制:高并發 I/O 的優化實現?
epoll(Efficient event polling implementation)機制誕生于 Linux 2.5.44 版本,是內核為解決高并發 I/O 場景(如萬級以上 FD 監聽)而設計的新一代 I/O 多路復用技術。epoll全稱event poll,但其中的e有double e的味道:efficient & event。它通過紅黑樹管理注冊 FD、就緒鏈表存儲就緒 FD、內存映射(mmap)減少拷貝三大優化,徹底解決了poll的性能瓶頸,實現了 “O (1) 就緒事件查詢”,成為高性能服務器的標配。?
3.1 epoll 的核心接口與數據結構?
3.1.1 三大核心系統調用?
epoll 通過三個獨立的系統調用來實現 “FD 注冊 - 事件監聽 - 就緒查詢” 的完整流程,避免了poll每次調用都需重新傳遞 FD 集合的問題。epool是anon_inode技術的一個應用案例。
-
epoll_create():創建一個 epoll 實例(內核的核心數據結構);?
-
epoll_ctl():向 epoll 實例中添加、修改或刪除需監聽的 FD 及其事件;?
-
epoll_wait():等待 epoll 實例中注冊的 FD 就緒,并返回就緒事件。?
3.1.2 關鍵數據結構?
epoll 的內核實現依賴三個核心數據結構:struct eventpoll、struct epitem、struct epoll_event。?
(1)用戶空間的struct epoll_event?
用戶進程通過該結構體與內核交互,描述 FD 的監聽事件和就緒狀態,定義如下:
#include <sys/epoll.h>?
struct epoll_event {?uint32_t events; // 監聽事件/就緒事件(與poll的events含義類似)?epoll_data_t data; // 關聯的用戶數據(如FD、自定義指針)?
};?
?
// 聯合體,存儲用戶數據?
typedef union epoll_data {?void *ptr; // 自定義指針(可指向用戶空間數據)?int fd; // 關聯的文件描述符?uint32_t u32; // 32位無符號整數?uint64_t u64; // 64位無符號整數?
} epoll_data_t;
與pollfd相比,epoll_event的優勢是通過epoll_data_t聯合體支持自定義數據,方便用戶進程關聯 FD 的上下文信息(如客戶端連接的會話數據)。?
(2)內核中的struct epitem?
epitem是內核中描述 “epoll 實例與 FD 關聯關系” 的結構,每個注冊到 epoll 的 FD 對應一個epitem,定義簡化如下:
struct epitem {?struct rb_node rbn; // 紅黑樹節點(用于加入epoll實例的紅黑樹)?struct list_head rdllink; // 就緒鏈表節點(FD就緒時加入就緒鏈表)?struct file *file; // 關聯的FD對應的struct file?struct epoll_event event; // 存儲用戶設置的監聽事件與用戶數據?struct epoll_table_struct *epoll_table; // 關聯的poll_table?
};
-
rbn:用于將epitem插入 epoll 實例的紅黑樹,實現 FD 的快速查找、插入、刪除;?
-
rdllink:用于 FD 就緒時,將epitem插入 epoll 實例的就緒鏈表,避免遍歷所有 FD;?
-
epoll_table:關聯poll_table,用于將進程注冊到 FD 的等待隊列。?
(3)內核中的struct eventpoll?
eventpoll是 epoll 實例的核心結構,每個epoll_create()調用會創建一個eventpoll,該對象是內核對象通過anon_inode技術供用戶態訪問。定義簡化如下:
struct eventpoll {?spinlock_t lock; // 保護紅黑樹和就緒鏈表的自旋鎖?struct rb_root rbr; // 紅黑樹的根節點(管理所有注冊的epitem)?struct list_head rdllist; // 就緒鏈表的頭節點(存儲所有就緒的epitem)?wait_queue_head_t wq; // epoll的等待隊列(存儲調用epoll_wait的進程)?struct page *tmp_page; // 用于mmap的內存頁(減少用戶態與內核態拷貝)?
};
-
rbr:紅黑樹的根,用于管理所有通過epoll_ctl注冊的epitem,紅黑樹的 key 是 FD,確保插入、刪除、查找的時間復雜度為 O (logn);?
-
rdllist:就緒鏈表,存儲所有就緒的epitem,epoll_wait只需遍歷該鏈表即可獲取就緒 FD,無需遍歷所有注冊 FD;?
-
wq:epoll 的等待隊列,當無 FD 就緒時,調用epoll_wait的進程會睡眠在該隊列中,FD 就緒時被喚醒;?
-
tmp_page:通過mmap將內核內存頁映射到用戶空間,避免epoll_event數組的拷貝。
3.2 epoll 的核心流程(基于 ET 模式)?
epoll 支持兩種觸發模式:水平觸發(Level Trigger,LT) 和邊緣觸發(Edge Trigger,ET)。LT 模式與poll類似,只要 FD 就緒,每次調用epoll_wait都會返回該 FD;ET 模式僅在 FD 狀態從 “未就緒” 變為 “就緒” 時返回一次,需配合非阻塞 I/O 使用,避免漏讀數據。以下以 ET 模式為例,拆解 epoll 的核心流程。?
3.2.1 步驟 1:創建 epoll 實例(epoll_create)?
用戶進程調用epoll_create(size_t size)(注:size參數在 Linux 2.6.8 后被忽略,僅用于兼容舊版本),內核會:?
-
分配一個struct eventpoll結構體;?
-
初始化結構體中的紅黑樹(rbr)、就緒鏈表(rdllist)、等待隊列(wq);?
-
分配一個內存頁(tmp_page),用于后續mmap;?
-
創建一個匿名文件(內核內部使用),并返回一個epoll 文件描述符(epfd),用戶進程通過epfd操作該 epoll 實例。?
3.2.2 步驟 2:注冊 FD 到 epoll 實例(epoll_ctl)?
用戶進程調用epoll_ctl(int epfd, int op, int fd, struct epoll_event *event),其中op為操作類型(EPOLL_CTL_ADD:添加、EPOLL_CTL_MOD:修改、EPOLL_CTL_DEL:刪除),內核會:?
-
通過epfd找到對應的eventpoll結構體;?
-
根據op類型執行對應操作。
OP | 操作 |
EPOLL_CTL_ADD(添加 FD) | a. 檢查fd是否已注冊(通過紅黑樹查找epitem),若已注冊則返回錯誤;? b. 分配一個struct epitem結構體,初始化rbn(紅黑樹節點)、rdllink (就緒鏈表節點),并關聯fd對應的struct file和用戶傳入的event;? c. 將epitem插入eventpoll的紅黑樹(rbr);? |
EPOLL_CTL_MOD(修改 FD) | a. 通過紅黑樹查找fd對應的epitem;? b. 更新epitem中的event字段(如修改監聽事件);? c. 重新調用設備poll方法,更新等待隊列注冊。? d. 調用fd對應的設備poll方法,傳遞epoll_table(由epitem關聯), 將當前進程注冊到fd的等待隊列中(若后續fd就緒,會觸發喚醒邏輯)。? |
EPOLL_CTL_DEL(刪除 FD) | a. 通過紅黑樹查找fd對應的epitem;? b. 將epitem從紅黑樹和就緒鏈表中移除;? c. 調用設備poll方法,將進程從fd的等待隊列中刪除;? d. 釋放epitem結構體。? |
3.2.3 步驟 3:等待 FD 就緒(epoll_wait)?
用戶進程調用epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout),其中events是用戶空間存儲就緒事件的數組,maxevents是數組長度,內核會:?
-
通過epfd找到eventpoll結構體,并加鎖(spinlock);?
-
檢查就緒鏈表(rdllist)是否為空:?
-
若非空:遍歷就緒鏈表,將每個epitem的event(含就緒事件和用戶數據)拷貝到events數組中,最多拷貝maxevents個元素;?
-
若為空:判斷timeout值,若timeout > 0,則將當前進程睡眠在eventpoll的等待隊列(wq)中,直到 FD 就緒、超時或被信號中斷;若timeout = 0,直接返回 0;若timeout = -1,無限等待。
? ? ? ?3.??解鎖并返回就緒事件的數量(若超時返回 0,失敗返回 - 1)。?
3.2.4 步驟 4:FD 就緒與進程喚醒?
當某個注冊的 FD 就緒時(如 TCP socket 收到數據),內核會:?
-
觸發 FD 所屬設備的中斷處理函數(如網絡中斷);?
-
中斷處理函數調用 FD 的poll方法,判斷 FD 狀態并標記為就緒;?
-
找到該 FD 對應的epitem(通過struct file關聯),將epitem加入eventpoll的就緒鏈表(rdllist);?
-
遍歷eventpoll的等待隊列(wq),喚醒所有睡眠在該隊列中的進程(即調用epoll_wait的進程);?
-
進程被喚醒后,重新執行epoll_wait的邏輯,遍歷就緒鏈表并返回就緒事件。?
3.2.5 步驟 5:用戶進程處理 I/O 事件?
用戶進程通過events數組獲取就緒 FD 后,需:?
-
檢查每個就緒 FD 的events字段(如POLLIN),確定事件類型;?
-
由于 ET 模式僅通知一次,需通過非阻塞 I/O(如read時設置O_NONBLOCK)循環讀取 / 寫入數據,直到返回EAGAIN(無更多數據可讀 / 寫);?
-
處理完數據后,可繼續調用epoll_wait等待下一次事件。?
3.3 epoll 的效率優化關鍵點?
epoll 相比poll的性能優勢,源于四個核心優化:?
-
紅黑樹管理注冊 FD:O (logn) 的操作復雜度?
poll每次調用都需遍歷所有 FD,時間復雜度為 O (n);而 epoll 通過紅黑樹管理注冊的 FD,插入、刪除、查找的時間復雜度均為 O (logn),即使 FD 數量達到 10 萬級,操作耗時也可忽略。?
-
就緒鏈表存儲就緒 FD:O (1) 的就緒查詢?
poll需遍歷所有 FD 才能判斷是否就緒,而 epoll 在 FD 就緒時主動將其加入就緒鏈表,epoll_wait只需遍歷就緒鏈表即可獲取就緒 FD,時間復雜度為 O (k)(k 為就緒 FD 數量),當大多數 FD 未就緒時(高并發場景常見),效率提升極為顯著。?
-
mmap 減少用戶態與內核態拷貝?
poll每次調用都需拷貝struct pollfd數組(用戶態→內核態→用戶態),而 epoll 通過mmap將內核中的tmp_page內存頁映射到用戶空間,epoll_event數組直接在映射內存中傳遞,無需拷貝,大幅減少了數據傳輸開銷。?
-
邊緣觸發(ET)減少無效通知?
LT 模式下,若用戶進程未完全處理 FD 數據,內核會反復通知該 FD,導致無效系統調用;ET 模式僅在 FD 狀態變化時通知一次,配合非阻塞 I/O 可避免無效通知,減少系統調用次數,進一步提升效率。