(四)五種IO模型

昀溪發表於2018-09-28

基本概念

我們之前編寫的套接字程式都是阻塞式的,其實這也是預設的形式。現在我們需要明確一些概念:

使用者空間和核心空間

 

首先要明確,使用者啟動的應用程式在系統中以一個程式的形式存在,而無論對於網路資料還是磁碟資料通常來講這個程式都無法直接訪問,必須由核心把資料複製到使用者空間也就是程式所在的記憶體空間裡這個程式才可以訪問。上圖結合了網路請求和磁碟資料,使用者發起一個HTTP請求,請求一個HTML頁面,這個頁面就在磁碟上,其實就是把這個HTML頁面傳送給使用者,使用者的瀏覽器解析HTML語言就顯示了頁面。使用者請求一個頁面其實就是阻塞的,瀏覽器必須等待所有資料返回後才能正確顯示這個頁面。所以這個請求的整個過程是阻塞的也是同步的,因為你無法讓當前這個瀏覽器標籤在等待伺服器返回資料的時候去幹別的。

其實對於我們之前寫的程式也是一樣,雖然伺服器做的事情就是回顯客戶端傳送的資料,其實客戶端傳送過來也是先進入伺服器核心的TCP協議棧,而你的服務端程式執行在使用者空間,核心需要把這個資料從核心空間複製到使用者空間,然後伺服器端程式才能拿到進而進行處理然後在進行傳送,傳送的過程其實就是接受的反向過程。

名詞解釋

阻塞:程式發起IO呼叫時,如果這個IO沒有準備好資料那麼程式呼叫的這個函式將不會返回,那麼這個程式就要進入睡眠狀態,也就是當前程式會被掛起。當資料複製到使用者空間後才返回。

非阻塞:程式發起IO呼叫時,被呼叫函式在完成IO之前不會阻塞當前程式,而是立即返回,返回的含義你可以理解為資料還沒準備好。雖然程式不阻塞,但是它需要頻繁的呼叫之前的函式看看資料有沒有準備好,所以這就是忙等,也叫輪訓。

同步:程式發起一個過程呼叫(功能、函式)呼叫後,在沒得到結果之前,該呼叫將不會返回。相當於你買麥當勞,你拿著小票在取餐處等著,如果你的餐沒有做好那麼服務員是不會給你端上來的。

非同步:程式發起一個過程呼叫,即使不能立即得到結果,但也會得到返回值。當IO完成後,核心會通知程式資源已經準備好了,你可以來讀取了。你可以理解為飯店點菜,點好了你可以幹別的,菜好了服務員就給你端上來。

同步和阻塞、非同步和非阻塞看起來概念上很像,但是他們所描述的物件不同,同步或非同步是被呼叫者如何響應呼叫者,而阻塞或非阻塞是呼叫者如何被處理的。或者換句話說阻塞或非阻塞這種狀態的描述物件是程式也就呼叫者被如何處理的,而同步或非同步的這種處理方式所描述的物件是被呼叫者會如何響應呼叫者的。所以說的是同一個事情,但是描述的時候所站在的角度不同。

如果要把同步、非同步、阻塞和非阻塞對應到後面要說的IO模型上的話,那麼阻塞和非阻塞就是程式呼叫的系統函式是否立即返回無論資料準備好沒有;同步和非同步就是資料準備好後從核心空間複製到使用者空間的過程中程式是否阻塞。

五種呼叫模型

阻塞式IO

 

阻塞式IO也是我們之前一直使用的模式其實也是預設模式。一次系統呼叫分為2個階段:

  1. 資料從磁碟到核心空間的緩衝區
  2. 核心把核心空間的資料複製到程式空間,然後刪除核心空間資料

程式呼叫recvfrom就被阻塞,這時候就等資料,資料準備好了就從核心空間複製到使用者空間,複製完成之後recvfrom函式才返回,這時候程式才開始對資料進行處理。在上面的圖示資料準備好我們理解為資料可讀,結合之前的例子就是我們的服務端程式阻塞在accept處,當有新連線進來之後連線套接字變為可讀這時候就accept就返回了,看下圖程式碼:

另外在接收客戶端資料的時候服務端呼叫recv函式其實也是阻塞在這裡,因為它要等著客戶端發資料來,如下圖:

因為connFd代表一個與客戶端的連線套接字,這個套接字可讀證明有資料到達且被複制到程式的緩衝區(也就是使用者空間),這時候這個函式才返回,否則就一直阻塞。

非阻塞式IO

你明白了上面的阻塞式IO後再看這個非阻塞式的就很好理解,呼叫函式不阻塞立即返回,但是核心不會告訴你啥時候資料到達,你的反覆呼叫,如果第N次呼叫剛好資料準備好,那麼就阻塞了,這時候開始等待資料複製,複製完畢函式返回。這種方式會大量消耗CPU時間。

IO複用

在這種模型中,這時候並不是程式直接發起資源請求的系統呼叫去請求資源,程式不會被“全程阻塞”,程式是呼叫select或poll函式。程式不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll幫助使用者程式去輪詢那些IO操作是否完成。

不過你可以看到之前都只使用一個系統呼叫,在IO複用中反而是用了兩個系統呼叫,但是使用IO複用你就可以等待多個描述符也就是透過單程式單執行緒實現併發處理,同時還可以兼顧處理套接字描述符和其他描述符。

訊號驅動模型

讓核心在檔案描述符就緒時透過訊號通知程式。從上圖可以看出這種模型程式建立訊號處理程式然後立即返回,當資料準備好後傳送訊號通知程式,然後程式呼叫recvfrom系統呼叫進行復制資料,當資料複製到使用者空間後函式返回(訊號驅動機制需要核心支援)。這就相當於是你點菜下單後可以繼續幹其他的事情雖然菜好了會通知你,但是你需要自己去拿。但這裡有個情況,如果通知程式來拿資料,但是程式沒來怎麼辦?這就引出了水平觸發和邊緣觸發概念

邊緣觸發(Edge triggered):以epoll為例,當被監控的檔案描述符上有可讀寫事件時,被呼叫者(epoll_wait())通知程式去讀寫,如果一次沒有把全部資料讀寫完畢比如讀寫緩衝區太小,那麼下次呼叫epoll_wait()時被呼叫者它不會通知程式,它只通知一次,直到該檔案描述符上出現第二次可讀寫事件才會通知。訊號驅動IO是邊緣觸發模型,epoll()支援水平也支援邊緣,預設是水平觸發。

水平觸發(Level triggered):以epoll為例,當被監控的檔案描述符上有可讀寫事件時,被呼叫者(epoll_wait())通知程式去讀寫,如果一次沒有把全部資料讀寫完畢比如讀寫緩衝區太小,那麼下次呼叫epoll_wait()時,它還會通知程式在上次沒有讀寫完的檔案描述符上繼續讀寫,當然如果你一直不讀寫,它會一直通知。如果系統中有大量你需要讀寫的檔案描述符,而且它每次都返回,這會大大降低程式檢查自己關心的檔案描述符的效率,相比之下邊緣觸發效率更高。Select()和poll()都是水平觸發的。

非同步IO模型

這種模型可以看出全程無阻塞,就等於你點菜下單後你可以續幹其他的事情,菜做好了服務員會給你端上來,剩下的就是你如何處理這些菜。從效率上來講非同步IO模型是最高的。

5種IO模型比較

從上圖可以效率從左至右逐步提高,其實IO複用也不是不阻塞,只是阻塞在像select這種IO複用函式這裡。

相關文章