📃個人主頁:island1314
🔥個人專欄:Linux—登神長階
目錄
- 一、前言:🔥 I/O 多路轉接
- 為什么需要I/O多路轉接?
- 二、I/O 多路轉接之 select
- 1. 初識 select
- 2. select 函數原型
- 2.1 關于 fd_set 結構
- 2.2 函數返回值
- 3. 理解 select 執行過程
- 3.1 socket 就緒條件
- 讀就緒
- 寫就緒
- 異常就緒(選學)
- 3.2 select 的特點
- 3.3 select 優缺點
- 3.4 注意事項
- 4. 代碼示例
- 5. 使用場景
- 三、后言
一、前言:🔥 I/O 多路轉接
💻 多路I/O轉接服務器? \colorbox{cyan}{ 多路I/O轉接服務器 } ?多路I/O轉接服務器??(或稱為多任務I/O服務器)是一種高效管理多個I/O操作的技術,允許單線程或單進程同時監控和處理多個I/O事件(如網絡套接字、文件描述符等)
- 核心思想:利用操作系統提供的多路I/O轉接機制(如
select
、poll
、epoll
等),由內核幫助應用程序高效地監視多個文件描述符(包括網絡連接、管道、文件等)的狀態變化,而不是讓應用程序自己輪詢每個連接的狀態 - 核心目標:用最小資源開銷實現高并發I/O處理,尤其適用于需要同時處理大量連接的場景(如Web服務器、實時通信系統等)
- 這種方式能夠顯著提高服務器的性能和可擴展性,尤其是在處理大量并發連接時
為什么需要I/O多路轉接?
傳統阻塞I/O模型中,每個I/O操作會阻塞線程直至完成。若需處理多個連接,通常需為每個連接分配獨立線程/進程,導致資源消耗大、上下文切換頻繁。
而I/O多路轉接通過單線程監控多個I/O流,僅在I/O就緒時觸發操作,避免了阻塞和資源浪費。
二、I/O 多路轉接之 select
1. 初識 select
💻 系統提供 select? \colorbox{pink}{ select } ?select?? 函數來實現多路復用 輸入 / 輸出 模型.
select
系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的;- 程序會停在
select
這里等待, 直到被監視的文件描述符有一個或多個發生了狀態改變;
核心原理
select
是一種 同步I/O多路復用 機制,允許程序在一個線程中監聽多個文件描述符(如套接字、文件等)的可讀、可寫或異常事件。- 其核心是通過 **輪詢(polling)**檢查文件描述符狀態,并阻塞等待直到至少一個描述符就緒或超時。
2. select 函數原型
💤 select
的函數原型如下:
#include <sys/select.h>int select(int nfds, // 監控的最大文件描述符值 +1fd_set *readfds, // 監聽可讀事件的描述符集合fd_set *writefds, // 監聽可寫事件的描述符集合fd_set *exceptfds, // 監聽異常事件的描述符集合struct timeval *timeout // 超時時間(NULL為無限等待)
);// 操作fd_set的宏:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符到集合
FD_ISSET(int fd, fd_set *set); // 檢查描述符是否在集合中
FD_CLR(int fd, fd_set *set); // 從集合移除描述符
📚 參數解釋:
nfds
是需要監視的最大的文件描述符值 +1rdset
,wrset
,exset
分別對應于需要檢測的可讀文件描述符的集合 , 可寫文件描述符的集合 及 異常文件描述符的集合timeout
為 結構體timeval
, 用來設置select()
的等待時間/* A time value that is accurate to the nearestmicrosecond but also has a range of years. */ struct timeval {__time_t tv_sec; /* Seconds. */__suseconds_t tv_usec; /* Microseconds. */ };
📚 參數 timeout 取值:
-
NULL
: 則表示select()
沒有timeout
,select
將一直被阻塞, 直到某個文件描述符上發生了事件 -
0
: 僅檢測描述符集合的狀態, 然后立即返回, 并不等待外部事件的發生(非阻塞) -
特定的時間值:
struct timeval timeout = {10, 0}
: 如果在指定的時間段里沒有事件發生,select
將超時返回
2.1 關于 fd_set 結構
typedef long int __fd_mask;/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FDELT(d) ((d) / __NFDBITS)
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))/* fd_set for select and pselect. */
typedef struct{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE //__FD_SETSIZE等于1024/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
- 其實這個結構就是一個 整數數組,更嚴格的說, 是一個 “位圖” . 使用位圖中對應的位來表示要監視的文件描述符.
- 一個long int類型的數組。因為每一位可以代表一個文件描述符。所以fd_set最多表示1024個文件描述符!
- 提供了一組操作
fd_set
的接口, 來比較方便的操作位圖
void FD_CLR(int fd, fd_set *set); // 用來清除描述詞組 set 中相關 fd 的位
int FD_ISSET(int fd, fd_set *set); // 用來測試描述詞組 set 中相關 fd 的位是否為真
void FD_SET(int fd, fd_set *set); // 用來設置描述詞組 set 中相關 fd 的位
void FD_ZERO(fd_set *set); // 用來清除描述詞組 set 的全部位
2.2 函數返回值
- 執行成功則返回 文件描述符狀態已改變的個數
- 如果返回 0 代表在描述符狀態改變前已超過 timeout 時間
- 當有錯誤發生時則返回-1, 錯誤原因存于 errno, 此時參數 readfds, writefds, exceptfds 和 timeout 的值變成不可預測
🙅 錯誤值可能為:
EBADF
: 文件描述詞為無效的或該文件已關閉EINTR
:此調用被信號所中斷EINVAL
: 參數 n 為負值ENOMEM
: 核心內存不足
3. 理解 select 執行過程
🦈 理解 select
模型的關鍵在于理解 fd_set
, 為說明方便, 取 fd_set
長度為 1 字節, fd_set
中的每一 bit 可以對應一個文件描述符 fd_set
。 則 1 字節長的 fd_set
最大可以對應 8 個 fd.
- 執行
fd_set
;FD_ZERO(&set);
則set
用位表示是 0000,0000 - 若 fd= 5,執行
FD_SET(fd,&set)
; 后set
變為 0001,0000(第 5 位置為 1) - 若再加入 fd= 2, fd=1,則
set
變為 0001,0011 - 執行
select(6,&set,0,0,0)
阻塞等待 select
返回, 此時set
變為 0000,0011。 注意: 沒有事件發生的 fd=5 被清空
3.1 socket 就緒條件
讀就緒
socket
內核中, 接收緩沖區中的字節數, 大于等于低水位標記SO_RCVLOWAT
. 此時可以無阻塞的讀該文件描述符, 并且返回值大于 0;socket
TCP 通信中, 對端關閉連接, 此時對該socket
讀, 則返回 0;- 監聽的
socket
上有新的連接請求; socket
上有未處理的錯誤;
寫就緒
socket
內核中, 發送緩沖區中的可用字節數(發送緩沖區的空閑位置大小), 大于等于低水位標記SO_SNDLOWAT
, 此時可以無阻塞的寫, 并且返回值大于 0;socket
的寫操作被關閉(close 或者 shutdown). 對一個寫操作被關閉的socket
進行寫操作, 會觸發 SIGPIPE 信號;socket
使用非阻塞 connect 連接成功或失敗之后;socket
上有未讀取的錯誤;
異常就緒(選學)
- socket 上收到帶外數據. 關于帶外數據, 和 TCP 緊急模式相關(回憶 TCP 協議頭中, 有一個緊急指針的字段), 自己收集相關資料
3.2 select 的特點
- 可監控的文件描述符個數取決于
sizeof(fd_set)
的值. 我這邊服務器上 sizeof(fd_set)= 512, 每 bit 表示一個文件描述符, 則我服務器上支持的最大文件描述符是 512*8=4096. - I將 fd 加入
select
監控集的同時, 還要再使用一個數據結構array
保存放到select
監控集中的 fd,- 用于再
select
返回后,array
作為源數據和fd_set
進行FD_ISSET
判斷** select
返回后會把以前加入的但并無事件發生的 fd 清空, 則每次開始select
前都要重新從array
取得 fd 逐一加入(FD_ZERO 最先)
, 掃描array
的同時 取得 fd 最大值maxfd
, 用于select
的第一個參數
- 用于再
備注: fd_set 的大小可以調整, 可能涉及到重新編譯內核.
3.3 select 優缺點
優點 | 缺點 |
---|---|
跨平臺支持(所有UNIX/Linux系統) | 文件描述符數量受限(默認1024,由FD_SETSIZE 定義) |
簡單易用,適合少量并發場景 | 線性掃描,時間復雜度O(n)(效率隨描述符數量下降) |
超時機制靈活 | 每次調用需重置fd_set (額外內存拷貝開銷) |
- 每次調用 select:都需要手動設置 fd 集合(從用戶態拷貝到內核態), 從接口使用角度來說也非常不便,而且 這個開銷在 fd 很多時會很大
- 同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd, 這個開銷在 fd 很多時也很大
3.4 注意事項
- 描述符上限:通過
FD_SETSIZE
宏定義(通常1024),需重新編譯內核修改。 - 性能問題:當監控數千描述符時,
select
的輪詢效率遠低于epoll
或kqueue
。 - 水平觸發:
select
是水平觸發模式,若未處理就緒事件,會持續通知。 - 非阻塞I/O:結合非阻塞socket可避免單次
read
/write
阻塞整個程序。
4. 代碼示例
示例一:檢測標準輸入輸出
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>int main()
{fd_set read_fds;FD_ZERO(&read_fds); // 清空FD_SET(0, &read_fds);while(true){printf("> ");fflush(stdout);int ret = select(1, &read_fds, NULL, NULL, NULL);if(ret < 0){perror("Select");continue;}if(FD_ISSET(0, &read_fds)){char buf[1024] = {0};read(0, buf, sizeof(buf) - 1);printf("Input: %s", buf);}else{printf("Error! Invalid fd\n");continue;}FD_ZERO(&read_fds);FD_SET(0, &read_fds);}return 0;
}
- 當只檢測文件描述符 0(標準輸入)時,因為輸入條件只有在你有輸入信息的時候才成立,所以如果一直不輸入,就會產生超時信息
示例二:TCP 服務器使用 select
處理多客戶端
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};// 創建TCP socketif ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 設置socket選項(允許地址重用)if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(8080);// 綁定socket到端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 開始監聽if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}fd_set readfds; // 描述符集合int client_sockets[MAX_CLIENTS] = {0}; // 客戶端socket數組int max_sd;while (true) {FD_ZERO(&readfds); // 清空集合FD_SET(server_fd, &readfds); // 添加服務器socket到監聽集合max_sd = server_fd;// 添加所有客戶端socket到集合for (int i = 0; i < MAX_CLIENTS; i++) {int sd = client_sockets[i];if (sd > 0) {FD_SET(sd, &readfds);if (sd > max_sd) max_sd = sd;}}// 調用select,阻塞等待事件int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);if ((activity < 0) && (errno != EINTR)) {perror("select error");}// 檢查服務器socket是否有新連接if (FD_ISSET(server_fd, &readfds)) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}// 將新客戶端socket加入數組for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_socket;std::cout << "New client connected, socket fd: " << new_socket << std::endl;break;}}}// 處理客戶端數據for (int i = 0; i < MAX_CLIENTS; i++) {int sd = client_sockets[i];if (FD_ISSET(sd, &readfds)) {int valread = read(sd, buffer, BUFFER_SIZE);if (valread == 0) { // 客戶端斷開連接getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);std::cout << "Client disconnected" << std::endl;close(sd);client_sockets[i] = 0; // 清除socket} else { // 處理數據buffer[valread] = '\0';std::cout << "Received: " << buffer << std::endl;send(sd, buffer, strlen(buffer), 0); // 回顯數據}}}}return 0;
}
- 初始化服務器
- 創建TCP socket,綁定端口并開始監聽。
- 設置
SO_REUSEADDR
允許地址重用(避免端口占用)。
select
監聽流程- 使用
fd_set
管理需要監聽的描述符集合。 - 每次循環重新初始化集合,添加服務器socket和所有客戶端socket。
- 調用
select
阻塞等待事件,返回就緒的描述符數量。
- 使用
- 處理新連接
- 當服務器socket就緒(
FD_ISSET
),調用accept
接受新連接。 - 將新客戶端socket存入數組。
- 當服務器socket就緒(
- 處理客戶端數據
- 遍歷所有客戶端socket,檢查是否有數據可讀。
- 若
read
返回0,表示客戶端斷開連接,關閉socket并清理數組。 - 否則回顯接收到的數據。
5. 使用場景
- 需要兼容多平臺的輕量級應用。
- 并發連接數較少(如<1000)。
- 超時機制需要精細控制的場景(如同時等待I/O和定時任務)
三、后言
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就結束啦,如果有不懂 和 發現問題的小伙伴可以在評論區說出來哦,同時我還會繼續更新關于【Linux】的內容,比如:多路轉接之
epoll
、poll
模型,請持續關注我 !!