相關概念
同步和異步
描述的是用戶線程與內核的交互方式:
- 同步是指用戶線程發(fā)起 I/O 請求后需要等待(阻塞)或者輪詢內核 I/O 操作(非阻塞)完成后才能繼續(xù)執(zhí)行;
- 異步是指用戶線程發(fā)起 I/O 請求后仍繼續(xù)執(zhí)行,當內核 I/O 操作完成后會通知用戶線程,或者調用用戶線程注冊的回調函數(shù)
同步與異步一般是面向操作系統(tǒng)和應用程序對 IO 操作的層面上來區(qū)別的
同步時:應用程序會直接參與 IO 讀寫操作,并且應用程序會直接阻塞到某一個方法上,直到數(shù)據(jù)準備就緒;或者采用輪詢的策略實時檢查數(shù)據(jù)的就緒狀態(tài),如果就緒則獲取數(shù)據(jù)
異步時:所有的 IO 讀寫操作交給操作系統(tǒng)處理,與應用程序沒有直接關系,程序不需要關心 IO 讀寫,當操作系統(tǒng)完成 IO 讀寫操作時,會給應用程序發(fā)送通知,應用程序直接拿走數(shù)據(jù)即可
同步 / 異步描述的是執(zhí)行 IO 操作的主體是誰,同步是由用戶進程自己去執(zhí)行最終的 IO 操作。異步是用戶進程自己不關系實際 IO 操作的過程,只需要由內核在 IO 完成后通知它既可,由內核進程來執(zhí)行最終的 IO 操作
阻塞和非阻塞
描述的是用戶線程調用內核 I/O 操作的方式:
- 阻塞是指 I/O 操作需要徹底完成后才返回到用戶空間;
- 非阻塞是指 I/O 操作被調用后立即返回給用戶一個狀態(tài)值,無需等到 I/O 操作徹底完成
阻塞 / 非阻塞描述的是函數(shù),指訪問某個函數(shù)時是否會阻塞線程
I/O 的兩個階段
階段 1:等待數(shù)據(jù)就緒(發(fā)起 IO 請求)。網(wǎng)絡 I/O 的情況就是等待遠端數(shù)據(jù)陸續(xù)抵達;磁盤 I/O 的情況就是等待磁盤數(shù)據(jù)從磁盤上讀取到內核態(tài)內存中
階段 2:數(shù)據(jù)拷貝(實際 IO 操作)。出于系統(tǒng)安全,用戶態(tài)的程序沒有權限直接讀取內核態(tài)內存,因此內核負責把內核態(tài)內存中的數(shù)據(jù)拷貝一份到用戶態(tài)內存中
同步 IO 和異步 IO 的區(qū)別就在于第二個步驟是否阻塞,如果實際的 IO 讀寫阻塞請求進程,那么就是同步 IO,因此阻塞 IO、非阻塞 IO、IO 復用、信號驅動 IO 都是同步 IO,如果不阻塞,而是操作系統(tǒng)幫你做完 IO 操作再將結果返回給你,那么就是異步 IO
阻塞 IO 和非阻塞 IO 的區(qū)別在于第一步,發(fā)起 IO 請求后是否會被阻塞,如果阻塞直到完成那么就是傳統(tǒng)的阻塞 IO,如果不阻塞,那么就是非阻塞 IO
同步與阻塞區(qū)別:
同步和異步關注的是消息通信機制。同步在發(fā)出調用后,沒得到結果之前不返回,但是一旦返回,就是調用結果;異步是調用發(fā)出后立即返回,但是沒有返回結果,直到被調用者通過狀態(tài)、通知、回調函數(shù)來通知調用者
阻塞和非阻塞關注的是程序在等待調用結果時的狀態(tài)。阻塞是指結果返回之前,當前線程會被掛起;非阻塞是指不能立即得到結果時,線程不會被阻塞
IO 就緒和完成的區(qū)別:就緒指的是還需要用戶自己去處理,完成指的是內核幫助完成了,用戶不用關心 IO 過程,只需要提供回調函數(shù)
select / poll / epoll 從本質上說都是同步非阻塞 IO,select 會收到 IO 就緒的狀態(tài),然后通知用戶去處理 IO,實際的 IO 操作還需要用戶等待內核復制操作
同步非阻塞 IO 指的是用戶調用讀寫方法是不阻塞的,立刻返回的,而且需要用戶線程來檢查 IO 狀態(tài)。需要注意的是,如果發(fā)現(xiàn)有可以操作的 IO,那么實際用戶進程還是會阻塞等待內核復制數(shù)據(jù)到用戶進程,它與同步阻塞 IO 的區(qū)別是后者全程等待
異步非阻塞 IO 指的是用戶調用讀寫方法是不阻塞的,立刻返回,而且用戶不需要關注讀寫,只需要提供回調操作,內核線程在完成讀寫后回調用戶提供的 callback
五種 I/O 模型簡述
- 阻塞 I/O 模型(同步阻塞)
老李去火車站買票,排隊三天買到一張退票
耗費:在車站吃喝拉撒睡 3 天,其他事一件沒干

