原文地址:http://www.jianshu.com/p/486b0965c296node
上一篇《聊聊同步、異步、阻塞與非阻塞》已經通俗的講解了,要理解同步、異步、阻塞與非阻塞重要的兩個概念點了,沒有看過的,建議先看這篇博文理解這兩個概念點。在認知上,創建統一的模型。這樣,你們在繼續看本篇時,纔不會理解有誤差。linux
那麼,在正式開始講Linux IO模型前,好比:同步IO和異步IO,阻塞IO和非阻塞IO分別是什麼,到底有什麼區別?不一樣的人在不一樣的上下文下給出的答案是不一樣的。因此先限定一下本文的上下文。git
在進行解釋以前,首先要說明幾個概念:github
用戶空間和內核空間web
進程切換數據庫
進程的阻塞編程
文件描述符緩存
緩存 IO安全
如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限
。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操做系統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間
。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。服務器
爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:
保存處理機上下文,包括程序計數器和其餘寄存器。
更新PCB信息。
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
選擇另外一個進程執行,並更新其PCB。
更新內存管理的數據結構。
恢復處理機上下文。
注:總而言之就是很耗資源,具體的能夠參考這篇文章:進程切換。
正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的
。
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念
。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表
。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。
緩存 IO 又被稱做標準 IO,大多數文件系統的默認 IO 操做都是緩存 IO
。在 Linux 的緩存 IO 機制中,操做系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間
。
緩存 IO 的缺點:
數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做
,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。
網絡IO的本質是socket的讀取,socket在linux系統被抽象爲流,IO能夠理解爲對流的操做
。剛纔說了,對於一次IO訪問(以read舉例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間
。因此說,當一個read操做發生時,它會經歷兩個階段:
第一階段:等待數據準備 (Waiting for the data to be ready)。
第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。
對於socket流而言,
第一步:一般涉及等待網絡上的數據分組到達,而後被複制到內核的某個緩衝區。
第二步:把數據從內核緩衝區複製到應用進程緩衝區。
網絡應用須要處理的無非就是兩大類問題,網絡IO,數據計算
。相對於後者,網絡IO的延遲,給應用帶來的性能瓶頸大於後者。網絡IO的模型大體有以下幾種:
同步模型(synchronous IO)
- 阻塞IO(bloking IO)
- 非阻塞IO(non-blocking IO)
- 多路複用IO(multiplexing IO)
- 信號驅動式IO(signal-driven IO)
異步IO(asynchronous IO)
注:因爲signal driven IO在實際中並不經常使用,因此我這隻說起剩下的四種IO Model。
在深刻介紹Linux IO各類模型以前,讓咱們先來探索一下基本 Linux IO 模型的簡單矩陣。以下圖所示:
每一個 IO 模型都有本身的使用模式,它們對於特定的應用程序都有本身的優勢。本節將簡要對其一一進行介紹。常見的IO模型有阻塞、非阻塞、IO多路複用,異步
。以一個生動形象的例子來講明這四個概念。週末我和女朋友去逛街,中午餓了,咱們準備去吃飯。週末人多,吃飯須要排隊,我和女朋友有如下幾種方案。
我和女朋友點完餐後,不知道何時能作好,只好坐在餐廳裏面等,直到作好,而後吃完才離開。女朋友本想還和我一塊兒逛街的,可是不知道飯能何時作好,只好和我一塊兒在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待作飯的時間浪費掉了。
這就是典型的阻塞
。
同步阻塞 IO 模型是最經常使用的一個模型,也是最簡單的模型
。在linux中,默認狀況下全部的socket都是blocking
。它符合人們最多見的思考邏輯。阻塞就是進程 "被" 休息, CPU處理其它進程去了
。
在這個IO模型中,用戶空間的應用程序執行一個系統調用(recvform),這會致使應用程序阻塞,什麼也不幹,直到數據準備好,而且將數據從內核複製到用戶進程,最後進程再處理數據,在等待數據處處理數據的兩個階段
,整個進程都被阻塞。不能處理別的網絡IO。調用應用程序處於一種再也不消費 CPU 而只是簡單等待響應的狀態
,所以從處理的角度來看,這是很是有效的。在調用recv()/recvfrom()函數時,發生在內核中等待數據和複製數據的過程,大體以下圖:
當用戶進程調用了recv()/recvfrom()這個系統調用,kernel就開始了IO的第一個階段:準備數據
(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。第二個階段:當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存
,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。
因此,blocking IO的特色就是在IO執行的兩個階段都被block了。
優勢:
可以及時返回數據,無延遲;
對內核開發者來講這是省事了;
缺點:
- 對用戶來講處於等待就要付出性能的代價了;
我女朋友不甘心白白在這等,又想去逛商場,又擔憂飯好了。因此咱們逛一會,回來詢問服務員飯好了沒有,來來回回好屢次,飯都還沒吃都快累死了啦。
這就是非阻塞
。須要不斷的詢問,是否準備好了。
同步非阻塞就是 「每隔一下子瞄一眼進度條」 的輪詢(polling)方式
。在這種模型中,設備是以非阻塞的形式打開的
。這意味着 IO 操做不會當即完成,read 操做可能會返回一個錯誤代碼,說明這個命令不能當即知足(EAGAIN 或 EWOULDBLOCK)。
在網絡IO時候,非阻塞IO也會進行recvform系統調用,檢查數據是否準備好,與阻塞IO不同,"非阻塞將大的整片時間的阻塞分紅N多的小的阻塞, 因此進程不斷地有機會 '被' CPU光顧"。
也就是說非阻塞的recvform系統調用調用以後,進程並無被阻塞,內核立刻返回給進程,若是數據還沒準備好,此時會返回一個error
。進程在返回以後,能夠乾點別的事情,而後再發起recvform系統調用。重複上面的過程,循環往復的進行recvform系統調用。這個過程一般被稱之爲輪詢
。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。須要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態
。
在linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時
,流程如圖所示:
當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。
因此,nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。
同步非阻塞方式相比同步阻塞方式:
優勢:可以在等待任務完成的時間裏幹其餘活了(包括提交其餘任務,也就是 「後臺」 能夠有多個任務在同時執行)。
缺點:任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次read操做,而任務可能在兩次輪詢之間的任意時間完成。這會致使總體數據吞吐量的下降。
與第二個方案差很少,餐廳安裝了電子屏幕用來顯示點餐的狀態,這樣我和女朋友逛街一會,回來就不用去詢問服務員了,直接看電子屏幕就能夠了。這樣每一個人的餐是否好了,都直接看電子屏幕就能夠了,
這就是典型的IO多路複用
。
因爲同步非阻塞方式須要不斷主動輪詢,輪詢佔據了很大一部分過程,輪詢會消耗大量的CPU時間,而 「後臺」 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。若是輪詢不是進程的用戶態,而是有人幫忙就行了。那麼這就是所謂的 「IO 多路複用」
。UNIX/Linux 下的 select、poll、epoll 就是幹這個的(epoll 比 poll、select 效率高,作的事情是同樣的)。
IO多路複用有兩個特別的系統調用select、poll、epoll函數
。select調用是內核級別的,select輪詢相對非阻塞的輪詢的區別在於---前者能夠等待多個socket,能實現同時對多個IO端口進行監聽
,當其中任何一個socket的數據準好了,就能返回進行可讀
,而後進程再進行recvform系統調用,將數據由內核拷貝到用戶進程,固然這個過程是阻塞的
。select或poll調用以後,會阻塞進程,與blocking IO阻塞不一樣在於,此時的select不是等到socket數據所有到達再處理, 而是有了一部分數據就會調用用戶進程來處理
。如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理。也能夠理解爲"非阻塞"吧
。
I/O複用模型會用到select、poll、epoll函數,這幾個函數也會使進程阻塞,可是和阻塞I/O所不一樣的的,這兩個函數能夠同時阻塞多個I/O操做
。並且能夠同時對多個讀操做,多個寫操做的I/O函數進行檢測,直到有數據可讀或可寫時(注意不是所有數據可讀或可寫),才真正調用I/O操做函數。
對於多路複用,也就是輪詢多個socket。多路複用既然能夠處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不肯定了
,固然也能夠針對不一樣的編號。具體流程,以下圖所示:
IO multiplexing就是咱們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO
。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select,那麼整個進程會被block
,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回
。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。
多路複用的特色是
經過一種機制一個進程能同時等待IO文件描述符
,內核監視這些文件描述符(套接字描述符),其中的任意一個進入讀就緒狀態,select, poll,epoll函數就能夠返回。對於監視的方式,又能夠分爲 select, poll, epoll三種方式。
上面的圖和blocking IO的圖其實並無太大的不一樣,事實上,還更差一些。由於這裏須要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)
。可是,用select的優點在於它能夠同時處理多個connection
。
因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。(select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。)
在IO multiplexing Model中,實際中,對於每個socket,通常都設置成爲non-blocking
,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block
。因此IO多路複用是阻塞在select,epoll這樣的系統調用之上,而沒有阻塞在真正的I/O系統調用如recvfrom之上。
在I/O編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者I/O多路複用技術進行處理
。I/O多路複用技術經過把多個I/O的阻塞複用到同一個select的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求
。與傳統的多線程/多進程模型比,I/O多路複用的最大優點是系統開銷小
,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,降底了系統的維護工做量,節省了系統資源,I/O多路複用的主要應用場景以下:
服務器須要同時處理多個處於監聽狀態或者多個鏈接狀態的套接字。
服務器須要同時處理多種網絡協議的套接字。
瞭解了前面三種IO模式,在用戶進程進行系統調用的時候,他們在等待數據到來的時候,處理的方式不同,直接等待,輪詢,select或poll輪詢
,兩個階段過程:
第一個階段有的阻塞,有的不阻塞,有的能夠阻塞又能夠不阻塞。
第二個階段都是阻塞的。
從整個IO過程來看,他們都是順序執行的,所以能夠歸爲同步模型(synchronous)。都是進程主動等待且向內核檢查狀態。【此句很重要!!!】
高併發的程序通常使用同步非阻塞方式而非多線程 + 同步阻塞方式
。要理解這一點,首先要扯到併發和並行的區別。好比去某部門辦事須要依次去幾個窗口,辦事大廳裏的人數就是併發數,而窗口個數就是並行度
。也就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求)
,而並行數是能夠同時工做的物理資源數量(如 CPU 核數)
。經過合理調度任務的不一樣階段,併發數能夠遠遠大於並行度,這就是區區幾個 CPU 能夠支持上萬個用戶併發請求的奧祕。在這種高併發的狀況下,爲每一個任務(用戶請求)建立一個進程或線程的開銷很是大。而同步非阻塞方式能夠把多個 IO 請求丟到後臺去,這就能夠在一個進程裏服務大量的併發 IO 請求
。
注意:IO多路複用是同步阻塞模型仍是異步阻塞模型,在此給你們分析下:
此處仍然不太清楚的,強烈建議你們在細究《聊聊同步、異步、阻塞與非阻塞》中講同步與異步的根本性區別,
同步是須要主動等待消息通知,而異步則是被動接收消息通知,經過回調、通知、狀態等方式來被動獲取消息
。IO多路複用在阻塞到select階段時,用戶進程是主動等待並調用select函數獲取數據就緒狀態消息,而且其進程狀態爲阻塞
。因此,把IO多路複用歸爲同步阻塞模式
。
信號驅動式I/O:首先咱們容許Socket進行信號驅動IO,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好時,進程會收到一個SIGIO信號,能夠在信號處理函數中調用I/O操做函數處理數據。過程以下圖所示:
女朋友不想逛街,又餐廳太吵了,回家好好休息一下。因而咱們叫外賣,打個電話點餐,而後我和女朋友能夠在家好好休息一下,飯好了送貨員送到家裏來。這就是典型的異步,只須要打個電話說一下,而後能夠作本身的事情,飯好了就送來了。
相對於同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用以後,不管內核數據是否準備好,都會直接返回給用戶進程,而後用戶態進程能夠去作別的事情
。等到socket數據準備好了,內核直接複製數據給進程,而後從內核向進程發送通知
。IO兩個階段,進程都是非阻塞的
。
Linux提供了AIO庫函數實現異步,可是用的不多。目前有不少開源的異步IO庫,例如libevent、libev、libuv。異步過程以下圖所示:
用戶進程發起aio_read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block
。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal或執行一個基於線程的回調函數來完成此次 IO 處理過程
,告訴它read操做完成了。
在 Linux 中,通知的方式是 「信號」:
若是這個進程正在用戶態忙着作別的事(例如在計算兩個矩陣的乘積),那就強行打斷之,調用事先註冊的信號處理函數
,這個函數能夠決定什麼時候以及如何處理這個異步任務。因爲信號處理函數是忽然闖進來的,所以跟中斷處理程序同樣,有不少事情是不能作的,所以保險起見,通常是把事件 「登記」 一下放進隊列,而後返回該進程原來在作的事
。
若是這個進程正在內核態忙着作別的事
,例如以同步阻塞方式讀寫磁盤,那就只好把這個通知掛起來了,等到內核態的事情忙完了,快要回到用戶態的時候,再觸發信號通知
。
若是這個進程如今被掛起了,例如無事可作 sleep 了,那就把這個進程喚醒
,下次有 CPU 空閒的時候,就會調度到這個進程,觸發信號通知。
異步 API 說來輕巧,作來難,這主要是對 API 的實現者而言的。Linux 的異步 IO(AIO)支持是 2.6.22 才引入的,還有不少系統調用不支持異步 IO。Linux 的異步 IO 最初是爲數據庫設計的,所以經過異步 IO 的讀寫操做不會被緩存或緩衝,這就沒法利用操做系統的緩存與緩衝機制
。
不少人把 Linux 的 O_NONBLOCK 認爲是異步方式,但事實上這是前面講的同步非阻塞方式。
須要指出的是,雖然 Linux 上的 IO API 略顯粗糙,但每種編程框架都有封裝好的異步 IO 實現。操做系統少作事,把更多的自由留給用戶,正是 UNIX 的設計哲學,也是 Linux 上編程框架百花齊放的一個緣由。
從前面 IO 模型的分類中,咱們能夠看出 AIO 的動機:
同步阻塞模型須要在 IO 操做開始時阻塞應用程序。這意味着不可能同時重疊進行處理和 IO 操做。
同步非阻塞模型容許處理和 IO 操做重疊進行,可是這須要應用程序根據重現的規則來檢查 IO 操做的狀態。
這樣就剩下異步非阻塞 IO 了,它容許處理和 IO 操做重疊進行,包括 IO 操做完成的通知。
IO多路複用除了須要阻塞以外,select 函數所提供的功能(異步阻塞 IO)與 AIO 相似
。不過,它是對通知事件進行阻塞,而不是對 IO 調用進行阻塞
。
有時咱們的 API 只提供異步通知方式,例如在 node.js 裏,但業務邏輯須要的是作完一件過後作另外一件事
,例如數據庫鏈接初始化後才能開始接受用戶的 HTTP 請求。這樣的業務邏輯就須要調用者是以阻塞方式來工做
。
爲了在異步環境裏模擬 「順序執行」 的效果,就須要把同步代碼轉換成異步形式,這稱爲 CPS(Continuation Passing Style)變換
。BYVoid 大神的 continuation.js 庫就是一個 CPS 變換的工具。用戶只需用比較符合人類常理的同步方式書寫代碼,CPS 變換器會把它轉換成層層嵌套的異步回調形式
。
另一種使用阻塞方式的理由是下降響應延遲
。若是採用非阻塞方式,一個任務 A 被提交到後臺,就開始作另外一件事 B,但 B 還沒作完,A 就完成了,這時要想讓 A 的完成事件被儘快處理(好比 A 是個緊急事務),要麼丟棄作到一半的 B,要麼保存 B 的中間狀態並切換回 A,任務的切換是須要時間的(無論是從磁盤載入到內存,仍是從內存載入到高速緩存),這勢必下降 A 的響應速度。所以,對實時系統或者延遲敏感的事務,有時採用阻塞方式比非阻塞方式更好
。
調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。
在說明synchronous IO和asynchronous IO的區別以前,須要先給出二者的定義。POSIX的定義是這樣子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
二者的區別就在於synchronous IO作」IO operation」的時候會將process阻塞
。按照這個定義,以前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。
有人會說,non-blocking IO並無被block啊。這裏有個很是「狡猾」的地方,定義中所指的」IO operation」是指真實的IO操做
,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了
,在這段時間內,進程是被block的。
而asynchronous IO則不同,當進程發起IO 操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成
。在這整個過程當中,進程徹底沒有被block。
各個IO Model的比較如圖所示:
經過上面的圖片,能夠發現non-blocking IO和asynchronous IO的區別仍是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,可是它仍然要求進程去主動的check
,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則徹底不一樣。它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知
。在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據