注:
1)本人非科班出身,文章的來源主要是基于一些能找到的資料,在理解的基礎上做一些總結歸納,以期對IO相關的知識體系化。水平有限,還請指教!
2)本文后半部分對IO所涉及的一些概念做了簡要概述,如果讀者從頭開始閱讀的情況下感覺一頭霧水,可以先去閱讀并熟悉一些基本的概念,有了基本的上下文概念后,再從頭看起。
Linux的網絡IO模型
網絡IO的本質是socket的讀寫,socket在Linux中被抽象為流,IO可以理解為對流的操作。
注意緩存IO這一概念
事先說明:
IO本身可以分為內存IO、網絡IO和磁盤IO三種,一般討論IO時更多是指后兩者,尤其是網絡IO。
-
阻塞/非阻塞:針對函數/方法的實現方式而言
即數據就緒之前是立刻返回還是等待,即發起IO請求后是否會阻塞。
-
同步/異步
IO讀操作指數據流經:網絡 -> 內核緩沖區 -> 用戶內存
而同步和異步的主要區別在于數據從 內核緩沖區 -> 用戶內存 這個過程需不需要用戶進程等待。
?
對于一個網絡IO,會涉及到兩個系統對象,一個是調用這個IO的process(or thread),另一個就是系統內核(kernel)
當一個read操作發生時,它會經歷兩個階段:
- 第一階段:等待數據準備 (Waiting for the data to be ready)。
- 第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。
對于socket流而言,
- 第一步:通常涉及等待網絡上的數據分組到達,然后被復制到內核的某個緩沖區。
- 第二步:把數據從內核緩沖區復制到應用進程緩沖區。
網絡應用處理的是兩大類問題:網絡IO、數據計算。前者給應用帶來的性能瓶頸更大。
網絡IO的模型大致有如下幾種:
- 同步模型(synchronous IO)
- 阻塞IO模型(blocking IO)
- 非阻塞IO模型(non-blocking IO)
- 多路復用IO模型(multiplexing IO)
- 信號驅動IO模型(signal-driven IO)
- 異步IO(asynchronous IO)
Blocking IO
在Linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程如下:
[圖片上傳失敗...(image-9e85ea-1527599992824)]
當用戶進程調用了recvfrom這個系統調用,如上所述,會有兩個階段
- 準備數據。很多時候數據在一開始還沒有到達,這個時候kernel就要等待足夠的數據到來。而用戶進程會一直阻塞。
- 當kernel等到數據準備好了,它會將數據從kernel中拷貝到用戶內存,然后kernel返回,用戶進程結束block狀態,重新運行。
Blocking IO的特點就是IO執行的兩個階段都是block了的。
Non-Blocking IO
在Linux中,可以通過設置socket使其變為non-blocking,其流程如下:
[圖片上傳失敗...(image-b07607-1527599992824)]
當用戶進程調用了recvfrom這個系統調用,如果kernel中的數據還沒有準備好,那么用戶進程不會block而是立刻返回一個error,即從用戶的角度而言,不需要等待,馬上得到一個結果。從圖中可以看出,用戶進程在判斷結果是一個error后,了解到數據還沒有準備好,于是就不斷重復上述操作直至kernel中的數據準備好,然后它馬上將數據拷貝到了用戶內存,然后返回。
Multiplexing IO
Select/Epoll,也被稱作是Event-Driven IO。好處是單個process可以同時處理多個網絡連接的IO。
基本原理可見下面的“IO復用技術”。也叫多路IO就緒通知。這是一種進程預先告知內核的能力,讓內核發現進程指定的一個或多個IO條件就緒了,就通知進程。使得一個進程能在一連串的事件上等待。
[圖片上傳失敗...(image-f4d1c4-1527599992824)]
這個流程和Blocking IO的流程其實并沒有太多不同,事實上僅從圖中看起來,由于需要進行兩次系統調用,可能更差一些。但是,Select的優勢在于它可以同時處理多個連接。
如果處理的連接數不是很高的話,使用“Select/Epoll 的 Web Server”不一定比使用“多線程 + BIO的Web Server”性能更好,反而延遲會更大。
Select/Epoll的優勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。
Asynchronous IO
[圖片上傳失敗...(image-2e105e-1527599992824)]
用戶進程發起read操作之后,立刻就可以開始去做其它的事。
而從kernel的角度,當它受到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產生任何block。然后,kernel會等待數據準備完成,然后將數據拷貝到用戶內存,當這一切都完成之后,kernel會給用戶進程發送一個signal,告訴它read操作完成了。
比較
[圖片上傳失敗...(image-89e729-1527599992824)]
IO復用技術
在IO編程過程中,當需要處理多個請求時,可以使用 多線程 和 IO復用 的方式進行處理。
IO復用是什么?
把多個IO的阻塞復用到一個select之類的阻塞上,從而使得系統在單線程的情況下同時支持處理多個請求。
IO復用常見的應用場景:
- 服務器需要同時處理多個處于監聽狀態和多個連接狀態的套接字;
- 服務器需要處理多種網絡協議的套接字
IO復用的實現方式目前主要有select、poll和epoll。
select和poll的原理基本相同:
- 注冊待偵聽的fd(這里的fd創建時最好使用非阻塞)
- 每次調用都去檢查這些fd的狀態,當有一個或者多個fd就緒的時候返回
- 返回結果中包括已就緒和未就緒的fd
Linux網絡編程過程中,相比于select/poll,epoll是有著更明顯優勢的一種選擇。
-
支持一個進程打開的socket描述符不受限制(僅受限于操作系統的最大文件句柄數)。
Select的缺陷:一個進程所打開的FD受限,默認是2048;盡管數值可以更改,但同樣可能導致網絡效率下降;可以選擇多進程的解決方案,但是進程的創建本身代價不小,而且進程間數據同步遠比不上線程間同步的高效。
epoll所支持的FD上限是最大可以打開文件的數目, /proc/sys/fs/file-max
-
IO效率可能隨著文件描述符數目的增加而線性下降。
select/poll是線性掃描FD的集合;epoll是根據FD上面的回調函數實現的,活躍的socket會主動去調用該回調函數,其它socket則不會,相當于市是一個AIO,只不過推動力在OS內核。
-
使用mmap加速內核與用戶空間的消息傳遞。
zero-copy的一種。
epoll的API更加簡單。
IO復用還有一個 水平觸發 和 邊緣觸發 的概念:
- 水平觸發:當就緒的fd未被用戶進程處理后,下一次查詢依舊會返回,這是select和poll的觸發方式。
- 邊緣觸發:無論就緒的fd是否被處理,下一次不再返回。理論上性能更高,但是實現相當復雜,并且任何意外的丟失事件都會造成請求處理錯誤。epoll默認使用水平觸發,通過相應選項可以使用邊緣觸發。
Java的IO模型
- 傳統的BIO模型
- 偽異步
- NIO
- AIO
BIO
BIO是一個典型的網絡編程模型,是通常我們實現一個服務端程序的過程。
步驟如下:
- 主線程accept請求阻塞
- 請求到達,創建新的線程來處理這個套接字,完成對客戶端的響應。
- 主線程繼續accept下一個請求
這種模型有一個很大的問題是:當客戶端連接增多時,服務端創建的線程也會暴漲,系統性能會急劇下降。
偽異步
在BIO模型的基礎上,類似于 tomcat的bio connector,采用的是線程池來避免對于每一個客戶端都創建一個線程:把請求拋到線程池中異步等待處理。
NIO
NIO API主要是三個部分:緩沖區(Buffers)、通道(Channels)和 Selector。
NIO基于事件驅動思想來實現的,它采用Reactor模式實現,主要用來解決BIO模型中一個服務端無法同時并發處理大量客戶端連接的問題。
NIO基于Selector進行輪訓,當socket有數據可讀、可寫、連接完成、新的TCP請求接入事件時,操作系統內核會觸發Selector返回準備就緒的SelectionKey集合,通過SelectableChannel進行讀寫操作。
由于jdk的Selector底層基于epoll實現,理論上可以同時處理操作系統最大文件句柄個數的連接。SelectableChannel的讀寫操作都是異步非阻塞的,當由于數據沒有就緒導致讀半包時,立即返回,不會同步阻塞等待數據就緒,當TCP緩沖區數據就緒之后,會觸發Selector的讀事件,驅動下一次讀操作。因此,一個Reactor線程就可以同時處理N歌客戶端的連接,使得Java服務器的并發讀寫能力得到極大的提升。
JDK1.4開始引入了NIO類庫,這里的NIO指的是Non-block IO,主要是使用Selector多路復用器來實現。Selector在Linux等主流操作系統上是通過epoll實現的。
NIO的實現流程,類似于select:
- 創建ServerSocketChannel監聽客戶端連接并綁定監聽端口,設置為非阻塞模式。
- 創建Reactor線程,創建多路復用器(Selector)并啟動線程。
- 將ServerSocketChannel注冊到Reactor線程的Selector上。監聽accept事件。
- Selector在線程run方法中無線循環輪詢準備就緒的Key。
- Selector監聽到新的客戶端接入,處理新的請求,完成tcp三次握手,建立物理連接。
- 將新的客戶端連接注冊到Selector上,監聽讀操作。讀取客戶端發送的網絡消息。
- 客戶端發送的數據就緒則讀取客戶端請求,進行處理。
相比BIO,NIO的編程非常復雜。
AIO
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 |
吞吐量 | 低 | 中 | 高 | 高 |
編程復雜度 | 簡單 | 簡單 | 非常復雜 | 復雜 |
概念
同步異步/阻塞非阻塞 角度一
同步和異步
這兩個概念與消息的通知機制 (synchronous communication/ asynchronous communication)有關。
[圖片上傳失敗...(image-8a971a-1527599992824)]
同步:
?
一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成后,依賴的任務才能算完成,這是一種可靠的任務序列。要么都成功,要么都失敗,兩個任務的狀態可以保持一致。? 在發出一個調用時,在沒有得到結果之前,該調用就不返回。但是一旦調用返回,就得到返回值了。換句話說,就是由調用者主動等待這個調用的結果。
? 對于同步型的調用,應用層需要自己去向系統內核問詢,如果數據還未讀取完畢,那此時讀取文件的任務還未完成,應用層根據其阻塞和非阻塞的劃分,或掛起或去做其他事情(所以同步和異步并不決定其等待數據返回時的狀態);如果數據已經讀取完畢,那此時系統內核將數據返回給應用層,應用層即可以用取得的數據做其他相關的事情。
異步:
?
不需要等到被依賴的任務完成,只是通知被依賴的任務要完成什么工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了。至于被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。? 調用在發出之后,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果。而是在調用發出后,被調用者*通過狀態、通知來通知調用者,或通過回調函數處理這個調用。
? 而對于異步型的調用,應用層無需主動向系統內核問詢,在系統內核讀取完文件數據之后,會主動通知應用層數據已經讀取完畢,此時應用層即可以接收系統內核返回過來的數據,再做其他事情。
也就是說,是否是同步還是異步,關注的是任務完成時消息通知的方式。由調用方盲目主動問詢的方式是同步調用,由被調用方主動通知調用方任務已完成的方式是異步調用。
消息的三種通知機制:狀態、通知和回調。
前者低效,后兩者高效、類似。
阻塞與非阻塞
這兩個概念與程序(線程)等待消息通知(無所謂同步或者異步)時的狀態有關。
[圖片上傳失敗...(image-5c552e-1527599992824)]
阻塞調用:
? 是指調用結果返回之前,當前線程會被掛起,一直處于等待消息通知,不能夠執行其他業務。函數只有在得到結果之后才會返回。
非阻塞調用:
? 和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回去完成其他任務。
總結來說,是否是阻塞還是非阻塞,關注的是接口調用(發出請求)后等待數據返回時的狀態。被掛起無法執行其他操作的則是阻塞型的,可以被立即「抽離」去完成其他「任務」的則是非阻塞型的。
阻塞和同步的討論
有人也許會把阻塞調用和同步調用等同起來,實際上它們是不同的。**
1、對于同步調用來說,很多時候當前線程可能還是激活的,只是從邏輯上當前函數沒有返回而已,此時,這個線程可能也會處理其他的消息。還有一點,在這里先擴展下:
(a) 如果這個線程在等待當前函數返回時,仍在執行其他消息處理,那這種情況就叫做同步非阻塞;
(b) 如果這個線程在等待當前函數返回時,沒有執行其他消息處理,而是處于掛起等待狀態,那這種情況就叫做同步阻塞;
所以同步的實現方式會有兩種:同步阻塞、同步非阻塞;同理,異步也會有兩種實現:異步阻塞、異步非阻塞;
2、對于阻塞調用來說,則當前線程就會被掛起等待當前函數返回;
雖然表面上看非阻塞的方式可以明顯的提高CPU的利用率,但是也帶了另外一種后果就是系統的線程切換增加。增加的CPU執行時間能不能補償系統的切換成本需要好好評估。
同步異步/阻塞非阻塞 角度二
[圖片上傳失敗...(image-3a670c-1527599992824)]
在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。Stevens給出的定義(其實是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。
同步異步/阻塞非阻塞 角度三
"同步異步"和"阻塞非阻塞"是兩個不同范圍的概念。
synchronous / asynchronous is to describe the relation between two modules.
blocking / non-blocking is to describe the situation of one module.
An example:
"I": a
"bookstore" : b
a asks b: do you have a book named "c++ primer"?
blocking: before b answers a, a keeps waiting there for the answer. Now a (one module) is blocking. a and b are two threads or two processes or one thread or one process? we DON'T know.
non-blocking: before b answers a, a just leaves there and every two minutes, a comes here for looking for the answer. Here a (one module) is non-blocking. a and b are two threads or two processes or one process? we DON'T know. BUT we are sure that a and b couldn't be one thread.
synchronous: before b answers a, a keeps waiting there for the answer. It means that a can't continue until b finishes its job. Now we say: a and b (two modules) is synchronous. a and b are two threads or two processes or one thread or one process? we DON'T know.
asynchronous: before b answers a, a leaves there and a can do other jobs. When b gets the answer, b will call a: hey! I have it! Then a will come to b to get the book when a is free. Now we say: a and b (two modules) is asynchronous. a and b are two threads or two processes or one process? we DON'T know. BUT we are sure that a and b couldn't be one thread.
同步異步/阻塞非阻塞 角度四
同步和異步是一個非常廣的概念,它們的重點在于多個任務和事件發生時,一個事件的發生或執行是否會導致整個流程的暫時等待。
阻塞和非阻塞的區別關鍵在于當發出請求一個操作時,如果條件不滿足,是會一直等待還是返回一個標志信息。
在討論IO(硬盤、網絡、外設)時,一個完整的IO讀請求操作包括兩個階段:
1)查看數據是否就緒;
2)進行數據拷貝(內核將數據拷貝到用戶線程)
阻塞(blocking IO)和非阻塞(non-blocking IO)的區別就在于第一個階段,如果數據沒有就緒,在查看數據是否就緒的過程中是一直等待,還是直接返回一個標志信息。
在《Unix網絡編程》一書中對同步IO和異步IO的定義是這樣的:
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和異步IO模型是針對用戶線程和內核的交互來說的:
同步IO:當用戶發出IO請求操作之后,如果數據沒有就緒,需要通過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶線程;
異步IO:只有IO請求操作的發出是由用戶線程來進行的,IO操作的兩個階段都是由內核自動完成,然后發送通知告知用戶線程IO操作已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。
這是同步IO和異步IO關鍵區別所在,同步IO和異步IO的關鍵區別反映在數據拷貝階段是由用戶線程完成還是內核完成。所以說<u>異步IO必須要有操作系統的底層支持</u>。
注意同步IO和異步IO與阻塞IO和非阻塞IO是不同的兩組概念。
阻塞IO和非阻塞IO是反映在當用戶請求IO操作時,如果數據沒有就緒,是用戶線程一直等待數據就緒,還是會收到一個標志信息這一點上面的。也就是說,阻塞IO和非阻塞IO是反映在IO操作的第一個階段,在查看數據是否就緒時是如何處理的。
同步/異步 與 阻塞/非阻塞
同步阻塞
效率是最低的,
異步阻塞
異步操作是可以被阻塞住的,只不過它不是在處理消息時阻塞,而是在等待消息通知時被阻塞。
同步非阻塞
實際上是效率低下的,
這個程序<u>需要在兩種不同的行為之間來回的切換</u>,效率可想而知是低下的。
異步非阻塞
效率更高。
用戶空間與內核空間
現在操作系統都是采用虛擬存儲器,對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。
操作系統的核心是內核,獨立于普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。
為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間劃分為兩部分,<u>一部分為內核空間,一部分為用戶空間</u>。
針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。
進程切換
為了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,并恢復以前掛起的某個進程的執行。這種行為被稱為進程切換/任務切換/上下文切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。
從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:
- 保存處理機上下文,包括程序計數器和其他寄存器。
- 更新PCB信息。
- 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
- 選擇另一個進程執行,并更新其PCB。
- 更新內存管理的數據結構。
- 恢復處理機上下文。
進程的阻塞
正在執行的進程,由于期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態??梢?,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不占用CPU資源的
。
文件描述符fd
文件描述符(File descriptor)是計算機科學中的一個術語,是一個用于表述指向文件的引用的抽象化概念
。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表
。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統。
緩存 IO
緩存 IO 又被稱作標準 IO,大多數文件系統的默認 IO 操作都是緩存 IO。在 Linux 的緩存 IO 機制中,操作系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩沖區中,然后才會從操作系統內核的緩沖區拷貝到應用程序的地址空間。
緩存 IO 的缺點:
數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。