Linux 網路/系統程式設計中,處理多路 I/O 並發的三大主流方案:
O_NONBLOCKselect/poll/epoll 內核級事件通知aio_read/io_uring)本文以「同時讀取鍵盤(stdin)與滑鼠(/dev/input/mouse1)」為例,展示前兩種方案的完整代碼實現。(註:Linux 原生 AIO 支援有限,範例採用 SIGIO 信號模擬)
核心:對所有關注的檔案描述符設定 O_NONBLOCK,迴圈中嘗試 read,EAGAIN 表示無資料,繼續下一個。
#include#include #include #include #define MOUSE_PATH "/dev/input/mouse1" int main() { int fd = open(MOUSE_PATH, O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open mouse"); return 1; } // 設定 stdin 為非阻塞 int flags = fcntl(0, F_GETFL); fcntl(0, F_SETFL, flags | O_NONBLOCK); char buf[200]; while (1) { // 讀滑鼠 memset(buf, 0, sizeof(buf)); int ret = read(fd, buf, 50); if (ret > 0) printf("滑鼠: [%s]\n", buf); // 讀鍵盤 memset(buf, 0, sizeof(buf)); ret = read(0, buf, 50); if (ret > 0) printf("鍵盤: [%s]\n", buf); // 實際專案需加 usleep 降低 CPU 佔用 } return 0; }
缺點:忙等待(Busy Waiting)浪費 CPU;若描述符眾多,輪詢延遲大。
select 實現select 讓內核監視多個 fd,任一就緒即返回,避免使用者態輪詢。
#include#include int main() { int fd = open(MOUSE_PATH, O_RDONLY); fd_set readset; struct timeval tv = {10, 0}; // 10 秒超時 while (1) { FD_ZERO(&readset); FD_SET(fd, &readset); FD_SET(0, &readset); int ret = select(fd + 1, &readset, NULL, NULL, &tv); if (ret < 0) { perror("select"); break; } if (ret == 0) { puts("Timeout"); break; } if (FD_ISSET(fd, &readset)) { char buf[300] = {0}; read(fd, buf, 50); printf("滑鼠: [%s]\n", buf); } if (FD_ISSET(0, &readset)) { char buf[300] = {0}; read(0, buf, 50); printf("鍵盤: [%s]\n", buf); } } return 0; }
poll 實現poll 使用 struct pollfd 陣列,無 fd 數量限制(select 受 FD_SETSIZE 限制,通常 1024)。
#includestruct pollfd fds[2] = { {0, POLLIN, 0}, // stdin {fd, POLLIN, 0} // mouse }; int ret = poll(fds, 2, 10000); // 10 秒超時(毫秒) if (ret > 0) { for (int i = 0; i < 2; i++) { if (fds[i].revents & POLLIN) { char buf[1024] = {0}; read(fds[i].fd, buf, sizeof(buf)); printf("fd %d: [%s]\n", fds[i].fd, buf); } } }
SIGIO)真正的 AIO (aio_read/io_uring) 需核心支援與連結 -laio。此處示範傳統 SIGIO 機制:檔案就緒時內核發送 SIGIO,使用者註冊處理函式。
#include#include int mousefd; void sigio_handler(int sig) { if (sig != SIGIO) return; char buf[1024] = {0}; read(mousefd, buf, sizeof(buf)); printf("滑鼠(異步): [%s]\n", buf); } int main() { mousefd = open(MOUSE_PATH, O_RDONLY); // 1. 設定檔案支援異步通知 int flags = fcntl(mousefd, F_GETFL); fcntl(mousefd, F_SETFL, flags | O_ASYNC); // 2. 設定接收 SIGIO 的進程/線程 fcntl(mousefd, F_SETOWN, getpid()); // 3. 註冊信號處理函式 signal(SIGIO, sigio_handler); // 主迴圈處理鍵盤(同步阻塞) char buf[1024]; while (1) { memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); printf("鍵盤: [%s]\n", buf); } }
| 維度 | 非阻塞輪詢 | select/poll | epoll (LT/ET) | 異步 I/O (AIO/io_uring) |
|---|---|---|---|---|
| 程式複雜度 | 低 | 中 | 中高 | 高 |
| CPU 佔用 | 高(忙等待) | 低(阻塞於內核) | 極低 | 極低 |
| 最大連接數 | 受限於輪詢頻率 | 1024 (select) / 無限 (poll) | 百萬級 | 百萬級 |
| 適用場景 | 極少量 fd、簡單工具 | 中小型服務、跨平台 | 高併發伺服器 (Nginx, Redis) | 高效能儲存/網路、資料庫引擎 |
| 資料拷貝 | 用戶態多次 | 用戶態多次 | 可零拷貝 (splice) | 內核直接到用戶緩衝區 |
epoll (LT 模式) 或 io_uring (Linux 5.1+,真正的非阻塞提交/完成)。libuv (Node.js 底層)、libevent、libev、ASIO (C++) 封裝平台差異。select/poll 處理大量連線;在高併發場景使用 SIGIO(信號處理複雜、不可重入風險高)。從「使用者態輪詢」到「內核態事件通知」再到「內核完成 I/O 後回呼」,並發 I/O 模型演進的核心是**將等待與調度下沉至內核**,釋放 CPU 執行業務邏輯。理解三大模型的原理、優劣與適用邊界,是系統程式設計師的必修課。