常用 IO 模型图解介绍
很多時(shí)候?qū)τ诓煌腎O模型的概念和原理我們可能不是很清楚,有時(shí)候可能也會(huì)在不同的IO間迷糊,筆者也是有同樣的問題。所以經(jīng)過系統(tǒng)的學(xué)習(xí)以后將我們常見的五種IO模型在這里做一下總結(jié),以供大家參考和學(xué)習(xí)。
1.基本概念
五種IO模型包括:阻塞IO、非阻塞IO、IO多路復(fù)用、信號(hào)驅(qū)動(dòng)IO、異步IO。為了對(duì)后面的內(nèi)容的一些西域不混淆,首先給大家介紹一下系統(tǒng)調(diào)用常用的幾個(gè)函數(shù)和基本概念。
1.1 系統(tǒng)調(diào)用函數(shù)
以下幾個(gè)系統(tǒng)函數(shù)參考了一些書籍和文章,如果有不正確的地方還請(qǐng)大家指出。
| recvfrom | 提供給用戶用于接收網(wǎng)絡(luò)IO的系統(tǒng)接口,從套接字上接收一個(gè)消息,可同時(shí)應(yīng)用于面向連接和無連接的套接字如果此系統(tǒng)調(diào)用返回值<0,并且 errno為EWOULDBLOCK或EAGAIN(套接字已標(biāo)記為非阻塞,而接收操作被阻塞或者接收超時(shí) )時(shí),連接正常,阻塞接收數(shù)據(jù)(這很關(guān)鍵,前4種IO模型都設(shè)計(jì)此系統(tǒng)調(diào)用) |
| select | 允許程序同時(shí)在多個(gè)底層文件描述符上,等待輸入的到達(dá)或輸出的完成。以數(shù)組形式存儲(chǔ)文件描述符,64位機(jī)器默認(rèn)2048個(gè)。當(dāng)有數(shù)據(jù)準(zhǔn)備好時(shí),無法感知具體是哪個(gè)流OK了,所以需要一個(gè)一個(gè)的遍歷,函數(shù)的時(shí)間復(fù)雜度為O(n) |
| poll | 以鏈表形式存儲(chǔ)文件描述符,沒有長(zhǎng)度限制。本質(zhì)與select相同,函數(shù)的時(shí)間復(fù)雜度也為O(n) |
| epoll | 基于事件驅(qū)動(dòng)。如果某個(gè)流準(zhǔn)備好了,會(huì)以事件通知,知道具體是哪個(gè)流,因此不需要遍歷,函數(shù)的時(shí)間復(fù)雜度為O(1) |
| sigaction | 用于設(shè)置對(duì)信號(hào)的處理方式,也可檢驗(yàn)對(duì)某信號(hào)的預(yù)設(shè)處理方式。Linux使用SIGIO信號(hào)來實(shí)現(xiàn)IO異步通知機(jī)制 |
1.2同步與異步
同步和異步是針對(duì)應(yīng)用程序和內(nèi)核交互而言的。
| 同步 | 用戶進(jìn)程觸發(fā)IO操作并等待或輪詢的去查看是否就緒 |
| 異步 | 用戶進(jìn)程觸發(fā)IO操作以后便開始做自己的事情,當(dāng)IO操作已經(jīng)完成的時(shí)候會(huì)得到IO完成的通知,需要CPU支持 |
1.3阻塞與非阻塞
阻塞和非阻塞是針對(duì)于進(jìn)程在訪問數(shù)據(jù)的時(shí)候。根據(jù)IO操作的就緒狀態(tài)來采取的不同的方式。
| 阻塞 | 阻塞方式下讀取或?qū)懭敕椒▽⒁恢钡却?/td> |
| 非阻塞 | 非阻塞方式下讀取或?qū)懭敕椒〞?huì)立即返回一個(gè)狀態(tài)值 |
2.IO模型介紹
2.1 阻塞IO模型
學(xué)習(xí)過操作系統(tǒng)的伙伴應(yīng)該知道,不管是網(wǎng)絡(luò)IO還是磁盤IO,對(duì)于讀操作而言,都是等到網(wǎng)絡(luò)的某個(gè)數(shù)據(jù)分組到達(dá)后/數(shù)據(jù)準(zhǔn)備好后,將數(shù)據(jù)拷貝到內(nèi)核空間的緩沖區(qū)中,再?gòu)膬?nèi)核空間拷貝到用戶空間的緩沖區(qū)。執(zhí)行流程圖大致如圖:
通過流程圖可以看到,阻塞IO的執(zhí)行過程是進(jìn)程進(jìn)行系統(tǒng)調(diào)用,等待內(nèi)核將數(shù)據(jù)準(zhǔn)備好并復(fù)制到用戶態(tài)緩沖區(qū)后,進(jìn)程放棄使用CPU并一直阻塞在此,直到數(shù)據(jù)準(zhǔn)備好。
2.2 非阻塞IO模型
每次應(yīng)用程序詢問內(nèi)核是否有數(shù)據(jù)準(zhǔn)備好。如果就緒,就進(jìn)行拷貝操作;如果未就緒,就不阻塞程序,內(nèi)核直接返回未就緒的返回值,等待用戶程序下一個(gè)輪詢。在每一次詢問之前,對(duì)于程序來說是非阻塞的,占用CPU資源,可以做其他事情。執(zhí)行流程圖大致如圖:
主要有兩個(gè)階段:
- 等待數(shù)據(jù)階段:未阻塞, 用戶進(jìn)程需要盲等,不停的去輪詢內(nèi)核;
- 數(shù)據(jù)復(fù)制階段:阻塞,此時(shí)進(jìn)行數(shù)據(jù)復(fù)制。
在這兩個(gè)階段中,用戶進(jìn)程只有在數(shù)據(jù)復(fù)制階段被阻塞了,而等待數(shù)據(jù)階段沒有阻塞,但是用戶進(jìn)程需要盲等,不停地輪詢內(nèi)核,看數(shù)據(jù)是否準(zhǔn)備好。
2.3 IO多路復(fù)用模型
相比于阻塞IO模型,多路復(fù)用只是多了一個(gè)select/poll/epoll函數(shù)。select函數(shù)會(huì)不斷地輪詢自己所負(fù)責(zé)的文件描述符/套接字的到達(dá)狀態(tài),當(dāng)某個(gè)套接字就緒時(shí),就對(duì)這個(gè)套接字進(jìn)行處理。select負(fù)責(zé)輪詢等待,recvfrom負(fù)責(zé)拷貝。當(dāng)用戶進(jìn)程調(diào)用該select,select會(huì)監(jiān)聽所有注冊(cè)好的IO,如果所有IO都沒注冊(cè)好,調(diào)用進(jìn)程就阻塞。多路復(fù)用一般都是用于網(wǎng)絡(luò)IO,服務(wù)端與多個(gè)客戶端的建立連接。執(zhí)行流程圖大致如圖:
對(duì)于客戶端來說,一般感受不到阻塞,因?yàn)檎?qǐng)求來了,可以用放到線程池里執(zhí)行;但對(duì)于執(zhí)行select的操作系統(tǒng)而言,是阻塞的,需要阻塞地等待某個(gè)套接字變?yōu)榭勺x。IO多路復(fù)用其實(shí)是阻塞在select,poll,epoll這類系統(tǒng)調(diào)用上的,復(fù)用的是執(zhí)行select,poll,epoll的線程。
2.4 信號(hào)驅(qū)動(dòng)IO模型
當(dāng)數(shù)據(jù)報(bào)準(zhǔn)備好的時(shí)候,內(nèi)核會(huì)向應(yīng)用程序發(fā)送一個(gè)信號(hào),進(jìn)程對(duì)信號(hào)進(jìn)行捕捉,并且調(diào)用信號(hào)處理函數(shù)來獲取數(shù)據(jù)報(bào)。執(zhí)行流程圖大致如圖:
該模型也分為兩個(gè)階段:
- 數(shù)據(jù)準(zhǔn)備階段:未阻塞,當(dāng)數(shù)據(jù)準(zhǔn)備完成之后,會(huì)主動(dòng)的通知用戶進(jìn)程數(shù)據(jù)已經(jīng)準(zhǔn)備完成,對(duì)用戶進(jìn)程做一個(gè)回調(diào);
- 數(shù)據(jù)拷貝階段:阻塞用戶進(jìn)程,等待數(shù)據(jù)拷貝。
2.5 異步IO模型
用戶進(jìn)程發(fā)起系統(tǒng)調(diào)用后,立刻就可以開始去做其他的事情,然后直到I/O數(shù)據(jù)準(zhǔn)備好并復(fù)制完成后,內(nèi)核會(huì)給用戶進(jìn)程發(fā)送通知,告訴用戶進(jìn)程操作已經(jīng)完成了。
異步I/O執(zhí)行的兩個(gè)階段都不會(huì)阻塞讀寫操作,由內(nèi)核完成,完成后內(nèi)核將數(shù)據(jù)放到指定的緩沖區(qū),通知應(yīng)用程序來取。
3.BIO,NIO,AIO介紹
操作系統(tǒng)的IO模型是底層基石,Java對(duì)于IO的操作其實(shí)就是進(jìn)一步的封裝。
3.1 BIO
BIO–同步阻塞,JDK1.4之前常用的編程方式,適用于連接數(shù)目比較小且固定的架構(gòu),對(duì)服務(wù)器資源要求高,并發(fā)局限于應(yīng)用中。在使用的時(shí)候,首先在服務(wù)端啟動(dòng)一個(gè)ServerSocket來監(jiān)聽網(wǎng)絡(luò)請(qǐng)求,客戶端啟動(dòng)Socket發(fā)起網(wǎng)絡(luò)請(qǐng)求,默認(rèn)情況下ServerSocket會(huì)建立一個(gè)線程來處理此請(qǐng)求,如果服務(wù)端沒有線程可用,客戶端則會(huì)阻塞等待或遭到拒絕,并發(fā)效率比較低。
我們使用java來模擬IO模型的連接,首先編寫服務(wù)端的代碼:
編寫一個(gè)客戶端:
/*** @Author likangmin* @create 2020/12/02 13:35*/ public class Client {public static void main(String[] args) {String host=null;int port=0;if(args.length>2){host=args[0];port=Integer.parseInt(args[1]);}else{host="127.0.0.1";port=9999;}Socket socket=null;BufferedReader reader = null;PrintWriter writer = null;Scanner s = new Scanner(System.in);try{socket = new Socket(host, port);String message = null;reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));writer = new PrintWriter(socket.getOutputStream(), true);while(true){message = s.nextLine();if(message.equals("exit")){break;}writer.println(message);writer.flush();System.out.println(reader.readLine());}}catch(Exception e){e.printStackTrace();}finally{if(socket != null){try {socket.close();} catch (IOException e) {e.printStackTrace();}}socket = null;if(reader != null){try {reader.close();} catch (IOException e) {e.printStackTrace();}}reader = null;if(writer != null){writer.close();}writer = null;}} }然后啟動(dòng),先啟動(dòng)服務(wù)端,再啟動(dòng)客戶端,然后輸入數(shù)據(jù)
會(huì)發(fā)現(xiàn)服務(wù)端一直會(huì)阻塞在接收數(shù)據(jù)那里。
3.2 NIO
NIO–同步非阻塞,是基于事件驅(qū)動(dòng)思想來完成的,適用于連接數(shù)目多且連接比較短(輕操作)的架構(gòu),比如聊天服務(wù)器,并發(fā)局限于應(yīng)用中,編程復(fù)雜,JDK1.4 開始支持。當(dāng) socket 有流可讀或可寫入時(shí),操作系統(tǒng)會(huì)相應(yīng)地通知應(yīng)用程序進(jìn)行處理,應(yīng)用再將流讀取到緩沖區(qū)或?qū)懭氩僮飨到y(tǒng)。一個(gè)有效的請(qǐng)求對(duì)應(yīng)一個(gè)線程,當(dāng)連接沒有數(shù)據(jù)時(shí),是沒有工作線程來處理的。
服務(wù)器實(shí)現(xiàn)模式為一個(gè)請(qǐng)求一個(gè)通道,即客戶端發(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到多路復(fù)用器上,多路復(fù)用器輪詢到連接有 I/O 請(qǐng)求時(shí)才啟動(dòng)一個(gè)線程進(jìn)行處理。NIO中有幾個(gè)比較重要的角色:緩沖區(qū)Buffer,通道Channel,多路復(fù)用器Selector。
(1)Buffer
在NIO庫中,所有數(shù)據(jù)都是用緩沖區(qū)(用戶空間緩沖區(qū))處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;在寫入數(shù)據(jù)時(shí),也是寫入到緩沖區(qū)中。任何時(shí)候訪問NIO中的數(shù)據(jù),都是通過緩沖區(qū)進(jìn)行操作。
緩沖區(qū)實(shí)際上是一個(gè)數(shù)組,并提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問以及維護(hù)讀寫位置等信息。
(2)Channel
nio中對(duì)數(shù)據(jù)的讀取和寫入要通過Channel,它就像水管一樣,是一個(gè)通道。通道不同于流的地方就是通道是雙向的,可以用于讀、寫和同時(shí)讀寫操作。
(3)Selector
多路復(fù)用器,用于注冊(cè)通道。客戶端發(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到多路復(fù)用器上,多路復(fù)用器輪詢到連接有I/O請(qǐng)求時(shí)才啟動(dòng)一個(gè)線程進(jìn)行處理。
這里我們同樣寫一個(gè)demo作為例子,來讓大家知道怎么使用和使用NIO的步驟,為了讓大家看的比較清楚,在適當(dāng)?shù)牡胤教砑恿俗⑨尅M瑯拥南葘懛?wù)端的代碼:
然后再寫客戶端的代碼:
/*** @Author likangmin* @create 2020/12/02 13:35*/ public class NIOClient {public static void main(String[] args) {// 遠(yuǎn)程地址創(chuàng)建InetSocketAddress remote = new InetSocketAddress("localhost", 9999);SocketChannel channel = null;// 定義緩存。ByteBuffer buffer = ByteBuffer.allocate(1024);try {// 開啟通道channel = SocketChannel.open();// 連接遠(yuǎn)程服務(wù)器。channel.connect(remote);Scanner reader = new Scanner(System.in);while(true){System.out.print("put message for send to server > ");String line = reader.nextLine();if(line.equals("exit")){break;}// 將控制臺(tái)輸入的數(shù)據(jù)寫入到緩存。buffer.put(line.getBytes("UTF-8"));// 重置緩存游標(biāo)buffer.flip();// 將數(shù)據(jù)發(fā)送給服務(wù)器channel.write(buffer);// 清空緩存數(shù)據(jù)。buffer.clear();// 讀取服務(wù)器返回的數(shù)據(jù)int readLength = channel.read(buffer);if(readLength == -1){break;}// 重置緩存游標(biāo)buffer.flip();byte[] datas = new byte[buffer.remaining()];// 讀取數(shù)據(jù)到字節(jié)數(shù)組。buffer.get(datas);System.out.println("from server : " + new String(datas, "UTF-8"));// 清空緩存。buffer.clear();}} catch (IOException e) {e.printStackTrace();} finally{if(null != channel){try {channel.close();} catch (IOException e) {e.printStackTrace();}}}} }然后我們先啟動(dòng)服務(wù)端,然后再啟動(dòng)客戶端
3.3 AIO
AIO–異步非阻塞,進(jìn)行讀寫操作時(shí),只須直接調(diào)用api的read或write方法即可。一個(gè)有效請(qǐng)求對(duì)應(yīng)一個(gè)線程,客戶端的IO請(qǐng)求都是OS先完成了再通知服務(wù)器應(yīng)用去啟動(dòng)線程進(jìn)行處理。這里就不做代碼演示了,大家又需要可以自己去找一些代碼查看,操作起來比較簡(jiǎn)單。
4.總結(jié)
最后給大家做一下總結(jié),從效率上來說,可以簡(jiǎn)單理解為阻塞IO<非阻塞IO<多路復(fù)用IO<信號(hào)驅(qū)動(dòng)IO<異步IO。從同步和異步來說,只有異步IO模型是異步的,其他均為同步。
總結(jié)
以上是生活随笔為你收集整理的常用 IO 模型图解介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis专题-集群模式
- 下一篇: 记一次fastjson转jackson的