目錄html
Linux NIO 系列(01) 五種網絡 IO 模型node
Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)linux
在正式開始講 Linux IO 模型前,先介紹 5 個基本概念。web
如今操做系統都是採用虛擬存儲器,那麼對 32 位操做系統而言,它的尋址空間 (虛擬存儲空間)爲 4G (2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核 (kernel),保證內核的安全,操做系統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對 Linux 操做系統而言,將最高的 1G 字節 (從虛擬地址 0xC0000000 到 0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節 (從虛擬地址 0x00000000 到 0xBFFFFFFF),供各個進程使用,稱爲用戶空間。數據庫
爲了控制進程的執行,內核必須有能力掛起正在 CPU 上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。總之,進程切換很耗資源。編程
正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程 (得到 CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用 CPU 資源的。緩存
文件描述符 (File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。安全
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於 UNIX、Linux 這樣的操做系統。網絡
Linux 內核將全部外部設備都看作一個文件來操做,對一個文件的讀寫操做會調用內核提供的系統命令,返回一個 file descriptor(fd,文件描述符)。而對一個 socket 的讀寫也會有相應的描述符,稱爲 socketfd(socket 描述符),描述符就是一個數字,它指向內核中的一個結構體(文件路徑,數據區等一些屬性)。多線程
緩存 IO 又被稱做標準 IO,大多數文件系統的默認 IO 操做都是緩存 IO。在 Linux 的緩存 IO 機制中,操做系統會將 IO 的數據緩存在文件系統的頁緩存 (page cache) 中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。
緩存 IO 的缺點:
網絡 IO 的本質是 socket 的讀取,socket 在 linux 系統被抽象爲流,IO 能夠理解爲對流的操做。剛纔說了,對於一次 IO 訪問 (以 read 舉例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。因此說,當一個 read 操做發生時,它會經歷兩個階段:
對於 socket 流而言,
網絡應用須要處理的無非就是兩大類問題,網絡 IO,數據計算。相對於後者,網絡 IO 的延遲,給應用帶來的性能瓶頸大於後者。網絡 IO 的模型大體有以下幾種:
注:因爲 signal driven IO 在實際中並不經常使用,因此這隻說起剩下的四種 IO Model。
我和女朋友點完餐後,不知道何時能作好,只好坐在餐廳裏面等,直到作好,而後吃完才離開。女朋友本想還和我一塊兒逛街的,可是不知道飯能何時作好,只好和我一塊兒在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待作飯的時間浪費掉了。這就是典型的阻塞。
同步阻塞 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 效率高,作的事情是同樣的)。
I/O 複用模型會用到 select、poll、epoll 函數,這幾個函數也會使進程阻塞。select 調用是內核級別的。
select 輪詢相對非阻塞的輪詢的區別在於:select 能夠對多個 socket 端口進行監聽,當其中任何一個 socket 的數據準好了,就能返回進行可讀,而後進程再進行 recvform 系統調用,將數據由內核拷貝到用戶進程,固然這個過程是阻塞的
select 相對 blocking IO 阻塞不一樣在於:此時的 select 不是等到 socket 數據所有到達再處理,而是有了一部分數據就會調用用戶進程來處理。如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理。也能夠理解爲"非阻塞"吧。
對於多路複用,也就是輪詢多個 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 之上。
瞭解了前面三種 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 變換器會把它轉換成層層嵌套的異步回調形式。
天天用心記錄一點點。內容也許不重要,但習慣很重要!
轉載:https://blog.csdn.net/bpingchang/article/details/51419890