傳統阻塞 I/O 模型下,單執行緒只能處理單一連線;多進程/多執行緒雖能並發,但上下文切換與記憶體開銷巨大。I/O 多路復用 讓單一執行緒同時監控多個檔案描述符,僅在有事件(可讀/可寫/異常)發生時才喚醒處理,極大降低系統開銷,是高併發伺服器(Nginx、Redis 早期版本)的核心基石。
#include
#include
int select(int maxfdp1,
fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
struct timeval *timeout);
// 返回:就緒描述符總數;0=超時;-1=錯誤
maxfdp1:最大描述符值 + 1(內核輪詢範圍 0 ~ maxfdp1-1)。readset / writeset / exceptset:輸入/輸出傳遞關注集合,返回時被修改為就緒集合。不關注者傳 NULL。timeout:
NULL:永久阻塞tv_sec=0, tv_usec=0:非阻塞輪詢FD_ZERO(fd_set *set); // 清空
FD_SET(int fd, fd_set *set); // 加入
FD_CLR(int fd, fd_set *set); // 移除
FD_ISSET(int fd, fd_set *set); // 測試是否就緒
支援多客戶端並發,單執行緒處理所有連線。
// select_server.c
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define LISTENQ 5
#define SERV_PORT 8787
int main(void) {
int listenfd, connfd, maxfd, maxi, nready;
int client[FD_SETSIZE];
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
maxfd = listenfd;
maxi = -1;
for (int i = 0; i < FD_SETSIZE; i++) client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;) {
rset = allset; // select 會修改集合,每次需重置
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
if (errno == EINTR) continue;
perror("select error");
exit(1);
}
// 新連線
if (FD_ISSET(listenfd, &rset)) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("新連線: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
int i;
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { client[i] = connfd; break; }
if (i == FD_SETSIZE) { fprintf(stderr, "客戶端過多\n"); exit(1); }
FD_SET(connfd, &allset);
if (connfd > maxfd) maxfd = connfd;
if (i > maxi) maxi = i;
if (--nready <= 0) continue;
}
// 處理資料
for (int i = 0; i <= maxi; i++) {
int sockfd = client[i];
if (sockfd < 0) continue;
if (FD_ISSET(sockfd, &rset)) {
int n = read(sockfd, buf, MAXLINE);
if (n == 0) { // 客戶端關閉
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
write(STDOUT_FILENO, buf, n); // 印到螢幕
write(sockfd, buf, n); // 回射給客戶端
}
if (--nready <= 0) break;
}
}
}
}
// select_client.c
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define SERV_PORT 8787
int main(void) {
int sockfd, maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
FD_ZERO(&rset);
for (;;) {
FD_SET(STDIN_FILENO, &rset);
FD_SET(sockfd, &rset);
maxfdp1 = (STDIN_FILENO > sockfd ? STDIN_FILENO : sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) {
int n = read(sockfd, recvline, MAXLINE);
if (n == 0) { printf("Server 關閉\n"); exit(0); }
write(STDOUT_FILENO, recvline, n);
}
if (FD_ISSET(STDIN_FILENO, &rset)) {
int n = read(STDIN_FILENO, sendline, MAXLINE);
if (n == 0) { FD_CLR(STDIN_FILENO, &rset); continue; }
write(sockfd, sendline, n);
}
}
}
gcc select_server.c -o server
gcc select_client.c -o client
./server & # 背景啟動
./client # 開啟多個終端機測試
| 缺點 | 影響 | 現代替代方案 |
|---|---|---|
| FD_SETSIZE 限制(通常 1024) | 無法支援超大規模連線 | poll()、epoll (Linux)、kqueue (BSD/macOS)、IOCP (Windows) |
| 每次呼叫需全量複製 fd_set | O(N) 開銷,N 大時效能急劇下降 | epoll 等僅傳遞就緒列表,O(1) |
| 不支援邊緣觸發 | 需自行管理緩衝區避免重複通知 | epoll EPOLLET 模式 |
select() 是理解 I/O 多路復用概念的最佳切入點,適合中小型並發場景(<1000 連線)。生產環境高併發建議直接使用 epoll (Linux) 或跨平台庫 libevent/libuv,但掌握 select() 的 FD 集合操作與事件迴圈邏輯,是通往高效能網路程式設計的必經之路。