IO模型
我們的程序基本上都是對數據的IO操作以及基于CPU的運算。
基于Java的開發大部分是網絡相關的編程,不管是基于如Tomcat般的Web容器,或是基于Netty開發的應用間的RPC服務。為了提供系統吞吐量, 降低硬件資源的開銷,IO模型也在不斷適應大規模、高并發需求不斷演進,今天我們就來看看這個在網絡上高頻出現的詞匯IO模型
linux IO模型
首先我們要明確,用戶程序從計算機硬件讀取數據(包括文件、網絡數據等),會經歷數據從硬件設備中讀取到系統內核后,再拷貝到用戶空間的過程。在linux系統中,針對這一操作提供了5種IO模型用于優化不同場景下的IO操作。
- 同步阻塞IO 系統程序調用recvfrom阻塞等待內核將數據準備(從網卡將數據讀取到內存中)。之后用戶通過recvfrom等待內核將數據準備好,此時內核將數據從內核緩沖區復制到用戶態緩沖區。
blocking I/O發起system call recvfrom()時,進程將一直阻塞等待另一端Socket的數據到來。在該模式下,會阻塞其他連接的建立,因此一般都會通過多線程處理Socket數據的讀取。
Blocking I/O優點是簡單易用,對于本地I/O而言性能很高。缺點是處理網絡I/O時,造成進程阻塞,以及創建線程的資源消耗。
- 同步非阻塞IO
系統程序調用recvfrom時并不會阻塞等待,但是需要調用方不停的去輪詢內核,獲取數據準備狀態。之后用戶發起的(同步)recvfrom檢查到內核將數據準備好后,進行數據由內核到用戶空間的復制。
相對于阻塞I/O的等待,非阻塞I/O隔一段時間就就需要發起system call判斷數據是否就緒。如果數據就緒,就從kernel space復制到user space,操作數據; 否則,kernel會立即返回EWOULDBLOCK這個錯誤。
recvfrom有個參數叫flags,默認情況下阻塞。可以設置flag為非阻塞讓kernel在數據未就緒時直接返回。這就是”非阻塞”主要是指數據準備階段。
- IO多路復用
系統程序調用select/poll/epoll會阻塞等待至少有一個套接字就緒則返回。用戶(同步)調用recvfrom,獲取這些就緒的套接字,輪詢將數據由內核復制到用戶態緩沖區。
I/O Multiplexing首先向kernel發起system call,傳入file descriptor和感興趣的事件(readable、writable等)讓kernel監測, 當其中一個或多個fd數據就緒,就會返回結果。程序再發起真正的I/O操作recvfrom讀取數據。
- 信號驅動IO
系統調用sigaction不會阻塞。當數據準備完成之后,會主動的通知用戶進程數據已經準備完成,對用戶進程做一個回調。用戶發起的(同步)recvfrom將就緒的數據由內核復制到用戶態緩沖區。
第一次發起system call不會阻塞進程,kernel的數據就緒后會發送一個signal給進程。進而發起真正的IO操作。
- 異步IO
系統調用aio_read不會阻塞。直到I/O數據準備好內核會直接將數據復制到用戶空間,然后內核主動會給用戶進程發送通知,告訴用戶進程信號表示并進行數據處理。
既然說到異步IO,則前面的幾種IO模型都是同步的,由上圖可以看到,在數據拷貝(內核態到用戶態)時,仍然是阻塞的。在異步IO中,請求連接到內核后,從數據準備到復制整個過程 都是在內核中完成,對應用戶程序不會阻塞,直到請求數據完全準備好后,通過回調函數通知用戶程序完成整個IO操作。
Java中的IO模型
Java中提供的IO相關的API,主要是基于操作系統底層的IO的操作。在Java中的BIO、NIO、AIO屬于Java對操作系統的各種IO模型的封裝。當我們使用這些API時,不用關注底層IO的實現。
- BIO
同步阻塞IO ,服務端通過阻塞輸入流來監聽客戶端是否有數據寫入,當處理輸入數據時,程序會等待內核完成處理完成并返回后才會繼續執行。
上圖可以看到,服務端通過ServerSocket#accept阻塞方法監聽客戶端的接入,然后阻塞在通過阻塞輸入流等待客戶端的輸入,如果一直沒有輸入,則其他客戶端都會被阻塞在此。
我們可以通過多線程來改善,每個客戶端連接時,都由獨立的線程來處理,雖然通過多線程可以解決客戶端間的阻塞問題,但單個線程內然是阻塞模式, 并且當客戶端過多時需要足夠的線程來支持,比較耗費系統資源。
- NIO
同步非阻塞IO ,基于多路復用模型,依賴于服務器操作系統,通過一個Selector即可監聽多個連接,并進行IO處理。但要注意,如果處理IO的過程較長一樣會影響到其他的連接。
服務端通過Selector#select阻塞方法,監聽Channel狀態,一旦有Channel準備就緒,程序才會繼續往下執行,因此需要不斷輪詢并監控Channel的狀態變更。與BIO的多線程模式非常相似,只不過BIO是基于多線程技術實現,而NIO是基于操作系統底層提供的函數,效率更好且資源消耗更少。
- AIO
異步非阻塞IO ,在JDK1.7之后提供了異步的相關Channel,AIO提供異步功能, 基于回調函數實現 ,同樣依賴于操作系統底層的異步IO模型,異步操作的實現是在對應的 accept、connection、read、write等方法異步執行,完成后會主動調用回調函數。
其中accept、read等方法都是非阻塞的,即立即返回結果,幾乎所有的異步操作都是基于回調函數實現,這種方式不管是對操作系統資源的利用以及效率上都是最佳的實現。
雖然三種IO模型的演進是為了提升系統處理IO的能力,但是開發的復雜度也同步上升:
- BIO方式適用于連接數目比較小且固定的架構,需要依賴于線程來支持多個客戶端接入,但程序直觀簡單易理解。
- NIO方式適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程比較復雜。
- AIO方式使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與并發操作,編程比較復雜。
同/異步與(非)阻塞
關于阻塞、非阻塞、同步、異步這些名詞的解釋,可以在網上找到很多解釋,但是如何能夠從本質上描述其含義,正如IO與NIO中說到的阻塞與非阻塞,又是怎么體現的呢?
我們一般說說的IO模型,其實是服務端進行IO操作執行與實現的形式,程序將數據從程序寫入或讀寫時,與硬件設備(比如硬盤、網卡)間,基于操作系統提供的系統api實現數據由用戶態與內核態交互的一種形式。
- 同步
程序執行需要等待返回后才會繼續。 - 異步
與同步相反,比較直觀的就是線程。 - 阻塞IO
程序需要等待內核IO操作完成后返回到用戶空間繼續執行用戶程序的操作指令。這里的阻塞主要是調用操作系統api被阻塞導致程序掛起,描述的是程序當前執行的狀態。 - 非阻塞IO
既然阻塞是調用操作系統api被阻塞,那么非阻塞則相反,得益于操作系統提供的函數支持,一般是通過輪詢機制與回調函數實現。
同步與異步屬于程序發起請求的方式;阻塞與非阻塞屬于服務響應IO操作的底層實現方式。
示例
基于上面的理解,我們看下在Java中如何實現BIO、NIO以及AIO。
BIO
Server:
serverSocket = new ServerSocket(port);
// 阻塞直到有連接
Socket clientSocket = serverSocket.accept();
// 阻塞讀取數據
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
log.info(" >> >> > Server接收消息:{}" , reader.readLine());
socket.shutdownInput();
log.info(" >> >> > Server回復消息:{}" , message);
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println(message);
Client:
// 連接服務端
socket = new Socket("127.0.0.1",port);
OutputStream out = socket.getOutputStream();
out.write(message.getBytes());
socket.shutdownOutput();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
log.info("接收Server回復:{}", reader.readLine());
NIO
省略
AIO
Server:
//
serverSocketChannel = AsynchronousServerSocketChannel.open();
//綁定端口
serverSocketChannel.bind(new InetSocketAddress(port));
//異步接收客戶端連接
serverSocketChannel.accept(null, new AcceptCompletionHandler< String >());
/**
* 處理客戶端連接
* @param < T >
*/
public class AcceptCompletionHandler< T > implements CompletionHandler< AsynchronousSocketChannel,T > {
@Override
public void completed(AsynchronousSocketChannel result, T attachment) {
log.info(" >> > 客戶端接入...");
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
//異步讀客戶端數據
result.read(byteBuffer, byteBuffer, new ReadCompletionHandler());
//接收其他的客戶端連接的
serverSocketChannel.accept(null, this);
}
@Override
public void failed(Throwable exc, T attachment) {
log.error(" >> > 客戶端接入失敗:{}", exc.getMessage());
}
}
/**
* 處理ServerChannel讀取
* @param < T >
*/
public class ReadCompletionHandler< T extends Buffer > implements CompletionHandler< Integer, T >{
@Override
public void completed(Integer result, T attachment) {
if(attachment.hasRemaining()){
// 切換成讀模式
attachment.flip();
//
if( attachment instanceof ByteBuffer ){
byte[] bytes = new byte[attachment.remaining()];
((ByteBuffer)attachment).get(bytes); // 從Buffer中取數據 get
log.info("Server接收消息:{}", new String(bytes));
}
}
}
@Override
public void failed(Throwable exc, T attachment) {
log.error("Server接收消息失敗:{}", exc.getMessage());
}
}
Client:
//創建異步通道實例
socketChannel = AsynchronousSocketChannel.open();
//連接服務端,異步方式
socketChannel.connect(new InetSocketAddress("127.0.0.1",port), null, new ConnetionComplateHandler());
// 消息發送
this.socketChannel.write(Charset.defaultCharset().encode(message));
/**
*
* @param < T >
*/
public class ConnetionComplateHandler< T > implements CompletionHandler< Void, T > {
@Override
public void completed(Void result, T attachment) {
log.info("Client連接服務的成功...");
}
@Override
public void failed(Throwable exc, T attachment) {
}
}
結束語
通過了解操作系統層面的IO模型可以讓我們理解IO是如何實現,以及通過Java語言提供的類庫實現了操作系統底層API調用的復雜性。
-
IO
+關注
關注
0文章
435瀏覽量
39077 -
數據
+關注
關注
8文章
6888瀏覽量
88826 -
Linux系統
+關注
關注
4文章
591瀏覽量
27352 -
程序
+關注
關注
116文章
3775瀏覽量
80845
發布評論請先 登錄
相關推薦
評論