文章目錄
- 1. 高級IO
- 1.1 五種IO模型
- 1.2 高級IO重要概念
- 1.2.1 同步通信 vs 異步通信
- 1.2.2 阻塞 vs 非阻塞
- 1.3非阻塞IO
- 1.3.1 fcntl
- 1.3.2 實現函數SetNoBlock
- 1.3.3 輪詢方式讀取標準輸入
- 1.3.4 I/O多路轉接之select
- 1.3.4.1 初識select:
- 1.3.4.2 select函數原型
- 1.3.4.3 理解select執行過程
1. 高級IO
1.1 五種IO模型
[釣魚例子]
- 阻塞IO: 在內核將數據準備好之前, 系統調用會一直等待. 所有的套接字, 默認都是阻塞方式.
阻塞IO是最常見的IO模型.
- 非阻塞IO: 如果內核還未將數據準備好, 系統調用仍然會直接返回, 并且返回EWOULDBLOCK錯誤碼.
非阻塞IO往往需要程序員循環的方式反復嘗試讀寫文件描述符, 這個過程稱為輪詢. 這對CPU來說是較大的浪費, 一般只有特定場景下才使用
- 信號驅動IO: 內核將數據準備好的時候, 使用SIGIO信號通知應用程序進行IO操作.
- IO多路轉接: 雖然從流程圖上看起來和阻塞IO類似. 實際上最核心在于IO多路轉接能夠同時等待多個文件
描述符的就緒狀態.
- 異步IO: 由內核在數據拷貝完成時, 通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據).
小結:
任何IO過程中, 都包含兩個步驟. 第一是等待, 第二是拷貝. 而且在實際的應用場景中, 等待消耗的時間往往都遠遠高于拷貝的時間. 讓IO更高效, 最核心的辦法就是讓等待的時間盡量少
1.2 高級IO重要概念
在這里, 我們要強調幾個概念
1.2.1 同步通信 vs 異步通信
同步和異步關注的是消息通信機制.
- 所謂同步,就是在發出一個調用時,在沒有得到結果之前,該調用就不返回. 但是一旦調用返回,就得到返回值了; 換句話說,就是由調用者主動等待這個調用的結果;
- 異步則是相反,調用在發出之后,這個調用就直接返回了,所以沒有返回結果; 換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果; 而是在調用發出后,被調用者通過狀態、通知來通知調用者,或通過回調函數處理這個調用.
另外, 我們回憶在講多進程多線程的時候, 也提到同步和互斥. 這里的同步通信和進程之間的同步是完全不想干的概念.
- 進程/線程同步也是進程/線程之間直接的制約關系
- 是為完成某種任務而建立的兩個或多個線程,這個線程需要在某些位置上協調他們的工作次序而等待、傳遞信息所產生的制約關系. 尤其是在訪問臨界資源的時候.
同學們以后在看到 “同步” 這個詞, 一定要先搞清楚大背景是什么. 這個同步, 是同步通信異步通信的同步, 還是同步與互斥的同步
1.2.2 阻塞 vs 非阻塞
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
- 阻塞調用是指調用結果返回之前,當前線程會被掛起. 調用線程只有在得到結果之后才會返回.
- 非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程.
1.3非阻塞IO
1.3.1 fcntl
一個文件描述符, 默認都是阻塞IO
函數原型如下.
#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );
傳入的cmd的值不同, 后面追加的參數也不相同.
fcntl函數有5種功能:
- 復制一個現有的描述符(cmd=F_DUPFD).
- 獲得/設置文件描述符標記(cmd=F_GETFD或F_SETFD).
- 獲得/設置文件狀態標記(cmd=F_GETFL或F_SETFL).
- 獲得/設置異步I/O所有權(cmd=F_GETOWN或F_SETOWN).
- 獲得/設置記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).
我們此處只是用第三種功能, 獲取/設置文件狀態標記, 就可以將一個文件描述符設置為非阻塞.
1.3.2 實現函數SetNoBlock
基于fcntl, 我們實現一個SetNoBlock函數, 將文件描述符設置為非阻塞
void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); // 獲取文件描述符當前的標志if (fl < 0) { // 如果獲取失敗perror("fcntl"); // 打印錯誤信息return; // 退出函數}fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 設置文件描述符為非阻塞模式
}
- 使用F_GETFL將當前的文件描述符的屬性取出來(這是一個位圖).
- 然后再使用F_SETFL將文件描述符設置回去. 設置回去的同時, 加上一個O_NONBLOCK參數.
1.3.3 輪詢方式讀取標準輸入
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>// 將文件描述符設置為非阻塞模式
void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL); // 獲取文件描述符當前的標志if (fl < 0) { // 如果獲取失敗perror("fcntl"); // 打印錯誤信息return; // 退出函數}fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 設置文件描述符為非阻塞模式
}int main() {SetNoBlock(0); // 將標準輸入(文件描述符0)設置為非阻塞模式while (1) { // 無限循環char buf[1024] = {0}; // 定義并初始化緩沖區ssize_t read_size = read(0, buf, sizeof(buf) - 1); // 嘗試從標準輸入讀取數據if (read_size < 0) { // 如果讀取失敗(在非阻塞模式下,沒有數據可讀會返回-1)perror("read"); // 打印錯誤信息sleep(1); // 休眠1秒continue; // 繼續下一次循環}printf("input:%s\n", buf); // 打印讀取到的輸入內容}return 0; // 程序結束(實際上這行代碼不會執行,因為有無限循環)
}
這段程序演示了非阻塞I/O的工作方式:
- 首先將標準輸入(文件描述符0)設置為非阻塞模式
- 在無限循環中,程序嘗試從標準輸入讀取數據
- 由于是非阻塞模式,當沒有輸入數據時,
read()
不會阻塞等待,而是立即返回-1,同時設置errno為EAGAIN或EWOULDBLOCK- 程序捕獲這個錯誤,打印錯誤信息,然后休眠1秒后再次嘗試讀取
- 當有數據輸入時,
read()
會成功讀取數據并返回讀取的字節數,然后程序打印輸入的內容這種模式允許程序在沒有輸入時繼續執行其他任務(在本例中只是休眠),而不是被阻塞在輸入操作上,適用于需要同時處理多個I/O事件的場景。
1.3.4 I/O多路轉接之select
1.3.4.1 初識select:
系統提供select函數來實現多路復用輸入/輸出模型.
- select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的;
- 程序會停在select這里等待,直到被監視的文件描述符有一個或多個發生了狀態改變;
1.3.4.2 select函數原型
select的函數原型如下: #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
參數解釋:
- 參數nfds是需要監視的最大的文件描述符值+1;
- rdset,wrset,exset分別對應于需要檢測的可讀文件描述符的集合,可寫文件描述符的集 合及異常文件描述符的集合;
- 參數timeout為結構timeval,用來設置select()的等待時間
參數timeout取值:
NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個文件描述符上發生了事件;
0:僅檢測描述符集合的狀態,然后立即返回,并不等待外部事件的發生。
特定的時間值:如果在指定的時間段里沒有事件發生,select將超時返回。
關于fd_set結構
其實這個結構就是一個整數數組, 更嚴格的說, 是一個 “位圖”. 使用位圖中對應的位來表示要監視的文件描述符.
提供了一組操作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的全部位
關于timeval結構
timeval結構用于描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函數返回,返回值為0。
函數返回值:
- 執行成功則返回文件描述詞狀態已改變的個數
- 如果返回0代表在描述詞狀態改變前已超過timeout時間,沒有返回
- 當有錯誤發生時則返回-1,錯誤原因存于errno,此時參數readfds,writefds, exceptfds和timeout的值變成不可預測。
錯誤值可能為:
- EBADF 文件描述詞為無效的或該文件已關閉
- EINTR 此調用被信號所中斷
- EINVAL 參數n 為負值。
- ENOMEM 核心內存不足
1.3.4.3 理解select執行過程
理解select模型的關鍵在于理解fd_set,為說明方便,取fd_set長度為1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd.
(1)執行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執行FD_SET(fd,&set);后set變為0001,0000(第5位置為1) *
(3)若再加入fd=2,fd=1,則set變為0001,0011
(4)執行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。
注意:沒有事件發生的fd=5被清空。