日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

阻塞IO、非阻塞IO、IO多路復用、AIO的區別

作者:沉淀的老山羊 更新時間: 2024-02-17 編程語言

在講解這三個I/O操作之前先普及一下I/O的基礎知識,不然聽后面的點會產生困惑,有基礎的朋友可以從BIO開始閱讀

什么是I/O操作?

I/O(Input/Output)操作指的是計算機系統與外部設備或程序之間的數據傳輸。I/O 操作包括讀取和寫入數據,用于在計算機系統和外部環境之間進行信息交換。

I/O 操作可以分為兩大類:

  1. 輸入操作(Input):
    • 從外部設備或其他程序讀取數據到計算機系統中。
    • 例子:
      • 從鍵盤輸入數據。
      • 從磁盤讀取文件內容。
      • 從網絡接收數據。
  2. 輸出操作(Output):
    • 將計算機系統中的數據發送到外部設備或其他程序。
    • 例子:
      • 向屏幕打印輸出信息。
      • 將數據寫入磁盤文件。
      • 向網絡發送數據。

I/O 操作是計算機系統中非常重要的一部分,因為計算機系統通常需要與外部世界進行交互。外部設備包括鍵盤、鼠標、磁盤驅動器、網絡接口等,而程序之間的數據傳輸也屬于 I/O 操作。

在計算機中,I/O 操作的速度相對較慢,因此在編程中,優化和有效地管理 I/O 操作對于提高系統性能和響應速度至關重要。對于高效的 I/O 操作,涉及到使用適當的 I/O 模型、緩沖、異步操作等技術。

用戶空間與內核空間

在這里插入圖片描述

操作系統的核心是內核,獨立于普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。

進程不能直接訪問硬件設備,當進程需要訪問硬件設備(比如讀取磁盤文件,接收網絡數據等等)時,必須由用戶態模式切換至內核態模式,通過系統調用訪問硬件設備。

文件描述符fd

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統。

應用程序中如何進行I/O操作?

我們程序中的IO讀寫其實調用的是操作系統內核中的read&write兩大系統調用

例如使用Java通過socket進行網絡I/O,也必須依賴系統內核

具體步驟:

  1. 網卡收到網線傳來的網絡數據,并將數據寫入內存
  2. 數據寫入內存后,網卡向cpu發送中斷信號(通知發生特定事件的一種機制),操作系統遍能得知有新數據到來,再通過網卡中斷程序去處理數據
  3. 將內存中的數據寫入到對應的socket的接收緩沖區中
  4. 當接收緩沖區的數據寫好后,應用程序開始進行數據處理
public class SocketServer {
  public static void main(String[] args) throws Exception {
    // 監聽指定的端口
    int port = 8080;
    ServerSocket server = new ServerSocket(port);
    // server將一直等待連接的到來
    Socket socket = server.accept();
    // 建立好連接后,從socket中獲取輸入流,并建立緩沖區進行讀取
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[1024];
    int len;
    // 此處的read操作是阻塞操作
    while ((len = inputStream.read(bytes)) != -1) {
      //獲取數據進行處理
      String message = new String(bytes, 0, len,"UTF-8");
    }
    // socket、server,流關閉操作,省略不表
  }
}

?? 以下幾種IO模型的區分點在于:

  1. 數據等待階段
  2. 將數據從內核空間的buffer拷貝到用戶空間進程的buffer

阻塞IO(blocking IO)

在這里插入圖片描述

特點:在IO執行的兩個階段都被block了

造成的影響

? 意味著如果在等待客戶端的連接或者處理讀寫請求時,服務端不能去做任何事情,當前操作完成,假如服務端在等待客戶端的寫操作,而客戶端一直沒響應,那么服務端就“卡死了”(不能處理其他客戶端的請求)

解決方法

? 使用多線程,每當一個客戶端連接上服務端,就專門開啟一個線程處理這個客戶端的請求,服務端能夠正常處理每一個客戶端的請求,主線程不會被阻塞

? 缺點:假如同時有1000個客戶端同時訪問服務端,就需要開啟1000個線程去處理,并且這1000個線程同時阻塞等待客戶端的I/O操作,嚴重浪費了CPU和內存的資源

