常用 IO 模型图解介绍
很多時候對于不同的IO模型的概念和原理我們可能不是很清楚,有時候可能也會在不同的IO間迷糊,筆者也是有同樣的問題。所以經過系統的學習以后將我們常見的五種IO模型在這里做一下總結,以供大家參考和學習。
1.基本概念
五種IO模型包括:阻塞IO、非阻塞IO、IO多路復用、信號驅動IO、異步IO。為了對后面的內容的一些西域不混淆,首先給大家介紹一下系統調用常用的幾個函數和基本概念。
1.1 系統調用函數
以下幾個系統函數參考了一些書籍和文章,如果有不正確的地方還請大家指出。
| recvfrom | 提供給用戶用于接收網絡IO的系統接口,從套接字上接收一個消息,可同時應用于面向連接和無連接的套接字如果此系統調用返回值<0,并且 errno為EWOULDBLOCK或EAGAIN(套接字已標記為非阻塞,而接收操作被阻塞或者接收超時 )時,連接正常,阻塞接收數據(這很關鍵,前4種IO模型都設計此系統調用) |
| select | 允許程序同時在多個底層文件描述符上,等待輸入的到達或輸出的完成。以數組形式存儲文件描述符,64位機器默認2048個。當有數據準備好時,無法感知具體是哪個流OK了,所以需要一個一個的遍歷,函數的時間復雜度為O(n) |
| poll | 以鏈表形式存儲文件描述符,沒有長度限制。本質與select相同,函數的時間復雜度也為O(n) |
| epoll | 基于事件驅動。如果某個流準備好了,會以事件通知,知道具體是哪個流,因此不需要遍歷,函數的時間復雜度為O(1) |
| sigaction | 用于設置對信號的處理方式,也可檢驗對某信號的預設處理方式。Linux使用SIGIO信號來實現IO異步通知機制 |
1.2同步與異步
同步和異步是針對應用程序和內核交互而言的。
| 同步 | 用戶進程觸發IO操作并等待或輪詢的去查看是否就緒 |
| 異步 | 用戶進程觸發IO操作以后便開始做自己的事情,當IO操作已經完成的時候會得到IO完成的通知,需要CPU支持 |
1.3阻塞與非阻塞
阻塞和非阻塞是針對于進程在訪問數據的時候。根據IO操作的就緒狀態來采取的不同的方式。
| 阻塞 | 阻塞方式下讀取或寫入方法將一直等待 |
| 非阻塞 | 非阻塞方式下讀取或寫入方法會立即返回一個狀態值 |
2.IO模型介紹
2.1 阻塞IO模型
學習過操作系統的伙伴應該知道,不管是網絡IO還是磁盤IO,對于讀操作而言,都是等到網絡的某個數據分組到達后/數據準備好后,將數據拷貝到內核空間的緩沖區中,再從內核空間拷貝到用戶空間的緩沖區。執行流程圖大致如圖:
通過流程圖可以看到,阻塞IO的執行過程是進程進行系統調用,等待內核將數據準備好并復制到用戶態緩沖區后,進程放棄使用CPU并一直阻塞在此,直到數據準備好。
2.2 非阻塞IO模型
每次應用程序詢問內核是否有數據準備好。如果就緒,就進行拷貝操作;如果未就緒,就不阻塞程序,內核直接返回未就緒的返回值,等待用戶程序下一個輪詢。在每一次詢問之前,對于程序來說是非阻塞的,占用CPU資源,可以做其他事情。執行流程圖大致如圖:
主要有兩個階段:
- 等待數據階段:未阻塞, 用戶進程需要盲等,不停的去輪詢內核;
- 數據復制階段:阻塞,此時進行數據復制。
在這兩個階段中,用戶進程只有在數據復制階段被阻塞了,而等待數據階段沒有阻塞,但是用戶進程需要盲等,不停地輪詢內核,看數據是否準備好。
2.3 IO多路復用模型
相比于阻塞IO模型,多路復用只是多了一個select/poll/epoll函數。select函數會不斷地輪詢自己所負責的文件描述符/套接字的到達狀態,當某個套接字就緒時,就對這個套接字進行處理。select負責輪詢等待,recvfrom負責拷貝。當用戶進程調用該select,select會監聽所有注冊好的IO,如果所有IO都沒注冊好,調用進程就阻塞。多路復用一般都是用于網絡IO,服務端與多個客戶端的建立連接。執行流程圖大致如圖:
對于客戶端來說,一般感受不到阻塞,因為請求來了,可以用放到線程池里執行;但對于執行select的操作系統而言,是阻塞的,需要阻塞地等待某個套接字變為可讀。IO多路復用其實是阻塞在select,poll,epoll這類系統調用上的,復用的是執行select,poll,epoll的線程。
2.4 信號驅動IO模型
當數據報準備好的時候,內核會向應用程序發送一個信號,進程對信號進行捕捉,并且調用信號處理函數來獲取數據報。執行流程圖大致如圖:
該模型也分為兩個階段:
- 數據準備階段:未阻塞,當數據準備完成之后,會主動的通知用戶進程數據已經準備完成,對用戶進程做一個回調;
- 數據拷貝階段:阻塞用戶進程,等待數據拷貝。
2.5 異步IO模型
用戶進程發起系統調用后,立刻就可以開始去做其他的事情,然后直到I/O數據準備好并復制完成后,內核會給用戶進程發送通知,告訴用戶進程操作已經完成了。
異步I/O執行的兩個階段都不會阻塞讀寫操作,由內核完成,完成后內核將數據放到指定的緩沖區,通知應用程序來取。
3.BIO,NIO,AIO介紹
操作系統的IO模型是底層基石,Java對于IO的操作其實就是進一步的封裝。
3.1 BIO
BIO–同步阻塞,JDK1.4之前常用的編程方式,適用于連接數目比較小且固定的架構,對服務器資源要求高,并發局限于應用中。在使用的時候,首先在服務端啟動一個ServerSocket來監聽網絡請求,客戶端啟動Socket發起網絡請求,默認情況下ServerSocket會建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕,并發效率比較低。
我們使用java來模擬IO模型的連接,首先編寫服務端的代碼:
編寫一個客戶端:
/*** @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;}} }然后啟動,先啟動服務端,再啟動客戶端,然后輸入數據
會發現服務端一直會阻塞在接收數據那里。
3.2 NIO
NIO–同步非阻塞,是基于事件驅動思想來完成的,適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程復雜,JDK1.4 開始支持。當 socket 有流可讀或可寫入時,操作系統會相應地通知應用程序進行處理,應用再將流讀取到緩沖區或寫入操作系統。一個有效的請求對應一個線程,當連接沒有數據時,是沒有工作線程來處理的。
服務器實現模式為一個請求一個通道,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求時才啟動一個線程進行處理。NIO中有幾個比較重要的角色:緩沖區Buffer,通道Channel,多路復用器Selector。
(1)Buffer
在NIO庫中,所有數據都是用緩沖區(用戶空間緩沖區)處理的。在讀取數據時,它是直接讀到緩沖區中的;在寫入數據時,也是寫入到緩沖區中。任何時候訪問NIO中的數據,都是通過緩沖區進行操作。
緩沖區實際上是一個數組,并提供了對數據的結構化訪問以及維護讀寫位置等信息。
(2)Channel
nio中對數據的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。通道不同于流的地方就是通道是雙向的,可以用于讀、寫和同時讀寫操作。
(3)Selector
多路復用器,用于注冊通道。客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
這里我們同樣寫一個demo作為例子,來讓大家知道怎么使用和使用NIO的步驟,為了讓大家看的比較清楚,在適當的地方添加了注釋。同樣的先寫服務端的代碼:
然后再寫客戶端的代碼:
/*** @Author likangmin* @create 2020/12/02 13:35*/ public class NIOClient {public static void main(String[] args) {// 遠程地址創建InetSocketAddress remote = new InetSocketAddress("localhost", 9999);SocketChannel channel = null;// 定義緩存。ByteBuffer buffer = ByteBuffer.allocate(1024);try {// 開啟通道channel = SocketChannel.open();// 連接遠程服務器。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;}// 將控制臺輸入的數據寫入到緩存。buffer.put(line.getBytes("UTF-8"));// 重置緩存游標buffer.flip();// 將數據發送給服務器channel.write(buffer);// 清空緩存數據。buffer.clear();// 讀取服務器返回的數據int readLength = channel.read(buffer);if(readLength == -1){break;}// 重置緩存游標buffer.flip();byte[] datas = new byte[buffer.remaining()];// 讀取數據到字節數組。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();}}}} }然后我們先啟動服務端,然后再啟動客戶端
3.3 AIO
AIO–異步非阻塞,進行讀寫操作時,只須直接調用api的read或write方法即可。一個有效請求對應一個線程,客戶端的IO請求都是OS先完成了再通知服務器應用去啟動線程進行處理。這里就不做代碼演示了,大家又需要可以自己去找一些代碼查看,操作起來比較簡單。
4.總結
最后給大家做一下總結,從效率上來說,可以簡單理解為阻塞IO<非阻塞IO<多路復用IO<信號驅動IO<異步IO。從同步和異步來說,只有異步IO模型是異步的,其他均為同步。
總結
以上是生活随笔為你收集整理的常用 IO 模型图解介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis专题-集群模式
- 下一篇: 记一次fastjson转jackson的