用一幅圖表示所支援的i/o模型
縱向維度是「阻塞(blocking)」、「非阻塞(non-blocking)」;橫向維度是「同步」、「非同步」。總結起來是四種模型 同步阻塞、同步非阻塞;非同步阻塞、非同步非阻塞 。《unix網路程式設計》中劃分出了「第五種」模型——「訊號驅動式io」其實屬於非同步阻塞型別,這種模型的通知方式有多種多樣後面展開說明。
從核心角度看i/o操作分為兩步:使用者層api呼叫;核心層完成系統呼叫(發起i/o請求)。所以「非同步/同步」的是指api呼叫;「阻塞/非阻塞」是指核心完成i/o呼叫的模式。用一幅圖表示更加明顯
同步是指函式完成之前會一直等待; 阻塞 是指系統呼叫的時候程序會被設定為sleep狀態直到等待的事件發生(比如有新的資料)。明白這一點之後再看這五種模型相信就會清晰很多,我們挨個分析:
同步阻塞
這種模型最為常見,使用者空間呼叫api(read
、write
)會轉化成乙個i/o請求,一直等到i/o請求完成api呼叫才會完成。這意味著: 在api呼叫期間使用者程式是同步的的;這個api呼叫會導致系統以阻塞的模式執行i/o,如果此時沒有資料則一直「等待」(放棄cpu主動掛起——sleep狀態) (注意,對於硬碟來說是不會出現阻塞的,無論是什麼時候讀它總是有資料。常見的阻塞裝置是終端、網絡卡之類的)。
以read
為例子,它由三個引數組成,第乙個函式是檔案描述符;第二個是 應用緩衝 ;第三個引數是需要讀取的位元組數。經過系統呼叫會以阻塞模式執行i/o,i/o模組讀取資料後會放入到pagecache中;最後一步是把資料從pagecache複製到 應用緩衝 。如果i/o請求無法得到滿足——沒有資料,則主動讓出cpu直到 有資料 (注意,即便系統呼叫讓出cpu也未必真的就讓出。read函式是同步的,所以cpu還是會被使用者空間**占用)。
同步非阻塞
這種模式通過呼叫read
、write
的時候指定o_nonblock
引數。和「同步阻塞」模式的區別在於系統呼叫的時候它是以非阻塞的方式執行,無論是否有資料都會立即返回。 以read
為例,如果成功讀取到資料它返回讀取到的位元組數;如果此時沒有資料則返回-1,同時設定errno為eagain(或者ewouldblock,二者相同)。所以這種模式下我們一般會用乙個「迴圈」不停的嘗試讀取資料,處理資料。
非同步阻塞
同步模型最主要的問題是占用cpu, 阻塞i/o會主動讓出cpu但是使用者空間的系統呼叫還是不會返回依然耗費cpu;非阻塞i/o必須不停的「輪詢」不斷嘗試讀取資料(會耗費更多cpu更加低效)。如果仔細分析同步模型霸佔cpu的原因不難得出結論——都是在等待資料到來。非同步模式正是意識到這一點所以把i/o讀取細化為 訂閱i/o事件,實際i/o讀寫,在「訂閱i/o事件」事件部分會主動讓出cpu直到事件發生 。非同步模式下的i/o函式和同步模式下的i/o函式是一樣的(都是read
、write
)唯一的區別是非同步模式 「讀」必有資料 而同步模式則未必。 常見的非同步阻塞函式包括select
,poll
,epoll
,這些函式的用法需要花費相當大的篇幅介紹而這篇文章我想集中精力介紹「i/o模型」。以select
為例我們看一下大致原理
非同步模式下我們的api呼叫分為兩步,第一步是通過select
訂閱讀寫事件 這個函式會主動讓出cpu直到事件發生(設定為sleep狀態,等待事件發生) ;select一旦返回就證明可以開始讀了所以第二部是通過read
讀取資料( 「讀」必有資料 )。
非同步阻塞模型之訊號驅動
「完美主義者」看了上面的select
之後會有點不爽——我還要「等待」讀寫事件(即便select
會主動讓出cpu),能不能有讀寫事件的時候主動通知我啊?。借助「訊號」機制我們可以實現這個,但是這並不完美而且有點弄巧成拙的意思。 具體用法:通過fcntl
函式設定乙個f_getfl|o_async
( 曾經訊號驅動i/o也叫「非同步i/o」所以才有o_async
的說法),當有i/o時間的時候作業系統會觸發sigio
訊號。在程式裡只需要繫結sigio
訊號的處理函式就可以了。但是這裡有個問題—— 訊號處理函式由哪個程序執行呢? ,答案是:「屬主」程序。作業系統只負責引數訊號而實際的訊號處理函式必須由使用者空間的程序實現。(這就是設定f_setown
為當前程序pid的原因) 訊號驅動效能要比select
、poll
高(避免檔案描述符的複製)但是缺點是致命的——*linux中訊號佇列是有限制的如果操過這個數字問題就完全無法讀取資料。
非同步非阻塞
這種模型是最「省事」的模型,系統呼叫完成之後就只要坐等資料就可以了。是不是特別爽?其實不然,問題出在實現上。linux上的aio兩個實現版本,posix的實現最爛(藍色巨人的鍋)效能很差而且是基於「事件驅動」還會出現「訊號佇列不足」的問題(所以它就偷偷的建立執行緒,導致執行緒也不可控了);乙個是linux自己實現的(redhat貢獻)native aio。native aio主要涉及到的兩個函式io_submit
設定需要i/o動作(讀、寫,資料大小,應用緩衝區等);io_getevents
等待i/o動作完成。沒錯,即便你的整個i/o行為是非阻塞的還是需要有乙個辦法知道資料是否讀取/寫入成功。
注意圖中,核心不再為i/o分配pagecache,所有的資料必須有使用者自己讀取到應用緩衝中維護。所以aio一定是和「直接i/o」配合使用。 aio針對網絡卡裝置的意義不大,首先它的實現本質上和epoll差不多;其次它在linux中的作用更多的是用於磁碟i/o(非同步非阻塞可以不用多執行緒就造成大量的i/o請求便於i/o模組「合併」優化會提高整體i/o的吞吐率——而且對cpu開銷比較少)。 在nginx中用了乙個技巧,可以實現aio和epoll聯動,aio讀取到資料後觸發epoll傳送資料。(這個特性是非常尷尬的,如果是磁碟檔案完全可以用sendfile搞定)。
linux在進行i/o操作的時候會先把資料放到pagecache中然後通過「記憶體對映」的方式返回給應用程式,這樣做的好處是可以預讀資料也能在多個程序讀取相同資料的時候起到cache的作用。應用程式不能直接使用pagecache中的資料,通常是複製到一塊「使用者空間」的記憶體中再使用。
同步i/o只能使用buffered i/o;非同步阻塞i/o可以buffered i/o也可以使用direct i/o;非同步非阻塞i/o只能使用direct i/o
考慮從磁碟讀取檔案經過網絡卡傳送出去,會有 四次記憶體複製 :1. dma會複製磁碟資料到核心空間,2. 應用程式複製核心空間的資料到使用者空間;3. 應用程式使用者空間的資料複製到socket緩衝(核心空間);4. 協議棧把資料複製到網絡卡的中傳送。 簡單來說zero copy就是 節省這個過程中的記憶體複製次數 。有幾種做法:
除此之外還可以利用splice
、mmap
做一些優化,根據不同的裝置需要採用不同的方式此處不再展開。
五種I O 模式
1.阻塞i o 模式是最普遍使用的i o 模式。大部分程式使用的都是阻塞模式的i o 缺 省的,乙個套接字建立後所處於的模式就是阻塞i o 模式。對於乙個udp 套接字來說,資料就緒的標誌比較簡單 l 已經收到了一整個資料報 l 沒有收到。而tcp 這個概念就比較複雜,需要附加一些其他的變數。在圖6...
五種IO模型
再講io模型之前,給大家舉乙個釣魚的例子。張三去釣魚,他釣魚的時候一動不動,一直看著魚竿,看有沒有動,無論是誰叫他,他都不動,只有等魚梢動了 魚上鉤了 他才會動 李四去釣魚,他沒有像張三那樣瓷楞著,只是時不時的輪詢檢查魚竿有沒有動。一直在動。王五也來釣魚,他就比較聰明了,在魚竿上掛個鈴鐺,只要鈴鐺響...
五種IO模型
阻塞io 在核心將資料準備好之前,系統呼叫會一直等待,所有的套接字都是預設阻塞方式 非阻塞io 如果核心還沒有將資料準備好,系統呼叫會直接返回,並返回錯誤碼 非阻塞io往往需要以迴圈的方式反覆讀寫檔案描述符,這個過程稱為輪詢,對cpu的浪費較大,一般只在特定的場景下使用 訊號驅動io 核心將資料準備...