總結

  • 優點

    • 編程模型簡單,易于理解
    • 適用于低并發,低負載的場景
  • 缺點

    • 阻塞 I/O 會導致線程被阻塞,無法應對高并發場景
    • 在高并發環境下,阻塞 I/O 可能導致大量線程被創建,增加系統開銷

非阻塞IO(nonblocking IO)

在這里插入圖片描述

特點:如果數據尚未準備好,不會一直等待,而是一邊向下執行任務一邊向內核詢問數據準備好了沒

舉個栗子

現在你是一個服務員(服務端)。當一個顧客坐下后點菜,然后開始等待他的菜做好。在這個等待的過程中,你可以去做其他事情,不需要一直等在那里,但是你需要隔一段時間就去問廚師起先顧客的菜好了嗎。

優點:解決了BIO的阻塞問題,在沒有I/O操作時,不會發生阻塞,會繼續處理其他任務,提高并發能力

缺點:一直去輪詢I/O操作是否完成,會造成CPU資源的浪費。就像是一個顧客剛點完菜,服務員就一直問菜煮好了嗎??????(內心:又不是預制菜??,哪有那么快),菜準備的越久,越浪費服務員的精力。

I/O多路復用

無論是阻塞I0還是非阻塞I0,用戶應用在一階段都需要調用recvfrom來獲取數據,差別在于無數據時的處理方案:

  • 如果調用recvfrom時,恰好沒有數據,阻塞I0會使進程阻塞,非阻塞I0使CPU空轉,都不能充分發揮CPU的作用。
  • 如果調用recvfrom時,恰好有數據,則用戶進程可以直接進入第二階段,讀取并處理數據

比如服務端處理客戶端Socket請求時,在單線程情況下,只能依次處理每一個socket,如果正在處理的socket恰好未就緒(數據不可讀或不可寫),線程就會被阻塞,所有其它客戶端socket都必須等待,性能自然會很差。

這就像服務員給顧客點餐,分兩步:

  1. 顧客思考要吃什么(等待數據就緒)
  2. 顧客想好了,開始點餐(讀取數據)要提高效率有幾種辦法?
  • 方案一:增加更多服務員(多線程)

  • 方案二:不排隊,誰想好了吃什么(數據就緒了),服務員就給誰點餐(用戶應用就去讀取數據)

那么問題來了:用戶進程如何知道內核中數據是否準備好了?這就需要使用上面開頭說的文件描述符fd

I/O多路復用:是利用單個線程來同時監聽多個FD,并在某個FD可讀、可寫時得到通知,從而避免無效的等待,充分利用CPU資源。

在這里插入圖片描述

問題:

在IO多路復用的時候,處理數據的兩個階段都需要阻塞等待,那與非阻塞又有什么區別呢?

答:非阻塞的痛點在于什么?雖然解決了單個線程在進行I/O時會被阻塞的問題,但是依然沒有解決單線程下無法處理多個socket的問題。但是I/O多路復用可以同時處理多個socket。

I/O多路復用模型的實現
select
//定義類型別名_-fd_mask,本質是longint
typedef long int __fd_mask;
/*fd_set記錄要監聽的fd集合,及其對應狀態*/
typedef struct {
//fds_bits是long類型數組,長度為1024/32=32
//共1024個bit位,每個bit位代表一個fd,0代表未就緒,1代表就緒
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS] ;
//...
}fd_set;
//select函數,用于監聽多個fd的集合
int select(
intnfds,//要監視的fd_set的最大fd+1
fd_set*readfds,//要監聽讀事件的fd集合
fd_set*writefds,//要監聽寫事件的fd集合 
fd_set*exceptfds,// 要監聽異常事件的fd集合
//超時時間,null-永不超時;0-不阻塞等待;大于0-固定等待時間
struct timeval *timeout
);

在這里插入圖片描述

select存在的問題:

  • 需要將整個fd_set從用戶空間拷貝到內核空間,select結束還要再次拷貝回用戶空間
  • select無法得知具體是哪個fd就緒,需要遍歷整個fd_set
  • fd_set監聽的fd數量不能超過1024

