I/O 多路復用是高性能網絡編程的基石,允許單線程同時監聽多個文件描述符。select 是最經典、可移植性最強的實現。本文深入解析其數據結構、API 細節、超時控制及完整服務器示例。
fd_set 本質是一個長整型數組,每個 bit 標記一個文件描述符。容量由 FD_SETSIZE 定義(通常 1024),限制了 select 最大監聽數。
四大操作宏(必須使用,避免直接位操作):
FD_ZERO(fd_set *set):清空集合。FD_SET(int fd, fd_set *set):添加 fd。FD_CLR(int fd, fd_set *set):移除 fd。FD_ISSET(int fd, fd_set *set):檢測 fd 是否就緒。#includeint select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
tv_sec=0, tv_usec=0:非阻塞輪詢,立即返回。NULL:永久阻塞,直到有事件發生。errno(如 EINTR 被信號中斷需重試)。TCP 帶外數據(OOB)觸發 異常條件,普通數據觸發 可讀條件。利用 exceptfds 區分。
int main(int argc, char *argv[]) {
// ... socket, bind, listen, accept 省略 ...
int connfd = accept(listenfd, ...);
fd_set readSet, exceptSet;
char buf[1024];
while (1) {
FD_ZERO(&readSet);
FD_ZERO(&exceptSet);
FD_SET(connfd, &readSet);
FD_SET(connfd, &exceptSet);
int ret = select(connfd + 1, &readSet, NULL, &exceptSet, NULL);
if (ret < 0) { perror("select"); break; }
if (FD_ISSET(connfd, &readSet)) {
ret = recv(connfd, buf, sizeof(buf), 0);
if (ret <= 0) break;
printf("普通數據: %.*s\n", ret, buf);
} else if (FD_ISSET(connfd, &exceptSet)) {
ret = recv(connfd, buf, sizeof(buf), MSG_OOB);
if (ret <= 0) break;
printf("帶外數據(OOB): %.*s\n", ret, buf);
}
}
close(connfd);
return 0;
}
| 缺點 | 影響 | 後繼者解決方案 |
|---|---|---|
| fd_set 大小固定 (FD_SETSIZE) | 無法監聽 >1024 fd | poll 無限制;epoll 紅黑樹管理 |
| 每次調用需全量拷貝 fd_set 到內核 | O(n) 開銷大 | poll 同樣;epoll 共享內存/就緒隊列 |
| 返回後需全量掃描 fd_set 找就緒 fd | O(n) 掃描開銷 | epoll 返回就緒列表 O(1) 遍歷 |
| 水平觸發 (LT) 模式固定 | 需循環讀寫直到 EAGAIN | epoll 支持邊緣觸發 (ET) 高效模式 |