前面總結了不少IO、NIO相關的基礎知識點,還總結了IO和NIO之間的區別及各自適用場景,本文會從另外一個視角來學習一下IO,即IO模型。什麼是IO模型?對於不一樣人、在不一樣場景下給出的答案是不一樣的,因此先限定一下本文的上下文:Linux環境下的network IO。linux
本文會從以下幾個方面展開:segmentfault
一些基礎概念緩存
I/O模型安全
總結網絡
IO模型這個概念屬於比較基礎的底層概念,在此以前容我再先簡單介紹一些涉及到的更底層的概念,幫助對I/O模型的理解:異步
如今操做系統都是採用虛擬存儲器,對於32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。socket
對於內核而言,全部打開文件都由文件描述符引用。文件描述符是一個非負整數。當打開一個現存文件或建立一個新文件時,內核向進程返回一個文件描述符。當讀、寫一個文件時,用open或create返回的文件描述符標識該文件,將其做爲參數傳送給read或write。文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。async
緩存I/O又被稱做標準I/O,大多數文件系統的默認I/O操做都屬於緩存I/O。在Linux的緩存I/O機制中,操做系統會將I/O的數據緩存在文件系統的頁緩存(page cache)中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。函數
由於數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,因此這些數據拷貝操做所帶來的CPU以及內存開銷是很是大的,這也是緩存I/O所帶來的缺點。學習
上面有提到,對於一次IO訪問(好比read),數據會先被複制到操做系統內核緩衝區中,而後再從操做系統內核的緩衝區複製到應用程序的地址空間。也就是說,當一個IO操做發生時,會經歷兩個階段:
這是由於存在上面兩個階段,linux系統產生了下面5種I/O模型:
下面咱們來一一介紹(本文暫介紹除信號驅動I/O外其他4種IO模型)。
在linux中,默認狀況下socket都是阻塞式的,一個典型的讀操做流程大概是這樣的:
當用戶進程調用recvfrom這個系統調用時,kernel就開始了IO的第一個階段:數準備階段(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的TCP包。這個時候kernel就要等待直到全部數據到達),這個過程至關於將數據被複制到操做系統內核的緩衝區,是須要一個過程,須要等待的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中複製到用戶內存,而後kernel返回結果,用戶進程纔會解除block的狀態,從新運行起來。
Blocking IO最大的特色就是在IO執行的兩個階段都被會阻塞。
能夠將設置socket設置爲non-blocking模式,對於此時的讀操做,流程大概是這個樣子的:
當用戶進程發起recvfrom這個系統調用時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個recvfrom調用,立刻就獲得了一個結果,無論有沒有數據。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發起recvfrom操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它接下來就將數據複製到用戶內存,而後返回。
Nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有,這個過程不阻塞,可是在IO的第二個階段仍是須要等待內核將數據複製到用戶進程的,也就是這部分仍是會阻塞的。
IO multiplexing就是咱們說的IO多路複用,有些地方也稱這種IO方式爲event driven IO。主要是經過select/epoll來實現單個process同時處理多個網絡鏈接的IO,它的基本原理是依賴select,poll,epoll這些function來不斷的輪詢負責的全部socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select時整個進程會被block,同時kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel複製到用戶進程。
因此,I/O多路複用的特色是經過一種機制使得一個進程能同時等待多個文件描述符(至於爲何是文件描述符,由於對於kernel而言,全部打開文件都由文件描述符引用,而一次IO鏈接也至關於打開文件),而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就能夠返回。
IO多路複用是創建在Nonblocking IO之上的,即經過單個線程來「監視」多個IO鏈接,誰準備就緒了就處理誰,處理的過程和Nonblocking的過程是同樣的。
Asynchronous IO不一樣於上面三種模型,先看一下它的流程:
用戶進程發起read操做以後,馬上就能夠開始去作其它的事情了。另外一方面,從kernel的角度,當它收到一個asynchronous read調用以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而且數據準備好以後再將數據複製到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。
其實,從這裏能夠看出異步I/O最大的特色就是,將前面提到的兩步IO操做所有異步化了,整個過程徹底不阻塞,而Nonblocking IO在複製數據到用戶進程這一步其實仍是阻塞的,只是數據準備階段不阻塞而已。
本文總結了linux網絡IO中常見的五種IO模型,雖然說不是Java IO,可是Java IO、NIO中也是遵循一樣的IO模型,關於這一點,後面會專門寫一篇文章來闡述。
五種IO模型分別爲:阻塞I/O(blocking IO)、非阻塞I/O(nonblocking IO)、I/O多路複用(IO multiplexing)、信號驅動I/O(signal driven IO)、異步I/O(asynchronous IO)。在這五種模型的基礎上,有兩個概念不得不提,阻塞和非阻塞、同步和異步,這兩個概念容易搞混,:
阻塞很好理解,就是進程或者線程停在那裏等待某個狀態,什麼都不幹。可是什麼狀況稱爲阻塞,什麼狀況稱爲非阻塞呢?在IO模型中的定義是取決於前面提到的第一階段:數據準備階段,也就是說,在這個階段會致使進程或線程阻塞的IO就成爲阻塞式IO,反之就是非阻塞式IO。因此Nonblocking IO在第一階段是能夠作別的事情的,可是在第二階段任然是阻塞的,這點須要注意。
在說明同步IO模型和異步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;
二者的區別就在於執行"IO Operation"的時候是否會致使進程或線程阻塞,這個"IO Operation"是指前面提到的第二階段:將數據從kernel複製到用戶進程中。按照這個定義,前面說的Blocking IO、Non-blocking IO、IO multiplexing就都屬於synchronous IO,只有異步IO才屬於Asynchronous IO,由於只有它在整個IO過程當中都不會致使阻塞。
最後再附上一張五種IO模型比較圖,以幫助理解: