首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Linux I/O 多路複用與信號驅動 I/O 實戰:select/poll/epoll 對比與聊天室範例

     編輯:關於C語言

一、I/O 模型對比

模型機制優點缺點適用場景
阻塞 I/O預設,呼叫阻塞直到就緒簡單併發需多執行緒/進程低併發、簡單服務
非阻塞 I/OO_NONBLOCK,輪詢檢查單執行緒處理多連線CPU 忙等待、延遲高極簡場景
I/O 多路複用select/poll/epoll 內核監視多 fd單執行緒高併發、無忙等select/poll 有 FD 限制/線性掃描高併發網路服務
信號驅動 I/OSIGIO 就緒通知無需輪詢、即時響應訊號處理複雜、可重入要求高特定場景 (如管道、終端)
非同步 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 關鍵差異

特性selectpollepoll (Linux)
最大 FD 數1024 (FD_SETSIZE)無限制 (陣列)無限制 (紅黑樹)
時間複雜度O(n) 全掃描O(n) 全掃描O(1) 就緒鏈表
FD 複製開銷每次調用複製 bitmap每次調用複製陣列註冊一次,無複製
觸發模式水平觸發 (LT)水平觸發LT / 邊緣觸發 (ET)
支援平台所有 Unix所有 UnixLinux 專用

五、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();
        }
    }
}

六、實務選型建議

  1. 連線數 < 1000:select 簡單可移植足夠。
  2. 連線數 1000-10000:poll 避免 FD_SETSIZE 限制。
  3. 連線數 > 10000 (C10K/C100K):epoll ET 模式 + 非阻塞 I/O + 線程池。
  4. 跨平台庫:libevent / libuv / libev 封裝 epoll/kqueue/IOCP/select 統一介面。
  5. 信號驅動:僅用於特定 fd (管道、終端、Socket 配合 O_ASYNC),一般網路服務首選 epoll。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved