傳統阻塞 I/O:單執行緒只能服務一個連線。
非阻塞 + 輪詢:CPU 佔用高、延遲不可控。
I/O 多路復用:單執行緒同時監控多個檔案描述符(fd),有事件時通知處理,無事件時睡眠釋放 CPU。select、poll、epoll 是 Linux 三大經典實現。
內核將進程掛起至等待隊列(wait_queue_head_t),事件發生時喚醒。核心 API:
wait_event(queue, condition):條件滿足前睡眠(不可中斷)。wait_event_interruptible(queue, condition):可被信號中斷。wake_up(queue):喚醒隊列上所有進程。內部流程:wait_event 定義 wait_queue_t 節點(含 current 指標與 autoremove_wake_function),加入隊列,設置 TASK_UNINTERRUPTIBLE,呼叫 schedule() 切換進程。喚醒時遍歷隊列調用節點 func,最終 try_to_wake_up 設置 TASK_RUNNING。
sys_select → core_sys_select → do_select
fd_set:1024 位位圖(FD_SETSIZE),每位對應一個 fd。in/out/ex 輸入、res_in/res_out/res_ex 輸出),長度 = 實際最大 fd 數。TASK_INTERRUPTIBLE。in/out/ex,對每個關注的 fd 呼叫驅動的 poll 方法(file_operations->poll)。poll 內部:檢查硬體狀態 → 若無事件,呼叫 poll_wait(filp, wait_addr, pt) 將當前進程掛入驅動的等待隊列。poll_wait → __pollwait → 分配 poll_table_entry,設置 wait.func = pollwake,加入 wait_addr 隊列。schedule_timeout 睡眠。wake_up(wait_addr) → 遍歷隊列 → pollwake → try_to_wake_up → 進程喚醒 → 重新掃描 → 返回結果。fd_set 進/出內核,開銷大。fd_set。struct pollfd {
int fd;
short events; // 輸入:關注事件
short revents; // 輸出:就緒事件
};
pollfd 陣列,無 fd 數上限(受限於記憶體)。events/revents 分離,無需每次重置輸入事件。sys_poll → do_sys_poll → do_poll → do_pollfd
邏輯與 select 高度相似,僅資料結構不同:do_pollfd 遍歷 pollfd 陣列呼叫驅動 poll,結果寫入 revents。
| 維度 | select | poll |
|---|---|---|
| fd 上限 | 1024 (FD_SETSIZE) | 無硬性上限 |
| 資料結構 | 位圖 fd_set | pollfd 結構體陣列 |
| 輸入/輸出 | 同一 fd_set 覆蓋 | events/revents 分離 |
| 複製開銷 | 每次全量複製 3×fd_set | 每次複製 pollfd 陣列 |
| 時間複雜度 | O(n) 線性掃描 | O(n) 線性掃描 |
| 可移植性 | POSIX 標準,廣泛支援 | POSIX 標準,廣泛支援 |
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sock1, &readfds);
FD_SET(sock2, &readfds);
int maxfd = (sock1 > sock2) ? sock1 : sock2;
struct timeval tv = {5, 0}; // 5 秒超時
int ret = select(maxfd + 1, &readfds, NULL, NULL, &tv);
if (ret > 0) {
if (FD_ISSET(sock1, &readfds)) handle(sock1);
if (FD_ISSET(sock2, &readfds)) handle(sock2);
}
struct pollfd fds[2] = {
{sock1, POLLIN, 0},
{sock2, POLLIN, 0}
};
int ret = poll(fds, 2, 5000); // 5000ms 超時
if (ret > 0) {
if (fds[0].revents & POLLIN) handle(sock1);
if (fds[1].revents & POLLIN) handle(sock2);
}
select/poll 同屬「水平觸發、線性掃描、用戶態/內核態全量複製」模型,適合 fd 數較少(<1000)場景。高併發(萬級 fd)需使用 epoll(邊緣觸發、就緒隊列、零拷貝、紅黑樹管理 fd)。下一篇將深入 epoll 內核實現與 LT/ET 模式實戰。