一、引入
網絡通信的本質就是進程間的通信,進程間通信的本質就是IO(Input,Output)
I/O(input/output)也就是輸入和輸出,在馮諾依曼體系結構當中,將數據從輸入設備拷貝到內存就叫作輸入,將數據從內存拷貝到輸出設備就叫作輸出
站在進程的角度,站在網絡的角度
- 如何理解IO?
IO = 等 + 拷貝,我們在使用read/recv/send/write等,有數據的時候就拷貝到自己的或者對應的緩沖區,沒有數據的時候,就進行阻塞等待或者非阻塞等待
- 什么叫做高效的IO?
本質就是單位時間內,減少等的比重
二、五種IO模型
1.例子引入
現在我們來談談釣魚,釣魚 = 等 + 釣,IO也是等 + 拷貝,我們現在借著釣魚的例子來理解五種IO模型
- 現在有5個人去釣魚
- 張三釣魚一直不動(別人與張三說話,張三也不理對方),眼睛一直盯著魚漂,魚漂動了,就拉起魚竿
- 李四釣魚一直在動,沒事就刷刷抖音,和張三說說話(張三不理他),順便檢測魚漂,魚漂動了,就拉起魚竿
- 王五釣魚在魚竿上掛了鈴鐺,魚一旦上鉤,鈴鐺就會響,就拉起魚竿
- 趙六釣魚買了很多魚竿,把每根魚竿插在岸邊,一直在岸邊跑來跑去,任何一個魚竿就緒,就拉起魚竿
- 田七釣魚帶來一個司機小王,但田七臨時有事離開釣魚塘,田七對小王說:漁具什么我全部給你準備好了,我在給你一個水桶,等你把水桶裝滿,你在打電話給我,然后回來;田七沒有參與調用,只負責發起釣魚
在IO中,這里的人可以看作系統調用、魚竿就是sockfd,釣魚塘是系統內部,魚就是數據,魚漂浮動就是數據就緒,釣就是發生拷貝
- 張三,李四和王五的釣魚效率本質是一樣的嗎?
是的,因為他們釣魚方式都是一樣的,都是先等魚上鉤,然后再將魚釣上來
其次,他們每個人都只拿一根魚竿,在等待魚的上鉤,當河里魚來咬魚鉤的時候,這條魚咬哪一個魚鉤的概率是相同的
- 誰的效率更高?
趙六,因為趙六減少了等待概率的發生,增加了拷貝的時間,所以他的效率是最高的
趙六的效率之所以高,是因為趙六一次等待多個魚竿的魚上鉤,可以將“等”的時間進行重疊
- 如何看待田七的釣魚方式?
田七沒有參與調用,只負責發起釣魚;田七沒有參與等+拷貝的任意一項,而真正釣魚的是小王,在小王釣魚的期間,田七可以干任意的事情,如果將釣魚看作是一種 IO 的話,那么田七的這種釣魚方式就叫作異步 IO。
- 概念整理
張三:阻塞IO
李四:非阻塞IO
王五:信號驅動IO
趙六:多路復用/多路轉接IO
田七+小王:異步IO
阻塞IO與非阻塞IO的本質就是等的方式不同
【例子】在之前的echo例子中,鍵盤向OS輸入,實際將鍵盤輸入的數據放入到OS內部的輸入緩沖區,當進程需要這個數據的時候,將輸入緩沖區的內容拷貝到進程,進程執行結果后將數據拷貝到OS內部的輸出緩沖區,顯示器從輸出緩沖區拷貝內容,最終就把結果回顯給我們
IO = 等 + 拷貝
多路轉接的作用就是為了等待多個fd,等待該fd上面的新事件(OS底層有數據了,讀時間就緒,或者OS底層有空間了,寫事件就緒)就緒,通知程序員,事件已經就緒,可以進行IO拷貝了
2.阻塞IO
阻塞IO:在內核將數據準備好之前,系統調用會一直等待;所有的套接字,默認都是阻塞方式
阻塞IO是最常見的IO模型
應用進程通過recvfrom函數從某個套接字上讀取數據時,如果底層的數據沒有準備好,那么這個進程就一直在這個地方等待著,一旦數據就緒后,才會將數據從內核拷貝到用戶空間,最后recvfrom才會返回
這種以阻塞方式進行IO操作的進程或線程,在“等”和“拷貝”期間都不會返回,在用戶看來就是阻塞了,因此被稱為阻塞IO
3.非阻塞IO
如果內核還未將數據準備好,系統調用仍然會直接返回,并且返回EWOULDBLOCK錯誤碼
非阻塞IO往往需要程序員循環的方式反復嘗試讀寫文件描述符,這個過程稱為輪詢,這對CPU來說是較大的浪費,一般只有特定場景下才使用
當調用recvfrom函數以非阻塞方式從某個套接字上讀取數據時,如果底層數據沒有準備好,那么recvfrom就會立馬錯誤返回,而不是讓該進程進行阻塞等待
因為沒有讀取數據,所以該進程或線程后續還需要繼續調用recvfrom函數,檢測底層數據是否就緒,如果沒有就繼續錯誤返回,直到監測到底層有數據后,再將數據從內核拷貝到用戶空間,再進行成功返回
阻塞與非阻塞的區別就是,非阻塞可以去做其他事情,而阻塞就一直在等
fcntl
在Linux操作系統中,fcntl()
函數是一個用于文件控制的系統調用。它允許你以不同的方式操作打開的文件描述符。這個函數接受三個參數:
fd
:要操作的文件描述符。
cmd
:指定要執行的文件控制命令。
...
:根據 cmd
命令的不同,可能需要傳遞額外的參數。
cmd 參數值 | 用途 |
---|---|
F_DUPFD | 復制文件描述符,返回一個新的文件描述符,它是當前最低可用文件描述符。 |
F_GETFD | 獲取文件描述符的close-on-exec標志。 |
F_SETFD | 設置文件描述符的close-on-exec標志。 |
F_GETFL | 獲取文件狀態標志和訪問模式(如O_RDONLY, O_WRONLY, O_RDWR)。 |
F_SETFL | 設置文件狀態標志,如O_APPEND, O_NONBLOCK等。 |
F_GETLK | 獲取記錄鎖。 |
F_SETLK | 設置或釋放記錄鎖(非阻塞)。 |
F_SETLKW | 設置或釋放記錄鎖(阻塞)。 |
將指定的文件描述符設置為非阻塞模式
void SetNonBlock(int fd)
{int fl = ::fcntl(fd, F_GETFL);if(fl < 0){std::cout << "fcntl error" << std::endl;return;}::fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
下面代碼演示了非阻塞能夠收到EWOULDBLOCK返回
并且也能區分error是出錯了 還是因為非阻塞返回的
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include "Comm.hpp"#include <sys/select.h>int main()
{char buffer[1024];SetNonBlock(0);while(true){// printf("Enter# ");// fflush(stdout);ssize_t n = ::read(0, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;printf("echo# %s", buffer);}else if(n == 0) // ctrl + d{printf("read done\n");break;}else{// 如果是非阻塞,底層數據沒有就緒,IO接口,會以出錯形式返回// 所以,如何區分 底層不就緒 vs 真的出錯了? 根據errno錯誤碼if(errno == EWOULDBLOCK){sleep(1);std::cout << "底層數據沒有就緒,開始輪詢檢測" << std::endl;std::cout << "可以做其他事情" << std::endl;// do other thingcontinue;}else if(errno == EINTR){continue;}else{perror("read");break;}// perror("read\n"); // printf("n=%ld\n", n);// //底層數據沒有就緒: errno 會被設置成為 EWOULDBLOCK EAGAIN// printf("errno=%d\n", errno); // break;}}return 0;
}
測試結果:
當我們輸入數據,而不按回車的時候,底層仍然在輪詢檢測,當我們輸入回車的時候;echo出來的內容與我們輸入的內容一致
4.信號驅動
內核將數據準備好的時候,使用SIGIO信號通知應用程序進行IO操作
應用程序通過系統調用sigaction來設置一個SIGIO信號的處理函數。這個處理函數將在接收到SIGIO信號時被觸發。內核處于等待狀態,直到數據準備好。數據準備好時,內核會發出一個SIGIO信號通知應用程序。應用程序捕獲到SIGIO信號后,它會執行recvfrom系統調用來從網絡接收數據,recvfrom系統調用完成后,內核會將控制權交還給應用程序,同時傳遞回成功的指示,數據報從內核空間復制到用戶空間,應用程序現在可以在用戶空間內處理收到的數據報了
如果數據正在從內核空間拷貝到用戶空間的緩沖區過程中,那么在此期間,應用程序可能會暫時阻塞,直到數據拷貝完成。
信號的產生是異步的,但信號驅動 IO 是同步 IO 的一種。
因為它依然參與了等 + 拷貝
5.IO多路轉接
雖然從流程圖上看起來和阻塞IO類似,實際上最核心在于IO多路轉接能夠同時等待多個文件描述符的就緒狀態
使用select最主要的目的:將等 + 拷貝兩個操作分開。select專門負責等,recvfrom負責拷貝
- 應用程序通過調用
select
函數來阻塞自己,等待多個套接字中的任何一個變為可讀狀態。這意味著應用程序會暫停執行,直到至少一個套接字準備好讀取數據。 - 當操作系統檢測到某個套接字的數據已經準備好可以讀取時,它會通知應用程序,并通過
select
函數返回這個信息。 - 應用程序收到操作系統的通知后,它可以通過
recvfrom
系統調用來實際從套接字中讀取數據。這個調用會將數據從網絡層復制到應用程序指定的緩沖區中。 - 內核負責管理數據的接收、存儲以及最終傳遞給應用程序的過程。具體來說,當數據到達內核的網絡堆棧時,內核會將其暫存起來,然后根據應用程序的要求進行相應的處理。
- 數據被內核成功接收并準備好供應用程序讀取的狀態。
- 內核將數據從其內部緩存(通常稱為“內核空間”)拷貝到應用程序分配的用戶空間內存區域
- 數據拷貝完成后,應用程序就可以訪問這些數據并進行進一步的處理了。
因為這些多路轉接接口是一次 “等” 多個文件描述符的,因此能將 “等” 的時間重疊,數據就緒后再調用對應的 recvfrom 等函數進行數據的拷貝,此時這些函數就能夠直接進行拷貝,而不需要 “等” 了
6.異步IO
由內核在數據拷貝完成時,通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據)。
應用進程調用aio_read函數發起一個異步讀操作。內核檢查數據是否準備好供讀取(如果數據未準備好,內核會等待直到數據準備好;一旦數據準備好,內核會將數據拷貝到用戶空間緩沖區中)當數據被成功拷貝到用戶空間時,內核通知應用程序數據已經可用,應用程序可以繼續執行其他任務,而不需要等待I/O操作的完成,當I/O操作完成后,內核通過信號或回調函數通知應用程序。
7.小結
任何IO過程中,都包含兩個步驟,第一個是等待,第二是拷貝,而且在實際的應用場景中,等待消耗的時間往往都遠遠高于拷貝的時間。讓IO更高效,最核心的辦法就是讓等待的時間盡量少
三、高級IO重要概念
1.同步通信 VS 異步通信(Synchronous Communication / Asynchronous Communication)
同步和異步關注的是消息通信機制。
- 所謂同步,就是在發出一個調用時,在沒有得到結果之前,該調用就不返回。但是一旦調用返回,就得到返回值了。換句話說,就是由調用者主動等待這個調用的結果。
- 異步則是相反,調用在發出之后,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果。而是在調用發出后,被調用者通過狀態、通知來通知調用者,或通過回調函數處理這個調用。
另外,我們回憶在講多進程多線程的時候,也提到同步和互斥,這里的同步通信和進程之間的同步是完全不想干的概念
- 進程 / 線程同步:指的是在保證數據安全的前提下,讓進程/線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,談論的是進程/線程間的一種工作關系。
- 同步 IO:指的是進程/線程與操作系統之間的關系,談論的是進程/線程是否需要主動參與 IO 過程。
注意:尤其是在訪問臨界資源的時候,一定要弄清楚這個 “同步”,是同步通信異步通信的同步,還是同步與互斥的同步。
2.阻塞 VS 非阻塞
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態。
- 阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回。
- 非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。
3.其他高級 IO
非阻塞 IO、 紀錄鎖、系統 V 流機制、 I/O 多路轉接(也叫 I/O 多路復用), readv 和 writev 函數以及存儲映射 IO( mmap ),這些統稱為高級 IO。