文章目錄
- 一、IO為什么慢?
- 一、阻塞IO
- 二、非阻塞IO
- 三、信號驅動IO
- 四、IO多路復用
- 五、異步IO
一、IO為什么慢?
- IO操作往往都是和外設交互,比如鍵盤、鼠標、打印機、磁盤。而最常見的就是內存與磁盤的交互,要知道磁盤是機械設備,需要機械運轉來存取數據,所以比較慢。
- IO本質是:等+拷貝時間往往消耗在等上,等待鍵盤輸入就緒、文件描述符就緒、硬件中斷就緒…
??外設就緒慢是相較于 CPU 來說的,它本身是很快,我們感知不到。
??IO的拷貝很快的,慢大部分時間都花在等上,所以接下來我們就從“等”下手,去解決IO慢的問題。
IO 效率高?單位時間內,傳輸的數據量越大,IO 效率越高。
下面講解的五種IO模型,我們從一個釣魚例子引入:
- 張三:全神貫注的盯著魚漂,本著魚漂不動我不動的原則,盡管馬蜂撲他臉上。(阻塞)
- 李四:不會因為魚不上鉤就緊緊盯著什么也不干,而是邊做其他事情,刷刷快手抖音,寫寫博客啥的。(非阻塞)
- 王五(聰明人):魚竿頂部加鈴鐺,他比李四好一些,不用時不時地去看魚漂。(信號驅動)
- 趙六(富人):拖來一卡車100支魚竿,全部用來釣魚,然后只用左右檢查是否有魚竿釣到魚。(多路復用)
- 田七(大老板):他知道他不是喜歡釣魚,而是喜歡吃魚。派小王去釣魚,他干自己的事,到時候負責吃就行。(異步IO)
一、阻塞IO
??張三釣魚的例子就是阻塞IO,它的特點是在內核將數據準備好之前,系統調?會?直等待。所有的套接字,默認都是阻塞?式。阻塞IO是最常見的IO模型。如下應用程序與內核的交互:
二、非阻塞IO
???阻塞IO:如果內核還未將數據準備好,系統調?仍然會直接返回,并且返回EWOULDBLOCK
錯誤碼。
???阻塞IO往往需要程序員循環的?式反復嘗試讀寫?件描述符,這個過程稱為輪詢。這對CPU來說是較?的浪費,?般只有特定場景下才使?。
阻塞和?阻塞關注的是程序在等待調?結果(消息,返回值)時的狀態。
- 阻塞調?是指調?結果返回之前,當前線程會被掛起,調?線程只有在得到結果之后才會返回。
- ?阻塞調?指在不能?刻得到結果之前,該調?不會阻塞當前線程。
文件默認情況下就是阻塞IO不用我們去修改,那么我們想要設置非阻塞IO要怎么做呢?
- 方法一:在文件
open
打開時添加O_NONBLOCK
標準位即可。
如果在文件打開之后想要設非阻塞IO呢? - 方法二:使用函數
fcntl
,需要包含頭文件unistd.h
和fcntl.h
int fcntl(int fd, int cmd, ... /* arg */ );
參數:
fd
:文件描述符(如 ```socket``、普通文件等)。cmd
:控制命令(如F_GETFL、F_SETFL
等)。F_GETL
:獲取當前文件描述符的標志,如O_RDONLY、O_NONBLOCK
等。F_SETFL
:設置文件描述符的標志。
arg
:可選參數,取決于cmd
。
返回值:
- 成功時返回值取決于 cmd,F_GETFL成功時返回
狀態標志符
,F_SETFL成功時返回0
,失敗時都返回-1
。 - 失敗時返回
-1
,并設置errno
(如 EBADF 無效描述符)。
獲取原標志符:int fg = fcntl(fd,F_GETFL);
設置O_NONBLOCK標志位:fcntl(fd, F_SETFL, fg | O_NONBLOCK)
??我們以read
為例,當將文件設為非阻塞后,我們再去讀取數據,即使數據沒就緒,也不會把程序掛起,而是直接返回一個小于0的結果繼續往下執行。所以數據并不一定一次read就讀取到,需要我們反復去read,也就是輪詢。
??不過還有一個問題,read失敗返回值小于0,因為數據未就緒返回值也是小于0,那么我們怎么區分呢?其實這兩種結果都有不同的erron
錯誤參數,到時候拿erron
去匹配就行,即:
EWOULDBLOCK或EAGAIN
:都表示數據未就緒。因為歷史版本原因所以有兩個版本,它們的值都是11,任意使用一個即可。EINTR
:因信號中斷導致的讀書失敗。- 其他:read讀取錯誤。
測試示例:
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main()
{int fd = 0;//獲取標志符int fn = fcntl(fd, F_GETFL);//增加O_NONBLOCK并設置fcntl(fd, F_SETFL, fn | O_NONBLOCK);int count = 0;while(1){char buffer[1024];int n = read(fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = '\0';cout<<buffer<<endl;sleep(1);}else if(n < 0){if(errno == EAGAIN || errno == EWOULDBLOCK){cout<<"文件未就緒..."<<endl;sleep(2);}else if(errno == EINTR){cout<<"信號中斷..."<<endl;continue;}else{perror("read fail\n");break;}}else{break;}}return 0;
}
三、信號驅動IO
信號驅動IO:內核將數據準備好的時候,使?SIGIO信號通知應?程序進?IO操作。
工作流程
- 注冊信號處理程序:應用進程首先使用
sigaction
系統調用,建立針對特定信號(通常是SIGIO
)的信號處理程序。在這一步,進程告知內核,當特定的 I/O 事件發生時,應該觸發哪個信號處理函數。 - 設置文件描述符:應用進程需要將相關的文件描述符(如套接字描述符)設置為信號驅動 I/O 模式,并指定該文件描述符的屬主(通常使用
fcntl
系統調用的F_SETOWN
命令設置屬主為當前進程 )。這一步確保內核知道當 I/O 事件就緒時,應該向哪個進程發送信號。 - 內核等待數據:完成上述設置后,應用進程可以繼續執行其他任務,而內核開始等待數據到達。當數據報準備好(比如有數據到達套接字 )時,內核會向應用進程發送預先設置好的信號(如
SIGIO
)。 - 信號處理:應用進程接收到
SIGIO
信號后,會暫停當前正在執行的任務,轉而執行之前注冊的信號處理程序。在信號處理程序中,通常會調用諸如recvfrom
這樣的函數來讀取數據。在數據從內核拷貝到用戶空間的應用緩沖區期間,進程會阻塞(這是信號處理函數內部執行具體 I/O 操作時的阻塞 ),直到數據拷貝完成。 - 處理數據:數據讀取完成后,信號處理程序執行結束,進程恢復之前被暫停的任務,或者開始對讀取到的數據進行處理。
四、IO多路復用
??IO多路復用允許單個線程/進程同時監控多個文件描述符(如套接字、管道等)的 I/O 事件,并在任一描述符就緒時進行讀寫操作。它是高并發網絡編程的核心技術之一。是真正意義上的高效 I/O 處理機制,它在單位時間內增加了傳輸效率,而其他四個只是在利用等的時間而已,并沒有提高IO效率。
核心思想
- 統一監控:通過系統調用(如
select、poll、epol
l)一次性注冊多個文件描述符,由內核通知哪些描述符已就緒(可讀/可寫/異常)。 - 避免阻塞輪詢:無需為每個描述符創建獨立線程,節省資源。
- 單線程高并發:一個線程即可處理成千上萬的連接(如 Nginx、Redis 的底層模型)。
??關于IO多路復用的很重要,涉及select/poll/epoll
,有繁多的細節和復雜的底層邏輯,統一放在下一章進行講解。
五、異步IO
??核心思想是 “發起 I/O 請求后立即返回,由內核完成所有操作(包括數據拷貝),并通過回調或信號通知進程結果”。與同步 I/O 不同,進程無需等待 I/O 完成即可繼續執行其他任務,真正實現 非阻塞 和 完全異步。
異步IO特性
- 非阻塞調用:調用 I/O 函數(如 aio_read)立即返回,不阻塞進程。
- 內核全權處理:內核負責數據準備、拷貝到用戶空間,全程無需進程參與。
- 通知機制:操作完成后,內核通過回調、信號或事件通知進程。
- 全程無等待:進程從發起請求到獲取結果均無需阻塞,最大化利用 CPU。
IO模型 | 控制方式 | 優勢 | 劣勢 |
---|---|---|---|
阻塞 I/O | 進程阻塞等待數據 | 編程簡單 | 無法并發處理多 I/O |
非阻塞 I/O | 輪詢檢查數據是否就緒 | 可同時處理其他任務 | 輪詢消耗 CPU |
I/O多路復用 | select/epoll | 統一監控 | 高并發支持 |
信號驅動 I/O | 內核信號通知 | 無輪詢,延遲低 | 信號處理復雜,不適合高頻場景 |
異步 I/O (AIO) | 內核完成所有操作后回調 | 真正的異步,全程無阻塞 | 實現復雜,部分系統支持不完善 |
非常感謝您能耐心讀完這篇文章。倘若您從中有所收獲,還望多多支持呀!
?