從5種網絡IO模型到零拷貝

1、什麼是IO

IO是輸入input輸出output的首字母縮寫形式,直觀意思是計算機輸入輸出,它描述的是計算機的數據流動的過程,所以IO第一大特徵是有數據的流動;另外,對於一次IO,它到底是輸入仍是輸出,是針對不一樣的主體而言的,不一樣的主體有不一樣的描述。可是對於一個Java程序員來講,咱們通常把程序當作IO的主體,也能夠理解爲內存中的進程。那麼對於IO的整個過程大致上分爲2個部分,第一個部分爲IO的調用,第二個過程爲IO的執行。IO的調用指的就是系統調用,IO的執行指的是在內核中相關數據的處理過程,這個過程是由操做系統完成的,與程序員無關。java

2、一些基本概念

阻塞IO:請求進程一直等待IO準備就緒。
非阻塞IO:請求進程不會等待IO準備就緒。
同步IO操做:致使請求進程阻塞,直到IO操做完成。
異步IO操做:不致使請求進程阻塞。linux

舉個小例子來理解阻塞,非阻塞,同步和異步的關係,咱們知道編寫一個程序能夠有多個函數,每個函數的執行都是相互獨立的,可是 對於一個程序的執行過程,每個函數都是必須的,那麼若是咱們須要等待一個函數的執行結束而後返回一個結果(好比接口調用),那麼咱們說該函數的調用是阻塞的,對於至少有一個函數調用阻塞的程序,在執行的過程當中,一定存在阻塞的一個過程,那麼咱們就說該程序的執行是同步的,對於異步天然就是全部的函數執行過程都是非阻塞的。

這裏的程序就是一次完整的IO,一個函數爲IO在執行過程當中的一個獨立的小片斷。程序員

咱們知道在Linux操做系統中內存分爲內核空間和用戶空間,而全部的IO操做都得得到內核的支持,可是因爲用戶態的進程沒法直接進行內核的IO操做,因此內核空間提供了系統調用,使得處於用戶態的進程能夠間接執行IO操做,IO調用的目的是將進程的內部數據遷移到外部即輸出,或將外部數據遷移到進程內部即輸入。而在這裏討論的數據一般是socket進程內部的數據。windows

3、5種IO模型

一、首先咱們來看看一次網絡請求中服務端作了哪些操做。

clipboard_愛奇藝.jpg
在上圖中,每個客戶端會與服務端創建一次socket鏈接,而服務端獲取鏈接後,對於全部的數據的讀取都得通過操做系統的內核,經過系統調用內核將數據複製到用戶進程的緩衝區,而後才完成客戶端的進程與客戶端的交互。那麼根據系統調用的方式的不一樣分爲阻塞和非阻塞,根據系統處理應用進程的方式不一樣分爲同步和異步。網絡

二、阻塞式IO

clipboard.png
每一次客戶端產生的socket鏈接其實是一個文件描述符fd,而每個用戶進程讀取的實際上也是一個個文件描述符fd,在該時期的系統調用函數會等待網絡請求的數據的到達和數據從內核空間複製到用戶進程空間,也就是說,不管是第一階段的IO調用仍是第二階段的IO執行都會阻塞,那麼就像圖中所畫的同樣,對於多個客戶端鏈接,只能開闢多個線程來處理。dom

三、非阻塞IO模型

對於阻塞IO模型來講最大的問題就體如今阻塞2字上,那麼爲了解決這個問題,系統的內核所以發生了改變。在內核中socket支持了非阻塞狀態。既然這個socket是不阻塞的了,那麼就可使用一個進程處理客戶端的鏈接,該進程內部寫一個死循環,不斷的詢問每個鏈接的網絡數據是否已經到達。此時輪詢發生在用戶空間,可是該進程依然須要本身處理全部的鏈接,因此該時期爲同步非阻塞IO時期,也即爲NIO。
clipboard.png異步

四、IO多路複用

在非阻塞IO模型中,雖然解決了IO調用阻塞的問題,可是產生了新的問題,若是如今有1萬個鏈接,那麼用戶線程會調用1萬次的系統調用read來進行處理,在用戶空間這種開銷太大,那麼如今須要解決這個問題,思路就是讓用戶進程減小系統調用,可是用戶本身是實現不了的,因此這就致使了內核發生了進一步變化。在內核空間中幫助用戶進程遍歷全部的文件描述符,將數據準備好的文件描述符返回給用戶進程。該方式是同步阻塞IO,由於在第一階段的IO調用會阻塞進程。jvm

4.一、select/poll

爲了讓內核幫助用戶進程完成文件描述符的遍歷,內核增長了系統調用select/poll(select與poll本質上沒有什麼不一樣,就是poll減小了文件描述符的個數限制),如今用戶進程只須要調用select系統調用函數,而且將文件描述符所有傳遞給select就可讓內核幫助用戶進程完成全部的查詢,而後將數據準備好的文件描述符再返回給用戶進程,最後用戶進程依次調用其餘系統調用函數完成IO的執行過程。
clipboard.pngsocket

4.二、epoll

在select實現的多路複用中依然存在一些問題。函數

