Redis:I/O模型

前言

前一篇概覽 Redis:概覽 中提到Redis使用了IO多路複用模型,所以單線程的Redis也很快。所以本篇主要講解Linux相關的I/O模型。

關於I/O我們可學了不少,java.io包下面的類全是關於I/O的操作。I/O就是Input/Output,是指輸入/輸出。

我們都知道I/O大致可以分爲BIO、NIO、AIO。BIO就是Blocking I/O(阻塞IO);NIO就是Non-Blocking I/O(非阻塞IO);AIO就是Asynchronous I/O(異步IO)。

說到這裏就不得不說一下阻塞/非阻塞、同步/異步了。阻塞/非阻塞和同步/異步完全是不同的概念,不要混爲一談。

  • 同步和異步:關注的是消息通信機制 。調用方發出一個調用後,響應方在沒有產生結果之前,都不會讓這個返回,也就是說響應方必須要產生了結果才能返回該調用,這就是同步;如果響應方在沒有產生結果之前,就立即返回了,至於真正的結果,往往通過回調的方式反饋給調用方,這就是異步
  • 阻塞和非阻塞:關注的是調用方在等待調用結果時的狀態。調用方發出一個調用後,響應方在沒有產生結果之前,調用方一直被掛起,不能做其他的事(敵不動,我不動),這就說明是阻塞的;如果響應方在沒有產生結果之前,調用方可以去做別的事,隔一段時間再來檢查調用是否返回了結果,這就是非阻塞的。

可以看到描述同步和異步的時候,只關心消息通信的機制,並不關心調用方的狀態;而描述阻塞和非阻塞的時候,只關心調用方的狀態,而不用關心消息通信機制。

舉個例子
你打電話問書店老闆有沒有《分佈式系統》這本書,如果是同步通信機制,書店老闆會說,你稍等,”我查一下",然後開始查啊查,等查好了(可能是5秒,也可能是一天)告訴你結果(返回結果)。而異步通信機制,書店老闆直接告訴你我查一下啊,查好了打電話給你,然後直接掛電話了(不返回結果)。然後查好了,他會主動打電話給你。在這裏老闆通過回電這種方式來回調。

還是這個例子
你打電話問書店老闆有沒有《分佈式系統》這本書,你如果是阻塞式調用,你會一直把自己“掛起”,直到得到這本書有沒有的結果,如果是非阻塞式調用,你不管老闆有沒有告訴你,你自己先一邊去玩了, 當然你也要偶爾過幾分鐘check一下老闆有沒有返回結果。

所有的系統I/O都分爲兩個階段:等待就緒執行操作

Linux的I/O模型主要有五類:阻塞 I/O(blocking IO)非阻塞 I/O(nonblocking IO)I/O 多路複用( IO multiplexing)信號驅動 I/O( signal driven IO)異步 I/O(asynchronous IO)

BIO

BIO模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。其操作流程如下
BIO調用流程
當調用方調用了recvfrom這個系統調用,kernel就開始了BIO的第一個階段:準備數據。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩衝區中是需要一個過程的。對用戶進程來說,自己會被阻塞。當kernel等到數據準備好了,它就會將數據從kernel中拷貝到用戶空間,然後kernel返回結果,用戶進程才解除阻塞的狀態,重新運行起來。

調用方從發起調用到收到響應,無論是等待就緒階段還是執行操作階段,一直都是被阻塞的,這種IO模型就是BIO。

NIO

NIO是同步非阻塞IO,其操作流程如下
NIO調用過程
當調用方調用了recvfrom這個系統調用,如果kernel中的數據還沒有準備好,會立刻返回一個error,調用方並不會被阻塞。對調用方來說,它發起一個read操作後,並不需要等待,而是馬上就得到了一個響應。調用方判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作,在兩次調用期間,可以進行別的操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的系統調用,那麼它馬上就將數據拷貝到了用戶內存,然後返回。

可以看到NIO相對於BIO的改進主要是等待就緒階段,需要不斷的發起調用來檢查響應方是否已經準備就緒,而後面的執行操作階段與BIO一致。

IO多路複用

NIO模型中,應用進程一直在以輪訓的方式調用系統函數recvfrom,輪詢會消耗大量的CPU資源。爲了改進這個問題,於是出現了IO多路複用模型,IO多路複用模型中,輪訓由內核來完成。內核可以監視多個描述符的讀/寫等事件,一旦某個描述符就緒(可讀或者可寫),就能夠將發生的事件通知給關心的應用程序去處理該事件。IO多路複用模型如下
IO多路複用
乍看之下,IO複用模型和BIO模型似乎區別不大,並且BIO模型只需要調用一個系統函數(recvfrom)而IO多路複用需要調用兩個系統函數(selectrecvfrom)。IO多路複用模型的優勢就是內核可以監視多個描述符的讀/寫等事件,也就是說一個線程可以處理多個連接,只有有任意的描述符可讀或者可寫,內核就通知對應的應用進程去處理。

IO多路複用的實現機制可以分成三類:selectpollepoll
select是最初實現IO多路複用的版本、poll是對select的優化,而epoll是對poll的優化。

select方式實現的IO多路複用模型存在如下三個問題:

  • 被監控的文件描述符集合有限制,一般32位默認是1024個。64位默認是2048。cat /proc/sys/fs/file-max命令可以查看
  • 文件描述符集合需要從用戶空間拷貝到內核空間的問題
  • 當被監控的文件描述符中某些有數據可讀的時候,希望能夠從通知中得到有可讀事件的文件描述符列表,而不是需要遍歷整個文件描述符集合。

poll方式只解決了第一個問題,也就是解除了被監控的文件描述符集合的限制。只有epoll解決了後面兩個問題。

對於用戶空間內核空間的數據拷貝問題,epoll通過內核與用戶空間mmap(內存映射)同一塊內存來解決。mmap將用戶空間的一塊地址和內核空間的一塊地址,同時映射到相同的一塊物理內存地址,使得這塊物理內存對內核和對用戶均可見,減少用戶態和內核態之間的數據拷貝。

epoll不是輪詢的方式遍歷文件描述符集合,不會隨着文件描述符數目的增加效率下降。只有活躍可用的文件描述符纔會調用回調函數。即epoll最大的優點就在於只關注可讀或者可寫的文件描述符。

信號驅動IO

信號驅動 I/O( signal driven IO)首先Socket進行信號驅動IO,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。過程如下圖所示
信號驅動IO

異步IO

異步 I/O(asynchronous IO):用戶進程進行aio_read系統調用之後,無論內核數據是否準備好,都會直接返回給用戶進程。用戶進程可以去做別的事情。等到socket數據準備好了,內核直接複製數據給用戶進程,然後從內核向進程發送通知。IO兩個階段,進程都是非阻塞的。異步過程如下圖所示:
AIO模型

總結

本文主要介紹了Linux操作系統中的五種I/O模型,一些應用軟件、java.io或者java.nio包下的類也是基於這些IO模型的封裝,所提供的上層API。理解IO模型非常重要,它能幫助我們構建跟高效的網絡服務器。

參考

  • https://www.zhihu.com/question/19732473/answer/20851256
  • https://www.zhihu.com/question/41706901
  • 《Unix網絡編程》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章