1 什么是多路復用
????????多路復用(Multiplexing)?是一種讓單個線程同時處理多個 I/O 通道的技術,核心是通過系統調用將 I/O 狀態查詢的工作交給操作系統內核,應用程序只需等待內核通知哪些通道就緒。
- 多路:指的是多個socket網絡連接
- 復用:指的是復用一個線程,使用一個線程來檢查多個文件描述符(Socket)的就緒狀態
- 多路復用主要使用的是三種技術:select、poll、epoll。epoll是最新的,也是目前最好的多路復用
2 三種模式的對比
特性 | select | poll | epoll |
數據結構 | 固定大小位圖(FD_SETSIZE) | 動態鏈表 | 紅黑樹 + 就緒鏈表 |
FD 數量限制 | 通常 1024 | 無限制 | 無限制 |
事件通知機制 | 遍歷所有 FD | 遍歷所有 pollfd | 回調函數直接加入就緒列表 |
時間復雜度 | O(n) | O(n) | O(1) |
用戶 / 內核空間復制 | 每次調用復制整個 FD 集合 | 每次調用復制整個 pollfd 數組 | 僅注冊時需要復制 |
觸發模式 | 僅支持水平觸發(LT) | 僅支持水平觸發(LT) | 支持水平觸發(LT)和邊緣觸發(ET) |
3 select
3.1 基本概念
select 是 Linux 系統中最早的 I/O 多路復用機制,它使用位圖(fd_set)來表示要監控的文件描述符集合。用戶空間調用 select 方法后,通過系統調用進入內核,內核會遍歷位圖中設置為 1 的每一位,對每個對應的文件描述符調用其 poll 方法來檢查是否有數據可讀、可寫或發生異常。當有事件就緒時,內核會將結果位圖拷貝回用戶空間,用戶程序通過檢查結果位圖來確定哪些文件描述符發生了事件
3.2 實現機制
- 數據結構:使用固定大小的位圖(bitmap)存儲文件描述符集合,默認最大支持 1024 個 FD。
- 調用流程:
- 用戶程序創建 fd_set 并添加關注的 FD
- 調用?
select()
?將 fd_set 從用戶空間復制到內核空間 - 內核遍歷所有 FD,檢查是否有就緒事件
- 將就緒狀態寫入 fd_set 并復制回用戶空間
- 用戶程序遍歷 fd_set 檢查哪些 FD 就緒
- 關鍵缺點:
- FD 數量限制(通常 1024)
- 每次調用需復制整個 fd_set
- 遍歷檢查就緒狀態的時間復雜度為 O (n)
3.3?select源碼分析
3.3.1 關鍵數據結構
// include/uapi/asm-generic/select.h
typedef struct {unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} fd_set;// fs/select.c
struct fdtable {unsigned int max_fds; // 最大文件描述符數fd_set *open_fds; // 打開的文件描述符集合fd_set *close_on_exec; // 執行exec時關閉的集合fd_set *reserve_fds; // 保留集合
};
3.3.2 核心調用鏈
sys_select() // 用戶調用的系統調用-> core_sys_select() // 核心處理函數-> do_select() // 執行select邏輯-> poll_initwait() // 初始化等待隊列-> f_op->poll() // 調用每個文件描述符的poll方法-> __pollwait() // 將當前進程添加到等待隊列-> check_events() // 檢查事件是否就緒
3.3.3?do_select 核心邏輯
// fs/select.c
int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{// 初始化等待隊列poll_initwait(&table);// 遍歷所有文件描述符for (i = 0; i < n; i++) {// 獲取文件描述符的poll方法const struct file_operations *f_op = file->f_op;if (f_op && f_op->poll) {// 調用驅動的poll方法,檢查事件并注冊回調wait_key_set(wait, in, out, exc);mask = (*f_op->poll)(file, wait);}}// 如果沒有就緒事件,進入睡眠if (!retval) {retval = poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack);}// 清理并返回就緒描述符數量poll_freewait(&table);return retval;
}
3.4? 優缺點
- 優點:
- 跨平臺支持(幾乎所有操作系統都實現了 select)
- 實現簡單,易于理解
- 缺點:
- FD 數量限制(通常 1024)
- 每次調用需復制整個 FD 集合,開銷大
- 遍歷檢查就緒狀態的時間復雜度為 O (n),性能隨 FD 數量增長而下降
4 poll
4.1 基本概念
poll 的機制與 select 類似,本質上沒有太大差別,也是用于管理多個描述符并進行輪詢,根據描述符的狀態進行處理。它使用 pollfd 結構來描述文件描述符集合,相比 select,poll 沒有最大文件描述符數量的限制。
4.2?實現機制
- 數據結構:使用鏈表代替固定大小的位圖,每個節點包含 FD、關注事件和就緒事件。
- 調用流程:
- 用戶程序創建 pollfd 數組并填充 FD 和關注事件
- 調用?
poll()
?將 pollfd 數組從用戶空間復制到內核空間 - 內核遍歷所有 pollfd 檢查就緒狀態
- 將就緒事件寫入 revents 字段并復制回用戶空間
- 用戶程序遍歷 pollfd 數組檢查就緒事件
- 改進點:取消了 FD 數量限制,采用鏈表存儲 FD 可動態擴展。
- 未解決的問題:仍需復制整個 pollfd 數組,遍歷檢查時間復雜度仍為 O (n)。
4.3?poll源碼分析
4.3.1 關鍵數據結構
// include/uapi/linux/poll.h
struct pollfd {int fd; // 文件描述符short events; // 關注的事件short revents; // 就緒的事件
};
4.3.2?核心函數調用鏈
sys_poll() // 用戶調用的系統調用-> do_sys_poll() // 核心處理函數-> poll_initwait() // 初始化等待隊列-> do_poll() // 執行poll邏輯-> f_op->poll() // 調用每個文件描述符的poll方法-> __pollwait() // 將當前進程添加到等待隊列
4.3.3??do_poll 核心邏輯
// fs/select.c
static int do_poll(unsigned int nfds, struct poll_list *list,struct timespec64 *end_time)
{// 遍歷所有pollfdfor (;;) {struct poll_list *walk;int fdcount = 0;// 遍歷每個pollfdfor (walk = list; walk != NULL; walk = walk->next) {struct pollfd * pfd, * pfd_end;pfd = walk->entries;pfd_end = pfd + walk->len;for (; pfd != pfd_end; pfd++) {// 獲取文件描述符的poll方法if (file->f_op && file->f_op->poll) {// 調用驅動的poll方法mask = file->f_op->poll(file, wait);}// 檢查事件是否匹配if ((mask & pfd->events) &&!test_and_set_bit(pfd->fd, bitmap))fdcount++;}}// 如果有就緒事件或超時,返回if (fdcount || timed_out || signal_pending(current))break;// 否則繼續睡眠fdcount = poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack);}return fdcount;
}
4.4 ?優缺點
- 優點:
- 取消了 FD 數量限制
- 采用動態鏈表存儲 FD,可擴展性更好
- 缺點:
- 仍需復制整個 pollfd 數組
- 遍歷檢查時間復雜度仍為 O (n),不適合大量連接場景
5 epoll
5.1 基本概念
epoll 是 Linux 內核為處理大批量文件描述符而改進的 poll,是 select/poll 的增強版本。它基于事件驅動,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中。
5.2 實現機制
- 核心數據結構:
- 紅黑樹:存儲注冊的 FD,支持 O (log n) 的插入、刪除和查找操作
- 就緒鏈表:存儲就緒的 FD,通過事件回調機制直接加入
- 三個核心系統調用:
- epoll_create:創建一個新的 epoll 實例,并返回一個文件描述符 epfd,用于在 epoll 實例上執行后續操作。
- epoll_ctl:用于向 epoll 實例中添加、修改或刪除文件描述符,需要 epfd、操作類型(如 EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)、要操作的文件描述符 fd 和指定感興趣的事件類型 event 這幾個參數
- epoll_wait:等待 epoll 實例中的 I/O 事件到達,它監視由 epoll_ctl 添加的文件描述符上的事件,需要 epfd 和用于存儲就緒事件的數組,以及可選的超時時間。
- 關鍵優化:
- 使用事件回調機制,無需遍歷所有 FD
- 通過內存映射技術減少用戶 / 內核空間的數據復制
- 高效的 FD 管理,紅黑樹支持大規模連接
epoll 內核實現原理中,有紅黑樹用于記錄用戶程序注冊的 epoll 事件,等待隊列用于喚醒 epoll 線程,就緒隊列用于記錄 socket 讀事件和寫事件。其流程如下:
- 用戶程序調用 epoll_create 函數后,在內核創建 struct eventpoll 對象,同時返回一個文件描述符給用戶。
- 用戶程序通過 epoll_ctl 函數添加、修改、刪除 socket 事件,注冊成功的 socket 事件會插入紅黑樹。
- 如果 epoll 就緒隊列有就緒事件,用戶程序調用 epoll_wait 函數會成功獲取到就緒事件;如果沒有就緒事件,則 epoll 線程陷入休眠。
- 當 socket 接收到數據后,通過 socket 等待隊列可以喚醒休眠的 epoll 線程,并將 socket 封裝成 epoll 就緒事件插入就緒隊列,epoll 線程將就緒事件拷貝至用戶程序。
5.3?epoll源碼分析
5.3.1 關鍵數據結構
// include/linux/epoll.h
struct eventpoll {spinlock_t lock; // 自旋鎖struct mutex mtx; // 互斥鎖wait_queue_head_t wq; // 等待隊列頭wait_queue_head_t poll_wait; // poll方法的等待隊列struct list_head rdllist; // 就緒描述符鏈表struct rb_root rbr; // 紅黑樹根節點struct epitem *ovflist; // 溢出鏈表
};struct epitem {struct rb_node rbn; // 紅黑樹節點struct list_head rdllink; // 就緒鏈表節點struct epoll_filefd ffd; // 文件描述符信息struct eventpoll *ep; // 所屬的eventpoll對象struct list_head fllink; // 文件的epitem鏈表struct epoll_event event; // 用戶關注的事件
};
5.3.2?核心函數調用鏈
// epoll_create 相關
sys_epoll_create()-> epoll_create1()-> epi = kmem_cache_alloc(epi_cache, GFP_KERNEL); // 創建epitem-> init_poll_funcptr(&epi->pt, ep_ptable_queue_proc); // 初始化poll函數指針// epoll_ctl 相關
sys_epoll_ctl()-> ep_insert() // 添加文件描述符-> ep_modify() // 修改事件-> ep_delete() // 刪除文件描述符// epoll_wait 相關
sys_epoll_wait()-> ep_poll()-> ep_events_available() // 檢查是否有就緒事件-> fetch_events() // 將就緒事件從rdllist復制到用戶空間-> schedule_hrtimeout_range() // 沒有事件時進入睡眠
5.3.3??ep_poll 核心邏輯
// epoll_create 相關
sys_epoll_create()-> epoll_create1()-> epi = kmem_cache_alloc(epi_cache, GFP_KERNEL); // 創建epitem-> init_poll_funcptr(&epi->pt, ep_ptable_queue_proc); // 初始化poll函數指針// epoll_ctl 相關
sys_epoll_ctl()-> ep_insert() // 添加文件描述符-> ep_modify() // 修改事件-> ep_delete() // 刪除文件描述符// epoll_wait 相關
sys_epoll_wait()-> ep_poll()-> ep_events_available() // 檢查是否有就緒事件-> fetch_events() // 將就緒事件從rdllist復制到用戶空間-> schedule_hrtimeout_range() // 沒有事件時進入睡眠
5.4 ?優缺點
- 優點:
- 無 FD 數量限制,可輕松處理數萬甚至數十萬個連接
- 事件驅動機制,時間復雜度 O (1),性能不會隨 FD 數量增長而下降
- 使用內存映射技術減少數據復制,效率高
- 支持邊緣觸發(ET)模式,減少不必要的系統調用
- 缺點:
- 僅 Linux 系統支持,不跨平臺
- 編程模型相對復雜,邊緣觸發模式需要更嚴謹的編程邏輯