一、用戶進程須要傳遞全部的文件描述符,而後內核將數據準備好的文件描述符再次傳遞回去,這種數據的拷貝下降了IO的速度。
二、內核依然會執行復雜度爲O(n)的主動遍歷操做。

對於第一個問題,提出了一個共享空間的概念,這個空間爲用戶進程和內核進程所共享,而且提供了mmap系統調用,實現用戶空間和內核空間到共享空間的映射,這樣用戶進程就能夠將1萬個文件描述符寫到共享空間中的紅黑樹上,而後內核將準備就緒的文件描述符寫入共享空間的鏈表中,而用戶進程發現鏈表中有數據了就直接讀取而後調用read執行IO便可。

對於第二個問題,內核引入了事件驅動機制(相似於中斷),再也不主動遍歷全部的文件描述符,而是經過事件驅動的方式主動通知內核該文件描述符的數據準備完畢了,而後內核就將其寫入鏈表中便可。

clipboard.png
對於epoll來講在第一階段的epoll_wait依然是阻塞的,故也是同步阻塞式IO。

五、信號驅動式IO

在IO執行的數據準備階段,不會阻塞用戶進程。當用戶進程須要等待數據的時候,會向內核發送一個信號,告訴內核須要數據,而後用戶進程就繼續作別的事情去了,而當內核中的數據準備好以後,內核立馬發給用戶進程一個信號,用戶進程收到信號以後,立馬調用recvfrom,去查收數據。該IO模型使用的較少。
clipboard.png

六、異步IO(AIO)

應用進程經過 aio_read 告知內核啓動某個操做,而且在整個操做完成以後再通知應用進程,包括把數據從內核空間拷貝到用戶空間。信號驅動 IO 是內核通知咱們什麼時候能夠啓動一個 IO 操做,而異步 IO 模型是由內核通知咱們 IO 操做什麼時候完成。是真正意義上的無阻塞的IO操做,可是目前只有windows支持AIO,linux內核暫時不支持。
clipboard.png

4、總結

前四種模型的主要區別於第一階段,由於他們的第二階段都是同樣的:在數據從內核拷貝到應用進程的緩衝區期間,進程都會阻塞。相反,異步 IO 模型在這兩個階段都不會阻塞,從而不一樣於其餘四種模型。
clipboard.png

5、直接內存與零拷貝

直接內存並非虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中農定義的內存區域。直接內存申請空間耗費更高的性能,直接內存IO讀寫的性能要優於普通的堆內存,對於java程序來講,系統內核讀取堆類的對象須要根據代碼段計算其偏移量來獲取對象地址,效率較慢,不太適合網絡IO的場景,對於直接內存來講更加適合IO操做,內核讀取存放在直接內存中的對象較爲方便,由於其地址就是裸露的進程虛擬地址,不須要jvm翻譯。那麼就可使用mmap開闢一塊直接內存mapbuffer和內核空間共享,而且該直接內存能夠直接映射到磁盤上的文件,這樣就能夠經過調用本地的put而不用調用系統調用write就能夠將數據直接寫入磁盤,RandomAccessFile類就是經過開闢mapbuffer實現的讀寫磁盤。

以消息隊列Kafka來講,有生產者和消費者,對於生產者,從網絡發來一個消息msg而且被拷貝到內核緩衝區,該消息經過Kafka調用recvfrom將內核中的msg讀到隊列中,而後加上消息頭head,再將該消息寫入磁盤。若是沒有mmap的話,就會調用一個write系統調用將該消息寫入內核緩衝區,而後內核將該消息再寫入磁盤。在此過程當中出現一次80中斷和2次拷貝。但實際上Kafka使用的是mmap開闢了直接內存到磁盤的映射,直接使用put將消息寫入磁盤。實際上也是經過內核訪問該共享區域將該消息寫入的磁盤。同時在Kafka中有一個概念叫segment,通常爲1G大小。它會充分利用磁盤的順序性,只追加數據,不修改數據。而mmap會直接開闢1G的直接內存,而且直接與segment造成映射關係,在segment滿了的時候再開闢一個新的segment,清空直接內存而後在與新的segment造成映射關係。
clipboard.png

零拷貝描述的是CPU不執行拷貝數據從一個存儲區域到另外一個存儲區域的任務,這一般用於經過網絡傳輸一個文件時以減小CPU週期和內存帶寬。

在Kafka的消費者讀取數據的時候,若是當前消費者想讀取的數據是否是當前直接內存所映射的segment怎麼辦?若是沒有零拷貝的話,進程會先去調用read讀取,而後數據會從磁盤被拷貝到內核,而後內核再拷貝到Kafka隊列,進程再調用write將數據拷貝到內核緩衝區,最後再發送給消費者。實際上能夠發現,數據沒有必要讀到Kafka隊列,直接讀到內核的緩衝區的時候發送給消費者就好了。實際上,linux內核中有一個系統調用就是實現了這種方式讀取數據——sendfile,它有2個參數,一個是infd(讀取數據的文件描述符),一個是outfd(客戶端的socket文件描述符).消費者只需調用該函數,告訴它須要讀取那個文件就能夠不通過Kafka直接將數據讀到內核,而後由內核寫到消費者進程的緩衝區中。
clipboard.png

相關文章
相關標籤/搜索