1. 程式人生 > >面試準備之IO模型

面試準備之IO模型

一些概念:

同步和非同步

同步和非同步是針對應用程式和核心的互動而言的,同步指的是使用者程序觸發I/O操作並等待或者輪詢的去檢視I/O操作是否就緒,而非同步是指使用者程序觸發I/O操作以後便開始做自己的事情,而當I/O操作已經完成的時候會得到I/O完成的通知。

阻塞和非阻塞

阻塞和非阻塞是針對於程序在訪問資料的時候,根據I/O操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函式的實現方式,阻塞方式下讀取或者寫入函式將一直等待,而非阻塞方式下,讀取或者寫入函式會立即返回一個狀態值。

伺服器端幾種模型:

1、阻塞式模型(blocking IO)

我們第一次接觸到的網路程式設計都是從 listen()、accpet()、send()、recv() 等介面開始的。使用這些介面可以很方便的構建C/S的模型。這裡大部分的 socket 介面都是阻塞型的。所謂阻塞型介面是指系統呼叫(一般是 IO 介面)不返回呼叫結果並讓當前執行緒一直阻塞,只有當該系統呼叫獲得結果或者超時出錯時才返回。

如下面一個簡單的Server端實現:

 View Code

示意圖如下:

這裡的socket的介面是阻塞的(blocking),線上程被阻塞期間,執行緒將無法執行任何運算或響應任何的網路請求,這給多客戶機、多業務邏輯的網路程式設計帶來了挑戰。

2、多執行緒的伺服器模型(Multi-Thread)

應對多客戶機的網路應用,最簡單的解決方式是在伺服器端使用多執行緒(或多程序)。多執行緒(或多程序)的目的是讓每個連線都擁有獨立的執行緒(或程序),這樣任何一個連線的阻塞都不會影響其他的連線。

多執行緒Server端的實現:

 View Code

上述多執行緒的伺服器模型可以解決一些連線量不大的多客戶端連線請求,但是如果要同時響應成千上萬路的連線請求,則無論多執行緒還是多程序都會嚴重佔據系統資源,降低系統對外界響應效率。

在多執行緒的基礎上,可以考慮使用“執行緒池”或“連線池”,“執行緒池”旨在減少建立和銷燬執行緒的頻率,其維持一定合理數量的執行緒,並讓空閒的執行緒重新承擔新的執行任務。“連線池”維持連線的快取池,儘量重用已有的連線、減少建立和關閉連線的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統。

3、非阻塞式模型(Non-blocking IO)

非阻塞的介面相比於阻塞型介面的顯著差異在於,在被呼叫之後立即返回。

非阻塞型IO的示意圖如下:

從應用程式的角度來說,blocking read 呼叫會延續很長時間。在核心執行讀操作和其他工作時,應用程式會被阻塞。

非阻塞的IO可能並不會立即滿足,需要應用程式呼叫許多次來等待操作完成。這可能效率不高,因為在很多情況下,當核心執行這個命令時,應用程式必須要進行忙碌等待,直到資料可用為止。

另一個問題,在迴圈呼叫非阻塞IO的時候,將大幅度佔用CPU,所以一般使用select等來檢測”是否可以操作“。

4、多路複用IO

支援I/O複用的系統呼叫有select、poll、epoll、kqueue等,

這裡以Select函式為例,select函式用於探測多個檔案控制代碼的狀態變化,以下為一個使用了使用了Select函式的Server實現:

 View Code

示意圖如下:

這裡Select監聽的socket都是Non-blocking的,所以在do_read() do_write()中對返回為EAGAIN/WSAEWOULDBLOCK都做了處理。

從程式碼中可以看出使用Select返回後,仍然需要輪訓再檢測每個socket的狀態(讀、寫),這樣的輪訓檢測在大量連線下也是效率不高的。因為當需要探測的控制代碼值較大時,select () 介面本身需要消耗大量時間去輪詢各個控制代碼。

很多作業系統提供了更為高效的介面,如 linux 提供 了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要實現更高效的伺服器程式,類似 epoll 這樣的介面更被推薦。遺憾的是不同的作業系統特供的 epoll 介面有很大差異,所以使用類似於 epoll 的介面實現具有較好跨平臺能力的伺服器會比較困難。

5、使用事件驅動庫libevent的伺服器模型

Libevent 是一種高效能事件迴圈/事件驅動庫。

為了實際處理每個請求,libevent 庫提供一種事件機制,它作為底層網路後端的包裝器。事件系統讓為連線新增處理函式變得非常簡便,同時降低了底層IO複雜性。這是 libevent 系統的核心。

建立 libevent 伺服器的基本方法是,註冊當發生某一操作(比如接受來自客戶端的連線)時應該執行的函式,然後呼叫主事件迴圈 event_dispatch()。執行過程的控制現在由 libevent 系統處理。註冊事件和將呼叫的函式之後,事件系統開始自治;在應用程式執行時,可以在事件佇列中新增(註冊)或 刪除(取消註冊)事件。事件註冊非常方便,可以通過它新增新事件以處理新開啟的連線,從而構建靈活的網路處理系統。

使用Libevent實現的一個回顯伺服器如下:

 View Code

6、訊號驅動IO模型(Signal-driven IO)

使用訊號,讓核心在描述符就緒時傳送SIGIO訊號通知應用程式,稱這種模型為訊號驅動式I/O(signal-driven I/O)。

圖示如下:

首先開啟套接字的訊號驅動式I/O功能,並通過sigaction系統呼叫安裝一個訊號處理函式。該系統呼叫將立即返回,我們的程序繼續工作,也就是說程序沒有被阻塞。當資料報準備好讀取時,核心就為該程序產生一個SIGIO訊號。隨後就可以在訊號處理函式中呼叫recvfrom讀取資料報,並通知主迴圈資料已經準備好待處理,也可以立即通知主迴圈,讓它讀取資料報。

無論如何處理SIGIO訊號,這種模型的優勢在於等待資料報到達期間程序不被阻塞。主迴圈可以繼續執行 ,只要等到來自訊號處理函式的通知:既可以是資料已準備好被處理,也可以是資料報已準備好被讀取。

7、非同步IO模型(asynchronous IO)

非同步I/O(asynchronous I/O)由POSIX規範定義。演變成當前POSIX規範的各種早起標準所定義的實時函式中存在的差異已經取得一致。一般地說,這些函式的工作機制是:告知核心啟動某個操作,並讓核心在整個操作(包括將資料從核心複製到我們自己的緩衝區)完成後通知我們。這種模型與前一節介紹的訊號驅動模型的主要區別在於:訊號驅動式I/O是由核心通知我們何時可以啟動一個I/O操作,而非同步I/O模型是由核心通知我們I/O操作何時完成。

示意圖如下:

我們呼叫aio_read函式(POSIX非同步I/O函式以aio_或lio_開頭),給核心傳遞描述符、緩衝區指標、緩衝區大小(與read相同的三個引數)和檔案偏移(與lseek類似),並告訴核心當整個操作完成時如何通知我們。該系統呼叫立即返回,並且在等待I/O完成期間,我們的程序不被阻塞。本例子中我們假設要求核心在操作完成時產生某個訊號,該訊號直到資料已複製到應用程序緩衝區才產生,這一點不同於訊號驅動I/O模型。