Java NIO學習系列五:I/O模型

木瓜芒果發表於2019-07-22

  前面總結了很多IO、NIO相關的基礎知識點,還總結了IO和NIO之間的區別及各自適用場景,本文會從另一個視角來學習一下IO,即IO模型。什麼是IO模型?對於不同人、在不同場景下給出的答案是不同的,所以先限定一下本文的上下文:Linux環境下的network IO。

  本文會從如下幾個方面展開:

  一些基礎概念

  I/O模型

  總結

 

1. 一些基礎概念

  IO模型這個概念屬於比較基礎的底層概念,在此之前容我再先簡單介紹一些涉及到的更底層的概念,幫助對I/O模型的理解:

1.1 使用者空間與核心空間

  現在作業系統都是採用虛擬儲存器,對於32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程式不能直接操作核心(kernel),保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程式使用,稱為使用者空間。

1.2 檔案描述符

  對於核心而言,所有開啟檔案都由檔案描述符引用。檔案描述符是一個非負整數。當開啟一個現存檔案或建立一個新檔案時,核心向程式返回一個檔案描述符。當讀、寫一個檔案時,用open或create返回的檔案描述符標識該檔案,將其作為引數傳送給read或write。檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。

1.3 快取 I/O

  快取I/O又被稱作標準I/O,大多數檔案系統的預設I/O操作都屬於快取I/O。在Linux的快取I/O機制中,作業系統會將I/O的資料快取在檔案系統的頁快取(page cache)中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。

  因為資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,所以這些資料拷貝操作所帶來的CPU以及記憶體開銷是非常大的,這也是快取I/O所帶來的缺點。

 

2. I/O模型

  上面有提到,對於一次IO訪問(比如read),資料會先被複制到作業系統核心緩衝區中,然後再從作業系統核心的緩衝區複製到應用程式的地址空間。也就是說,當一個IO操作發生時,會經歷兩個階段:

  1. 資料準備階段(Waiting for the data to be ready);
  2. 將資料從核心複製到程式中(Copying the data from the kernel to the process);

  這是因為存在上面兩個階段,linux系統產生了下面5種I/O模型:

  • 阻塞I/O(blocking IO)
  • 非阻塞I/O(nonblocking IO)
  • I/O多路複用(IO multiplexing)
  • 訊號驅動I/O(signal driven IO)
  • 非同步I/O(asynchronous IO)

  下面我們來一一介紹(本文暫介紹除訊號驅動I/O外其餘4種IO模型)。

2.1 阻塞I/O(blocking IO)

  在linux中,預設情況下socket都是阻塞式的,一個典型的讀操作流程大概是這樣的:

  當使用者程式呼叫recvfrom這個系統呼叫時,kernel就開始了IO的第一個階段:數準備階段(對於網路IO來說,很多時候資料在一開始還沒有到達。比如,還沒有收到一個完整的TCP包。這個時候kernel就要等待直到所有資料到達),這個過程相當於將資料被複制到作業系統核心的緩衝區,是需要一個過程,需要等待的。而在使用者程式這邊,整個程式會被阻塞(當然,是程式自己選擇的阻塞)。當kernel一直等到資料準備好了,它就會將資料從kernel中複製到使用者記憶體,然後kernel返回結果,使用者程式才會解除block的狀態,重新執行起來。

  Blocking IO最大的特點就是在IO執行的兩個階段都被會阻塞。

2.2 非阻塞I/O(nonblocking IO) 

  可以將設定socket設定為non-blocking模式,對於此時的讀操作,流程大概是這個樣子的:

  當使用者程式發起recvfrom這個系統呼叫時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程式,而是立刻返回一個error。從使用者程式角度講 ,它發起一個recvfrom呼叫,馬上就得到了一個結果,不管有沒有資料。使用者程式判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次發起recvfrom操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程式的system call,那麼它接下來就將資料複製到使用者記憶體,然後返回。

  Nonblocking IO的特點是使用者程式需要不斷的主動詢問kernel資料好了沒有,這個過程不阻塞,但是在IO的第二個階段還是需要等待核心將資料複製到使用者程式的,也就是這部分還是會阻塞的。

2.3 I/O多路複用(IO multiplexing)

  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的過程是一樣的。

2.4 非同步I/O(asynchronous IO)

  Asynchronous IO不同於上面三種模型,先看一下它的流程:

  使用者程式發起read操作之後,立刻就可以開始去做其它的事情了。另一方面,從kernel的角度,當它收到一個asynchronous read呼叫之後,首先它會立刻返回,所以不會對使用者程式產生任何block。然後,kernel會等待資料準備完成,並且資料準備好之後再將資料複製到使用者記憶體,當這一切都完成之後,kernel會給使用者程式傳送一個signal,告訴它read操作完成了。

  其實,從這裡可以看出非同步I/O最大的特點就是,將前面提到的兩步IO操作全部非同步化了,整個過程完全不阻塞,而Nonblocking IO在複製資料到使用者程式這一步其實還是阻塞的,只是資料準備階段不阻塞而已。

 

 3. 總結

  本文總結了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)。在這五種模型的基礎上,有兩個概念不得不提,阻塞和非阻塞、同步和非同步,這兩個概念容易搞混,:

3.1 阻塞和非阻塞

  阻塞很好理解,就是程式或者執行緒停在那裡等待某個狀態,什麼都不幹。但是什麼情況稱為阻塞,什麼情況稱為非阻塞呢?在IO模型中的定義是取決於前面提到的第一階段:資料準備階段,也就是說,在這個階段會導致程式或執行緒阻塞的IO就成為阻塞式IO,反之就是非阻塞式IO。所以Nonblocking IO在第一階段是可以做別的事情的,但是在第二階段任然是阻塞的,這點需要注意。

3.2 同步和非同步

  在說明同步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模型比較圖,以幫助理解:

 

參考文獻

Linux IO模式及 select、poll、epoll詳解

 

相關文章