- 非阻塞 I/O 模型(同步非阻塞)
老李去火車站買票,隔 12 小時去火車站問有沒有退票,三天后買到一張票
耗費:往返車站 6 次,路上 6 小時,其他時間做了好多事

socket 設置為 NONBLOCK(非阻塞)就是告訴內核,當所請求的 I/O 操作無法完成時,不要將進程睡眠,而是返回一個錯誤碼(EWOULDBLOCK),這樣請求就不會阻塞
I/O 操作函數(shù)將不斷的測試數(shù)據(jù)是否已經(jīng)準備好,如果沒有準備好,繼續(xù)測試,直到數(shù)據(jù)準備好為止。整個 I/O 請求的過程中,雖然用戶線程每次發(fā)起 I/O 請求后可以立即返回,但是為了等到數(shù)據(jù),仍需要不斷地輪詢、重復請求,消耗了大量的 CPU 的資源
數(shù)據(jù)準備好了,從內核拷貝到用戶空間
一般很少直接使用這種模型,而是在其他 I/O 模型中使用非阻塞 I/O 這一特性。這種方式對單個 I/O 請求意義不大,但給 I/O 多路復用鋪平了道路
- I/O 復用模型(同步阻塞)
老李去火車站買票,委托黃牛,等著黃牛拿票。黃牛買到后即通知老李去領,然后老李去火車站交錢領票
select
黃牛不斷去輪詢各個渠道問有沒有票,輪詢渠道數(shù)有限制,1024 個
poll
同樣去輪詢各個渠道,但是渠道數(shù)沒限制
epoll
黃牛不輪詢了,而是渠道有票數(shù)變化時才動身
耗費:往返車站 1 次,路上 1 小時,黃牛手續(xù)費 100 元,看起來比阻塞 I/O 更貴,但是 1 個黃牛可以同時處理好幾個客戶的票啊

I/O 多路復用會用到 select、poll 或者 epoll 函數(shù),這兩個函數(shù)也會使進程阻塞,但是和阻塞 I/O 所不同的是,這三個函數(shù)可以同時阻塞多個 I/O 操作。而且可以同時對多個讀操作,多個寫操作的 I/O 函數(shù)進行檢測,直到有數(shù)據(jù)可讀或可寫時,才真正調用 I/O 操作函數(shù)
I/O 多路復用模型使用了 Reactor 設計模式實現(xiàn)了這一機制
select/poll 方法由一個用戶態(tài)線程負責輪詢多個 socket,直到某個階段 1 的數(shù)據(jù)就緒,再通知實際的用戶線程執(zhí)行階段 2 的拷貝。通過一個專職的用戶態(tài)線程執(zhí)行非阻塞 I/O 輪詢,模擬實現(xiàn)了階段一的異步化。而 epoll 函數(shù)不輪詢,而是采用回調函數(shù)機制
從流程上來看,使用 select 函數(shù)進行 IO 請求和同步阻塞模型沒有太大的區(qū)別(只不過是被 select 阻塞,而不是被 recvfrom 阻塞),甚至還多了添加監(jiān)視 socket,以及調用 select 函數(shù)的額外操作,效率更差。但是,使用 select 以后最大的優(yōu)勢是用戶可以在一個線程內同時處理多個 socket 的 IO 請求。用戶可以注冊多個 socket,然后不斷地調用 select 讀取被激活的 socket,即可達到在同一個線程內同時處理多個 IO 請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的
如果處理的連接數(shù)不是很高的話,使用 select/epoll 不一定比使用 multi-threading + blocking IO 的性能更好,可能延遲還更大。select/epoll 的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接
- 信號驅動 I/O 模型(同步非阻塞)
老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李,然后老李去火車站交錢領票
耗費:往返車站 2 次,路上 2 小時,免黃牛費 100 元

首先允許 socket 進行信號驅動 I/O,并安裝一個信號處理函數(shù),進程繼續(xù)運行并不阻塞。當數(shù)據(jù)準備好時,進程會收到一個 SIGIO 信號,可以在信號處理函數(shù)中調用 I/O 操作函數(shù)處理數(shù)據(jù)
- 異步 I/O 模型(異步非阻塞)
老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李并快遞送票上門
耗費:往返車站 1 次,路上 1 小時,免黃牛費 100 元

調用 aio_read 函數(shù),告訴內核描述字,緩沖區(qū)指針,緩沖區(qū)大小,文件偏移以及通知的方式,然后立即返回。當內核將數(shù)據(jù)拷貝到緩沖區(qū)后,再通知應用程序
異步 I/O 模型使用了 Proactor 設計模式實現(xiàn)了這一機制
五種 I/O 模型比較
