1. 程式人生 > 實用技巧 >I/O模型、阻塞型I/O、非阻塞型I/O、I/O複用

I/O模型、阻塞型I/O、非阻塞型I/O、I/O複用

I/O模型

有五種I/O模型:

  1. 阻塞型I/O:blocking IO
  2. 非阻塞型I/O:nonblocking IO
  3. I/O複用:IO multiplexing
  4. 訊號驅動I/O:signal driven IO
  5. 非同步I/O:asynchronous IO

對於一個network IO (以read舉例),它會涉及到兩個系統物件:一個是呼叫這個IO的程序,另一個就是系統核心(kernel)。當一個read操作發生時,它會經歷兩個階段:

階段1: 等待資料準備 (Waiting for the data to be ready)

階段2: 將資料從核心拷貝到程序中 (Copying the data from the kernel to the process)

img

阻塞型I/O

阻塞型IO就是,資料的讀入和寫出都在一個執行緒內,需要等待其完成。

​ 當在使用阻塞IO的時候,應用程式會被無情的掛起,等待核心完成操作,因為此時的核心可能將CPU時間切換到了其它需要的程序中,在我們的應用程式看來感覺被卡主(阻塞)了。

​ 採用 BIO 通訊模型 的服務端,通常由一個獨立的 Acceptor 執行緒負責監聽客戶端的連線。我們一般通過在while(true) 迴圈中服務端會呼叫 accept() 方法等待接收客戶端的連線的方式監聽請求,請求一旦接收到一個連線請求,就可以建立通訊套接字在這個通訊套接字上進行讀寫操作,此時不能再接收其他客戶端連線請求,只能等待同當前連線的客戶端的操作執行完成, 不過可以通過多執行緒來支援多個客戶端的連線,如上圖所示。

img

問題:

我們都知道重複的進行建立銷燬執行緒是很耗費系統資源的,並且其執行緒的利用率也不是很高。

非阻塞型I/O

當使用非阻塞函式的時候,和阻塞IO類比,核心會立即返回,返回後獲得足夠的CPU時間繼續做其它的事情。

當用戶程序發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回一個error。 從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次 傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

所以,使用者程序第一個階段不是阻塞的,需要不斷的主動詢問kernel資料好了沒有;第二個階段依然總是阻塞的。

NIO

I/O複用

IO複用(IO multiplexing),也稱事件驅動IO(event-driven IO),就是在單個執行緒裡同時監控多個套接字,通過 select 或 poll 輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程序。

IO複用同非阻塞IO本質一樣,不過利用了新的select系統呼叫,由核心來負責本來是請求程序該做的輪詢操作。看似比非阻塞IO還多了一個系統呼叫開銷,不過因為可以支援多路IO,才算提高了效率。

I/O複用的特點是進行了兩次系統呼叫,程序先是阻塞在 select/poll 上,再是阻塞在讀操作的第二個階段上。

img

select的缺點

  • select返回的是含有整個控制代碼的陣列,應用程式需要遍歷整個陣列才能發現哪些控制代碼發生了事件
  • select的觸發方式是水平觸發,應用程式如果沒有完成對一個已經就緒的檔案描述符進行IO操作,那麼之後每次select呼叫還是會將這些檔案描述符通知程序
  • 核心 / 使用者空間記憶體拷貝問題,select每次都會改變核心中的控制代碼資料結構集,因而每次select呼叫時都需要從使用者空間向核心空間複製所有的控制代碼資料結構,產生巨大的開銷
  • 單個程序能夠監視的檔案描述符的數量存在最大限制,通常是1024,當然可以更改數量

epoll實現

epoll在核心中會維護一個紅黑樹和一個雙向連結串列,紅黑樹存放通過epoll_ctl方法向epoll物件中新增進來的事件,所以不需要每次呼叫epoll_wait都全量複製所有的事件結構。雙向連結串列存放就緒的事件,所有新增到epoll中的事件都會與裝置(網絡卡)驅動程式建立回撥關係,也就是說,當相應的事件發生時會呼叫這個回撥方法,這個回撥方法在核心中叫ep_poll_callback,它會將發生的事件新增到rdlist雙鏈表中。呼叫epoll_wait就會直接返回連結串列中的就緒事件,效率高。

缺點:

我們現在可以使用select/poll等系統呼叫進行事件的檢測,但是select會返回所有建立連線的檔案描述符,我們需要一個一個去遍歷檔案描述符,才能知道哪些資料準備好了,然後才能進行後續的操作,但是如果有10K個連線過來,然後我們呼叫一次select 或者poll的函式呼叫,然後去遍歷所有的檔案描述符,結果只有兩個客戶端的資料準備好了,那麼我們剩下的9998次都是在空轉,消耗了很多的系統資源。

訊號驅動I/O

訊號驅動IO與BIO和NIO最大的區別就在於,在IO執行的資料準備階段,不會阻塞使用者程序。
如下圖所示:當用戶程序需要等待資料的時候,會向核心傳送一個訊號,告訴核心我要什麼資料,然後使用者程序就繼續做別的事情去了,而當核心中的資料準備好之後,核心立馬發給使用者程序一個訊號,說”資料準備好了,快來查收“,使用者程序收到訊號之後,立馬呼叫recvfrom,去查收資料。

SIGIO

非同步I/O

非同步IO真正實現了IO全流程的非阻塞。使用者程序發出系統呼叫後立即返回,核心等待資料準備完成,然後將資料拷貝到使用者程序緩衝區,然後傳送訊號告訴使用者程序IO操作執行完畢(與SIGIO相比,一個是傳送訊號告訴使用者程序資料準備完畢,一個是IO執行完畢)。其流程如下:

AIO

img

馬士兵講BIO、NIO、AIO


我的個人部落格地址:https://dlddw.xyz/
所有文章均在個人部落格中首發。歡迎小夥伴們訪問和留言!