代碼:https://gitee.com/nanyi-c/linux/tree/master/day50
一、I/O多路轉接之select
1.初始select
系統提供select函數來實現多路復用輸入/輸出模型
- select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的
- 程序會停在select這里等待,直到被監視的文件描述符有一個或多個發生了狀態改變
2.select函數原型
select
是一個在 Unix 和類 Unix 操作系統中的系統調用,用于監控多個文件描述符,等待其中一個或多個文件描述符變得“就緒”。就緒可以意味著可讀、可寫或者發生異常
參數說明
參數 | 描述 |
---|---|
nfds | 這是你監控的文件描述符集(readfds、writefds、exceptfds)中最高文件描述符的編號加1。簡單來說,它是監控的文件描述符范圍的上限 |
readfds | 輸入輸出型參數,只關心讀事件 |
writefds | 輸入輸出型參數,只關心寫事件 |
exceptfds | 輸入輸出型參數,只關心異常事件 |
timeout | 輸入輸出型參數,設置為 NULL ,select 將無限期阻塞,直到至少有一個文件描述符就緒。如果 timeout 設置為非 NULL 值,它將在指定的秒數和微秒數后超時 |
以下是 timeval
結構體的定義:
timeval結構用于描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則發生函數返回,返回值為0
struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */
};
select
調用的返回值和含義如下:
- 返回值大于0:表示就緒的文件描述符數量。輸入輸出型參數
- 返回0:表示超時發生,沒有文件描述符就緒。
- 返回-1:表示出錯,并且設置
errno
來指示錯誤類型。
錯誤值可能為:
-
EBADF 文件描述詞為無效的或該文件已關閉
-
EINTR 此調用被信號所中斷
-
EINVAL 參數n為負值
-
ENOMEM 核心內存不足
常見的程序片段如下:
fs_set readset;
FD_SET (fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL) ;
if(FD_ISSET (fd, readse) ([....}
1.select要正常工作,需要借助一個輔助數組,來保存所有合法fd
2.每次使用都要重置
3.就緒了,循環檢測處理所有事件
關于fd_set結構
在Linux系統中,fd_set
是一個數據結構,用于表示一組文件描述符的集合。它通常與 select
系統調用一起使用,以便同時監控多個文件描述符的狀態(是否可讀、可寫或有異常發生)。
fd_set
是一個固定大小的位掩碼,其中每一位代表一個文件描述符。在內部,它通常是一個長整型數組,數組中的每個元素代表一定范圍內的文件描述符。由于 fd_set
的大小是固定的,所以它有一個最大文件描述符的限制,這個限制在 Linux 系統中通常是 FD_SETSIZE
(通常定義為 1024)。
常見宏操作
FD_ZERO(fd_set *set)
:將fd_set
清零,即初始化fd_set
,使其不包含任何文件描述符。FD_SET(int fd, fd_set *set)
:將指定的文件描述符fd
添加到fd_set
集合中。FD_CLR(int fd, fd_set *set)
:從fd_set
集合中移除指定的文件描述符fd
。FD_ISSET(int fd, fd_set *set)
:檢查指定的文件描述符fd
是否在fd_set
集合中。這個宏在select
調用后使用,以確定哪些文件描述符已經就緒。
3.理解select執行過程
理解 select 模型的關鍵在于理解 fd_set,為說明方便,取 fd_set 長度為 1 字節,fd_set 中的每一 bit 可以對應一個文件描述符 fd,則 1 字節長的 fd_set 最大可以對應 8 個 fd。
- 執行 fd_set 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) 阻塞等待。
- 若 fd=1,fd=2 上都發生可讀事件,則 select 返回,此時 set 變為 0000,0011
注意 :沒有事件發生的 fd=5 被清空。
4.socket就緒條件
讀就緒
- socket 內核中,接收緩沖區中的字節數,大于等于低水位標記 SO_RCVLOWAT,此時可以無阻塞的讀取該文件描述符,并且返回值大于 0。
- socket TCP 通信中,對端關閉連接,此時對該 socket 讀,則返回 0。
- 監聽 socket 上有新的連接請求。
- socket 上有未處理的錯誤。
寫就緒
- socket 內核中,發送緩沖區中的可用字節數(發送緩沖區的空閑位置大小),大于等于低水位標記 SO_SNDLOWAT,此時可以無阻塞的寫,并且返回值大于 0。
- socket 的寫操作被關閉(close 或者 shutdown),如果此時進行寫操作的話,會觸發 SIGPIPE 信號。
- socket 使用非阻塞 connect 連接成功或失敗之后,socket 上有未讀取的錯誤。
異常就緒
- socket上收到帶外數據,關于帶外數據,和TCP緊急模式相關(回憶TCP協議頭中,有一個緊急指針的字段)
二、SelectServer.hpp
1.基礎框架
2.設計Loop
測試
每隔3s輪詢一次
如果設置為nullptr,那么就永久阻塞式等待,直到有新鏈接
無論哪種方式我們建立鏈接時,服務器會瘋狂輸出,這是因為我們還沒有對收到新鏈接后怎么做
如果事件就緒,但是不處理,select會一直通知我,直到我處理了
我們重新設置一下
此時便不會瘋狂輸出,還會顯示剩余時間
時間就緒后就可以處理事件了,在rfds內
因為rfds是輸入輸出型參數,這里已經返回了哪些事已經就緒的
處理事件
首先我們要判斷我們的文件描述符是不是就緒的,如果是,就可以建立連接了
測試
已經獲得了一個新的sockfd
接下來我們可以讀取嗎?絕對不能讀!讀取的時候,條件不一定滿足(建立連接–》不發請求,(底層沒有數據)讀的時候被阻塞,單進程絕對掛掉)
誰最清楚底層fd的數據是否就緒了呢??通過select!
想辦法把新的fd添加給select,由select統一進行監管。
select 為什么等待的fd會越來越多??
只要將新的fd,添加到fd_array中即可
初始化數組,并且把0號位給listen套接字
重新設計Loop,并且找到最大的文件fd,使用for循環可以處理多個文件描述符,進行動態更新和確定最大文件描述符
添加一個debug函數便于我們調試以及查看信息
HandlerEvent
函數負責檢測哪些文件描述符就緒,并根據它們是監聽套接字還是普通套接字來調用相應的處理函數。
處理新鏈接,并把新鏈接的fd添加到fd_array中
處理普通fd就緒 進行IO
測試
三、select總結
小結:
- select要正常工作,需要借助一個輔助數組,來保存所有合法fd
- 每次使用都要重置
- 就緒了,循環檢測處理所有事件
缺點
- 每次調用 select,都需手動設置 fd 集合,從接口使用角度來說也非常不便。
- 每次調用 select,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大。
- 同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大。
- select 可監控的文件描述符數量太少。