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