說到IO模型,都會牽扯到同步、異步、阻塞、非阻塞這幾個詞。從詞的表面上看,不少人都以爲很容易理解。可是細細一想,卻總會發現有點摸不着頭腦。本身也曾被這幾個詞弄的迷迷糊糊的,每次看相關資料弄明白了,而後很快又給搞混了。經歷過這麼幾回以後,發現這東西必須得有所總結提煉纔不至於再次混爲一談。尤爲是最近看到好幾篇講這個的文章,不少都有謬誤,很容易把原本就搞不清楚的人弄的更加迷糊。java
最適合IO模型的例子應該是我們日常生活中的去餐館吃飯這個場景,下文就結合這個來說解一下經典的幾個IO模型。在此以前,先須要說明如下幾點:node
IO有內存IO、網絡IO和磁盤IO三種,一般咱們說的IO指的是後二者。nginx
阻塞和非阻塞,是函數/方法的實現方式,即在數據就緒以前是馬上返回仍是等待,即發起IO請求是否會被阻塞。編程
以文件IO爲例,一個IO讀過程是文件數據從磁盤→內核緩衝區→用戶內存的過程。同步與異步的區別主要在於數據從內核緩衝區→用戶內存這個過程需不須要用戶進程等待,即實際的IO讀寫是否阻塞請求進程。(網絡IO把磁盤換作網卡便可)windows
去餐館吃飯,點一個本身最愛吃的蓋澆飯,而後在原地等着一直到蓋澆飯作好,本身端到餐桌就餐。這就是典型的同步阻塞。當廚師給你作飯的時候,你須要一直在那裏等着。數組
網絡編程中,讀取客戶端的數據須要調用recvfrom。在默認狀況下,這個調用會一直阻塞直到數據接收完畢,就是一個同步阻塞的IO方式。這也是最簡單的IO模型,在一般fd較少、就緒很快的狀況下使用是沒有問題的。tomcat
接着上面的例子,你每次點完飯就在那裏等着,忽然有一天你發現本身真傻。因而,你點完以後,就回桌子那裏坐着,而後估計差很少了,就問老闆飯好了沒,若是好了就去端,沒好的話就等一會再去問,依次循環直到飯作好。這就是同步非阻塞。網絡
這種方式在編程中對socket設置O_NONBLOCK便可。但此方式僅僅針對網絡IO有效,對磁盤IO並無做用。由於本地文件IO就沒有被認爲是阻塞,咱們所說的網絡IO的阻塞是由於網路IO有無限阻塞的可能,而本地文件除非是被鎖住,不然是不可能無限阻塞的,所以只有鎖這種狀況下,O_NONBLOCK纔會有做用。並且,磁盤IO時要麼數據在內核緩衝區中直接能夠返回,要麼須要調用物理設備去讀取,這時候進程的其餘工做都須要等待。所以,後續的IO複用和信號驅動IO對文件IO也是沒有意義的。多線程
此外,須要說明的一點是nginx和node中對於本地文件的IO是用線程的方式模擬非阻塞的效果的,而對於靜態文件的io,使用zero copy(例如sendfile)的效率是很是高的。異步
接着上面的列子,你點一份飯而後循環的去問好沒好顯然有點得不償失,還不如就等在那裏直到準備好,可是當你點了好幾樣飯菜的時候,你每次都去問一下全部飯菜的狀態(未作好/已作好)確定比你每次阻塞在那裏等着好多了。固然,你問的時候是須要阻塞的,一直到有準備好的飯菜或者你等的不耐煩(超時)。這就引出了IO複用,也叫多路IO就緒通知。這是一種進程預先告知內核的能力,讓內核發現進程指定的一個或多個IO條件就緒了,就通知進程。使得一個進程能在一連串的事件上等待。
IO複用的實現方式目前主要有select、poll和epoll。
select和poll的原理基本相同:
註冊待偵聽的fd(這裏的fd建立時最好使用非阻塞)
每次調用都去檢查這些fd的狀態,當有一個或者多個fd就緒的時候返回
返回結果中包括已就緒和未就緒的fd
相比select,poll解決了單個進程可以打開的文件描述符數量有限制這個問題:select受限於FD_SIZE的限制,若是修改則須要修改這個宏從新編譯內核;而poll經過一個pollfd數組向內核傳遞須要關注的事件,避開了文件描述符數量限制。
此外,select和poll共同具備的一個很大的缺點就是包含大量fd的數組被總體複製於用戶態和內核態地址空間之間,開銷會隨着fd數量增多而線性增大。
select和poll就相似於上面說的就餐方式。但當你每次都去詢問時,老闆會把全部你點的飯菜都輪詢一遍再告訴你狀況,當大量飯菜很長時間都不能準備好的狀況下是很低效的。因而,老闆有些不耐煩了,就讓廚師每作好一個菜就通知他。這樣每次你再去問的時候,他會直接把已經準備好的菜告訴你,你再去端。這就是事件驅動IO就緒通知的方式-epoll。
epoll的出現,解決了select、poll的缺點:
基於事件驅動的方式,避免了每次都要把全部fd都掃描一遍。
epoll_wait只返回就緒的fd。
epoll使用nmap內存映射技術避免了內存複製的開銷。
epoll的fd數量上限是操做系統的最大文件句柄數目,這個數目通常和內存有關,一般遠大於1024。
目前,epoll是Linux2.6下最高效的IO複用方式,也是Nginx、Node的IO實現方式。而在freeBSD下,kqueue是另外一種相似於epoll的IO複用方式。
此外,對於IO複用還有一個水平觸發和邊緣觸發的概念:
水平觸發:當就緒的fd未被用戶進程處理後,下一次查詢依舊會返回,這是select和poll的觸發方式。
邊緣觸發:不管就緒的fd是否被處理,下一次再也不返回。理論上性能更高,可是實現至關複雜,而且任何意外的丟失事件都會形成請求處理錯誤。epoll默認使用水平觸發,經過相應選項可使用邊緣觸發。
上文的就餐方式仍是須要你每次都去問一下飯菜情況。因而,你再次不耐煩了,就跟老闆說,哪一個飯菜好了就通知我一聲吧。而後就本身坐在桌子那裏幹本身的事情。更甚者,你能夠把手機號留給老闆,本身出門,等飯菜好了直接發條短信給你。這就相似信號驅動的IO模型。
流程以下:
開啓套接字信號驅動IO功能
系統調用sigaction執行信號處理函數(非阻塞,馬上返回)
數據就緒,生成sigio信號,經過信號回調通知應用來讀取數據。
此種io方式存在的一個很大的問題:Linux中信號隊列是有限制的,若是超過這個數字問題就沒法讀取數據。
以前的就餐方式,到最後老是須要你本身去把飯菜端到餐桌。這下你也不耐煩了,因而就告訴老闆,能不能飯好了直接端到你的面前或者送到你的家裏(外賣)。這就是異步非阻塞IO了。
對比信號驅動IO,異步IO的主要區別在於:信號驅動由內核告訴咱們什麼時候能夠開始一個IO操做(數據在內核緩衝區中),而異步IO則由內核通知IO操做什麼時候已經完成(數據已經在用戶空間中)。
異步IO又叫作事件驅動IO,在Unix中,POSIX1003.1標準爲異步方式訪問文件定義了一套庫函數,定義了AIO的一系列接口。使用aio_read或者aio_write發起異步IO操做,使用aio_error檢查正在運行的IO操做的狀態。可是其實現沒有經過內核而是使用了多線程阻塞。此外,還有Linux本身實現的Native AIO,依賴兩個函數:io_submit和io_getevents,雖然io是非阻塞的,但仍須要主動去獲取讀寫的狀態。
須要特別注意的是:AIO是I/O處理模式,是一種接口標準,各家操做系統能夠實現也能夠不實現。目前Linux中AIO的內核實現只對文件IO有效,若是要實現真正的AIO,須要用戶本身來實現。
上文講述了UNIX環境的五種IO模型。基於這五種模型,在Java中,隨着NIO和NIO2.0(AIO)的引入,通常具備如下幾種網絡編程模型:
BIO
NIO
AIO
BIO是一個典型的網絡編程模型,是一般咱們實現一個服務端程序的過程,步驟以下:
主線程accept請求阻塞
請求到達,建立新的線程來處理這個套接字,完成對客戶端的響應。
主線程繼續accept下一個請求
這種模型有一個很大的問題是:當客戶端鏈接增多時,服務端建立的線程也會暴漲,系統性能會急劇降低。所以,在此模型的基礎上,相似於 tomcat的bio connector,採用的是線程池來避免對於每個客戶端都建立一個線程。有些地方把這種方式叫作僞異步IO(把請求拋到線程池中異步等待處理)。
JDK1.4開始引入了NIO類庫,這裏的NIO指的是Non-blcok IO,主要是使用Selector多路複用器來實現。Selector在Linux等主流操做系統上是經過epoll實現的。
NIO的實現流程,相似於select:
建立ServerSocketChannel監聽客戶端鏈接並綁定監聽端口,設置爲非阻塞模式。
建立Reactor線程,建立多路複用器(Selector)並啓動線程。
將ServerSocketChannel註冊到Reactor線程的Selector上。監聽accept事件。
Selector在線程run方法中無線循環輪詢準備就緒的Key。
Selector監聽到新的客戶端接入,處理新的請求,完成tcp三次握手,創建物理鏈接。
將新的客戶端鏈接註冊到Selector上,監聽讀操做。讀取客戶端發送的網絡消息。
客戶端發送的數據就緒則讀取客戶端請求,進行處理。
相比BIO,NIO的編程很是複雜。
JDK1.7引入NIO2.0,提供了異步文件通道和異步套接字通道的實現。其底層在windows上是經過IOCP,在Linux上是經過epoll來實現的(LinuxAsynchronousChannelProvider.java,UnixAsynchronousServerSocketChannelImpl.java)。
建立AsynchronousServerSocketChannel,綁定監聽端口
調用AsynchronousServerSocketChannel的accpet方法,傳入本身實現的CompletionHandler。包括上一步,都是非阻塞的
鏈接傳入,回調CompletionHandler的completed方法,在裏面,調用AsynchronousSocketChannel的read方法,傳入負責處理數據的CompletionHandler。
數據就緒,觸發負責處理數據的CompletionHandler的completed方法。繼續作下一步處理便可。
寫入操做相似,也須要傳入CompletionHandler。
其編程模型相比NIO有了很多的簡化。
. | 同步阻塞IO | 僞異步IO | NIO | AIO |
---|---|---|---|---|
客戶端數目 :IO線程 | 1 : 1 | m : n | m : 1 | m : 0 |
IO模型 | 同步阻塞IO | 同步阻塞IO | 同步非阻塞IO | 異步非阻塞IO |
吞吐量 | 低 | 中 | 高 | 高 |
編程複雜度 | 簡單 | 簡單 | 很是複雜 | 複雜 |