當用戶調用了select,那么整個進程會被block,而同時,系統會監視所有select負責的socket,當任何一個socket的數據準備好了,select就會返回。

poll
//pollfd中的事件類型
#define POLLIN //可讀事件
#define POLLOUT //可寫事件
#define POLLERR //錯誤事件
#define POLLNVAL //fd未打開
//pollfd結構
struct pollfd/*要監聽的fd*/
short int events;/*要監聽事件類型:讀、寫、異常*/
short int revents;/*實際發生的事件類型*/
};
//Poll函數
int poll (
struct pollfd* fds,//pollfd數組,可以自定義大小
nfds_t nfds,//數組元素個數
int timeout//超時時間
);

I0流程:

  1. 創建pollfd數組,向其中添加關注的fd信息,數組大小自定義
  2. 調用poll函數,將pollfd數組拷貝到內核空間,轉鏈表存儲,無上限
  3. 內核遍歷fd,判斷是否就緒
  4. 數據就緒或超時后,拷貝polfd數組到用戶空間,返回就緒fd數量n
  5. 用戶進程判斷n是否大于0
  6. 大于0則遍歷pollfd數組,找到就緒的fd

與select對比:

  • 優點:select模式中的fd_set大小固定為1024,而pollfd在內核中采用
    鏈表,理論上無上限
  • 缺點:監聽FD越多,每次遍歷消耗時間也越久
epoll

在這里插入圖片描述

在這里插入圖片描述

步驟

  1. 創建epoll實例
  2. 添加要監聽的FD到紅黑樹,關聯callback
  3. epoll_wait等待FD就緒,如果有FD就緒后,會將FD添加到list_head中,在用戶調用epoll_wait后就會將這些就緒的FD拷貝到event數組中,相比于前兩種監聽模式,epoll不需要遍歷所有的FD集合就知道哪些FD就緒
總結

select模式存在的三個問題:

  • 能監聽的FD最大不超過1024
  • 每次select都需要把所有要監聽的FD都拷貝到內核空間
  • 每次都要遍歷所有的FD來判斷就緒狀態

poll存在的問題:

  • poll雖然解決了select監聽FD上限的問題,但是隨著監聽FD數量的上升,性能反而會下降

epoll如何解決這些問題:

解決FD上限問題:基于epoll實例中的紅黑數保存要監聽的FD,理論上無上限,而增刪改查銷量都非常高,性能不會隨著FD數量增多反而下降

FD拷貝問題:每一個FD只需要執行一次epoll_ctl添加到紅黑樹,以后每次epoll_wait無需傳遞任何參數,無需重復拷貝FD到內核空間

查找FD效率低問題:內核會將就緒的FD直接拷貝到用戶空間的指定位置,用戶進程無需遍歷所有FD就能知道就緒的FD是誰

異步IO(async IO)

上面三種IO模型都有一個共同的缺點:當系統中數據準備好的時候,recvfrom會將數據從內核空間拷貝到用戶內存中,在這段時間內,進程是被阻塞的

在這里插入圖片描述

AIO 就是用來解決數據拷貝階段的阻塞問題

  • 同步意味著,在進行讀寫操作時,線程需要等待結果,還是相當于閑置
  • 異步意味著,在進行讀寫操作時,線程不必等待結果,而是將來由操作系統來通過回調方式由另外的線程來獲得結果

異步模型需要底層操作系統(Kernel)提供支持

  • Windows 系統通過 IOCP 實現了真正的異步 IO
  • Linux 系統異步 IO 在 2.6 版本引入,但其底層實現還是用多路復用模擬了異步 IO,性能沒有優勢

異步IO整個操作都是非阻塞的,用戶進程調用完異步API后就可以去做其它事情,內核等待數據就緒并拷貝到用戶空間后才會遞交信號,通知用戶進程

原文鏈接:https://blog.csdn.net/m0_62963408/article/details/136074921

  • 上一篇:沒有了
  • 下一篇:沒有了
欄目分類
最近更新