一、I/O 模型對比
| 模型 | 機制 | 優點 | 缺點 | 適用場景 |
| 阻塞 I/O | 預設,呼叫阻塞直到就緒 | 簡單 | 併發需多執行緒/進程 | 低併發、簡單服務 |
| 非阻塞 I/O | O_NONBLOCK,輪詢檢查 | 單執行緒處理多連線 | CPU 忙等待、延遲高 | 極簡場景 |
| I/O 多路複用 | select/poll/epoll 內核監視多 fd | 單執行緒高併發、無忙等 | select/poll 有 FD 限制/線性掃描 | 高併發網路服務 |
| 信號驅動 I/O | SIGIO 就緒通知 | 無需輪詢、即時響應 | 訊號處理複雜、可重入要求高 | 特定場景 (如管道、終端) |
| 非同步 I/O (AIO) | aio_read/write 完成通知 | 真正非阻塞、內核協助複製 | Linux 支援有限、複雜 | 磁碟 I/O 密集 |
二、select 實作聊天室伺服器
// server_select.c
#include
#include
#include
#include
#include
#include
#include
#define MAX_CLIENTS 100
#define BUF_SIZE 1024
int server_fd;
int client_fds[MAX_CLIENTS];
int client_count = 0;
fd_set master_set;
void handle_sigint(int sig) {
printf("\nShutting down...\n");
for (int i = 0; i < client_count; i++)
if (client_fds[i] != -1) close(client_fds[i]);
close(server_fd);
exit(0);
}
int main() {
signal(SIGINT, handle_sigint);
server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {.sin_family=AF_INET, .sin_port=htons(9999), .sin_addr.s_addr=INADDR_ANY};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 20);
FD_ZERO(&master_set);
FD_SET(server_fd, &master_set);
int max_fd = server_fd;
for (int i = 0; i < MAX_CLIENTS; i++) client_fds[i] = -1;
printf("Server started on port 9999\n");
while (1) {
fd_set read_set = master_set; // select 會修改集合,需複製
int activity = select(max_fd + 1, &read_set, NULL, NULL, NULL);
if (activity < 0) { perror("select"); break; }
// 新連線
if (FD_ISSET(server_fd, &read_set)) {
struct sockaddr_in cli_addr; socklen_t len = sizeof(cli_addr);
int new_fd = accept(server_fd, (struct sockaddr*)&cli_addr, &len);
if (new_fd < 0) { perror("accept"); continue; }
int i;
for (i = 0; i < MAX_CLIENTS; i++)
if (client_fds[i] == -1) { client_fds[i] = new_fd; break; }
if (i == MAX_CLIENTS) { close(new_fd); continue; }
FD_SET(new_fd, &master_set);
if (new_fd > max_fd) max_fd = new_fd;
printf("New client: fd=%d, total=%d\n", new_fd, ++client_count);
}
// 處理客戶端資料
char buf[BUF_SIZE];
for (int i = 0; i < MAX_CLIENTS; i++) {
int fd = client_fds[i];
if (fd == -1) continue;
if (FD_ISSET(fd, &read_set)) {
int n = read(fd, buf, BUF_SIZE - 1);
if (n <= 0) { // 斷開或錯誤
printf("Client fd=%d disconnected\n", fd);
close(fd);
FD_CLR(fd, &master_set);
client_fds[i] = -1;
client_count--;
} else {
buf[n] = '\0';
// 廣播給其他客戶端
for (int j = 0; j < MAX_CLIENTS; j++) {
if (client_fds[j] != -1 && client_fds[j] != fd)
write(client_fds[j], buf, n);
}
}
}
}
}
return 0;
}
三、信號驅動 I/O 客戶端
// client_sigio.c
#include
#include
#include
#include
#include
#include
#include
int sockfd;
void sigio_handler(int sig) {
char buf[1024];
int n = read(sockfd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
write(STDOUT_FILENO, "MSG: ", 5);
write(STDOUT_FILENO, buf, n);
}
}
int main() {
struct sigaction sa = {0};
sa.sa_handler = sigio_handler;
sa.sa_flags = SA_RESTART; // 關鍵:被訊號中斷的 read 自動重啟
sigaction(SIGIO, &sa, NULL);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {.sin_family=AF_INET, .sin_port=htons(9999), .sin_addr.s_addr=inet_addr("127.0.0.1")};
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
printf("Connected\n");
// 設定非阻塞 + 信號驅動
int flags = fcntl(sockfd, F_GETFL);
fcntl(sockfd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
fcntl(sockfd, F_SETOWN, getpid()); // 接收 SIGIO 的進程
char buf[1024];
while (fgets(buf, sizeof(buf), stdin)) {
write(sockfd, buf, strlen(buf));
}
close(sockfd);
return 0;
}
四、select vs poll vs epoll 關鍵差異
| 特性 | select | poll | epoll (Linux) |
| 最大 FD 數 | 1024 (FD_SETSIZE) | 無限制 (陣列) | 無限制 (紅黑樹) |
| 時間複雜度 | O(n) 全掃描 | O(n) 全掃描 | O(1) 就緒鏈表 |
| FD 複製開銷 | 每次調用複製 bitmap | 每次調用複製陣列 | 註冊一次,無複製 |
| 觸發模式 | 水平觸發 (LT) | 水平觸發 | LT / 邊緣觸發 (ET) |
| 支援平台 | 所有 Unix | 所有 Unix | Linux 專用 |
五、epoll 邊緣觸發 (ET) 高效能範例
// epoll_et.c 關鍵邏輯
int epfd = epoll_create1(0);
struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = listen_fd};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// accept 循環直到 EAGAIN (ET 模式必須)
while ((conn_fd = accept(listen_fd, ...)) > 0) {
set_nonblock(conn_fd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
}
} else {
// 讀取循環直到 EAGAIN
while ((n = read(fd, buf, sizeof(buf))) > 0) process(buf, n);
if (n == 0 || (n < 0 && errno != EAGAIN)) close_fd();
}
}
}
六、實務選型建議
- 連線數 < 1000:select 簡單可移植足夠。
- 連線數 1000-10000:poll 避免 FD_SETSIZE 限制。
- 連線數 > 10000 (C10K/C100K):epoll ET 模式 + 非阻塞 I/O + 線程池。
- 跨平台庫:libevent / libuv / libev 封裝 epoll/kqueue/IOCP/select 統一介面。
- 信號驅動:僅用於特定 fd (管道、終端、Socket 配合
O_ASYNC),一般網路服務首選 epoll。