## 1\. 前言
最近在學習 Java NIO 方面的知識,為了加深理解。特地去看了 Unix/Linux I/O 方面的知識,并寫了一些代碼進行驗證。在本文接下來的一章中,我將通過舉例的方式向大家介紹五種 I/O 模型。如果大家是第一次了解 I/O 模型方面的知識,理解起來會有一定的難度。所以在看文章的同時,我更建議大家動手去實現這些 I/O 模型,感覺會不一樣。好了,下面咱們一起進入正題吧。
## 2\. I/O 模型
本章將向大家介紹五種 I/O 模型,包括阻塞 I/O、非阻塞 I/O、I/O 復用、信號驅動式 I/O 、異步 I/O 等。本文的內容參考了《UNIX網絡編程》,文中所用部分圖片也是來自于本書。關于《UNIX網絡編程》這本書,我想就不用多說了。很多寫網絡編程方面的文章一般都會參考該書,本文也不例外。如果大家想進深入學習網絡編程,建議去讀讀這本書。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#21-阻塞-io-模型)2.1 阻塞 I/O 模型
阻塞 I/O 是最簡單的 I/O 模型,一般表現為進程或線程等待某個條件,如果條件不滿足,則一直等下去。條件滿足,則進行下一步操作。相關示意圖如下:
[](http://www.coolblog.xyz/)
上圖中,應用進程通過系統調用 recvfrom 接收數據,但由于內核還未準備好數據報,應用進程就阻塞住了。直到內核準備好數據報,recvfrom 完成數據報復制工作,應用進程才能結束阻塞狀態。
這里簡單解釋一下應用進程和內核的關系。內核即操作系統內核,用于控制計算機硬件。同時將用戶態的程序和底層硬件隔離開,以保障整個計算機系統的穩定運轉(如果用戶態的程序可以控制底層硬件,那么一些病毒就會針對硬件進行破壞,比如 CIH 病毒)。應用進程即用戶態進程,運行于操作系統之上,通過系統調用與操作系統進行交互。上圖中,內核指的是 TCP/IP 等協議及相關驅動程序。客戶端發送的請求,并不是直接送達給應用程序,而是要先經過內核。內核將請求數據緩存在內核空間,應用進程通過 recvfrom 調用,將數據從內核空間拷貝到自己的進程空間內。大致示意圖如下:
[](http://www.coolblog.xyz/)
阻塞 I/O 理解起來并不難,不過這里還是舉個例子類比一下。假設大家日常工作流程設這樣的(其實就是我日常工作的流程??),我們寫好代碼后,本地測試無誤,通過郵件的方式,告知運維同學發布服務。運維同學通過發布腳本打包代碼,重啟服務(心疼我司的人肉運維)。一般項目比較大時,重啟一次比較耗時。而運維同學又有點死腦筋,非要等這個服務重啟好,再去做其他事。結果一天等待的時間比真正工作的時間還要長,然后就被開了。運維同學用這個例子告訴我們,阻塞式 I/O 效率不太好。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#22-非阻塞-io-模型)2.2 非阻塞 I/O 模型
與阻塞 I/O 模型相反,在非阻塞 I/O 模型下。應用進程與內核交互,目的未達到時,不再一味的等著,而是直接返回。然后通過輪詢的方式,不停的去問內核數據準備好沒。示意圖如下:
[](http://www.coolblog.xyz/)
上圖中,應用進程通過 recvfrom 系統調用不停的去和內核交互,直到內核準備好數據報。從上面的流程中可以看出,應用進程進入輪詢狀態時等同于阻塞,所以非阻塞的 I/O 似乎并沒有提高進程工作效率。
再用上面的例子進行類比。公司辭退了上一個怠工的運維同學后,又招了一個運維同學。這個運維同學每次重啟服務,隔一分鐘去看一下,然后進入發呆狀態。雖然真正的工作時間增加了,但是沒用啊,等待的時間還是太長了。被公司發現后,又被辭了。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#23-io-復用模型)2.3 I/O 復用模型
Unix/Linux 環境下的 I/O 復用模型包含三組系統調用,分別是 select、poll 和 epoll(FreeBSD 中則為 kqueue)。select 出現的時間最早,在 BSD 4.2中被引入。poll 則是在 AT&T System V UNIX 版本中被引入(詳情請參考 UNIX man-page)。epoll 出現在 Linux kernel 2.5.44 版本中,與之對應的 kqueue 調用則出現在 FreeBSD 4.1,早于 epoll。select 和 poll 出現的時間比較早,在當時也是比較先進的 I/O 模型了,滿足了當時的需求。不過隨著因特網用戶的增長,C10K 問題出現。select 和 poll 已經不能滿足需求了,研發更加高效的 I/O 模型迫在眉睫。到了 2000 年,FreeBSD 率先發布了 select、poll 的改進版 kqueue。Linux 平臺則在 2002 年 2.5.44 中發布了 epoll。好了,關于三者的一些歷史就說到這里。本節接下來將以 select 函數為例,簡述該函數的使用過程。
select 有三個文件描述符集(readfds),分別是可讀文件描述符集(writefds)、可寫文件描述符集和異常文件描述符集(exceptfds)。應用程序可將某個 socket (文件描述符)設置到感興趣的文件描述符集中,并調用 select 等待所感興趣的事件發生。比如某個 socket 處于可讀狀態了,此時應用進程就可調用 recvfrom 函數把數據從內核空間拷貝到進程空間內,無需再等待內核準備數據了。示意圖如下:
[](http://www.coolblog.xyz/)
一般情況下,應用進程會將多個 socket 設置到感興趣的文件描述符集中,并調用 select 等待所關注的事件(比如可讀、可寫)處于就緒狀態。當某些 socket 處于就緒狀態后,select 返回處于就緒狀態的 sockct 數量。注意這里返回的是 socket 的數量,并不是具體的 socket。應用程序需要自己去確定哪些 socket 處于就緒狀態了,確定之后即可進行后續操作。
I/O 復用本身不是很好理解,所以這里還是舉例說明吧。話說公司的運維部連續辭退兩個運維同學后,運維部的 leader 覺得需要親自監督一下大家工作。于是 leader 在周會上和大家說,從下周開始,所有的發布郵件都由他接收,并由他轉發給相關運維同學,同時也由他重啟服務。各位運維同學需要告訴 leader 各自所負責監控的項目,服務重啟好后,leader 會通過內部溝通工具通知相關運維同學。至于服務重啟的結果(成功或失敗),leader 不關心,需要運維同學自己去看。運維同學看好后,需要把結果回復給開發同學。
上面的流程可能有點啰嗦,所以還是看圖吧。
[](http://www.coolblog.xyz/)
把上面的流程進行分步,如下:
1. 開發同學將發布郵件發送給運維 leader,并指明這個郵件應該轉發給誰
2. 運維告訴 leader,如果有發給我的郵件,請發送給我
3. leader 把郵件轉發給相關的運維同學,并著手重啟服務
4. 運維同學看完郵件,告訴 leader 某某服務重啟好后,請告訴我
5. 服務重啟好,leader 通知運維同學xx服務啟動好了
6. 運維同學查看服務啟動情況,并返回信息給開發同學
這種方式為什么可以提高工作效率呢?原因在于運維同學一股腦把他所負責的幾十個項目都告訴了 leader,由 leader 重啟服務,并通知運維同學。運維同學這個時候等待 leader 的通知,只要其中一個或幾個服務重啟好了,運維同學就回接到通知,然后就可去干活了。而不是像以前一樣,非要等某個服務重啟好再進行后面的工作。
說一下上面例子的角色扮演。開發同學是客戶端,leader 是內核。開發同學發的郵件相當于網絡請求,leader 接收郵件,并重啟服務,相當于內核準備數據。運維同學是服務端應用進程,告訴 leader 自己感興趣的事情,并在最后將事情的處理結果返回給開發同學。
不知道大家有沒有理解上面的例子,I/O 復用本身可能就不太好理解,所以看不懂也不要氣餒。另外,上面的例子只是為了說明情況,現實中并不會是這樣干,不然 leader 要累死了。如果大家覺得上面的例子不太好,我建議大家去看看權威資料《UNIX網絡編程》。同時,如果能用 select 寫個簡單的 tcp 服務器,有助于加深對 I/O 復用的理解。如果不會寫,也可以參考我寫的代碼[select\_server.c](https://github.com/coolblog-xyz/toyhttpd/blob/master/select_server.c)。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#24-信號驅動式-io-模型)2.4 信號驅動式 I/O 模型
信號驅動式 I/O 模型是指,應用進程告訴內核,如果某個 socket 的某個事件發生時,請向我發一個信號。在收到信號后,信號對應的處理函數會進行后續處理。示意圖如下:
[](http://www.coolblog.xyz/)
再用之前的例子進行說明。某個運維同學比較聰明,他寫了一個監控系統。重啟服務的過程由監控系統來做,做好后,監控系統會給他發個通知。在此之前,運維同學可以去做其他的事情,不用一直發呆等著了。運維同學收到通知后,首先去檢查服務重啟情況,接著再給開發同學回復郵件就行了。
相比之前的工作方式,是不是感覺這種方式更合理。從流程上來說,這種方式確實更合理。進程在信號到來之前,可以去做其他事情,而不用忙等。但現實中,這種 I/O 模型用的并不多。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#25-異步-io-模型)2.5 異步 I/O 模型
異步 I/O 是指應用進程把文件描述符傳給內核后,啥都不管了,完全由內核去操作這個文件描述符。內核完成相關操作后,會發信號告訴應用進程,某某 I/O 操作我完成了,你現在可以進行后續操作了。示意圖如下:
[](http://www.coolblog.xyz/)
上圖通過 aio\_read 把文件描述符、數據緩存空間,以及信號告訴內核,當文件描述符處于可讀狀態時,內核會親自將數據從內核空間拷貝到應用進程指定的緩存空間呢。拷貝完在告訴進程 I/O 操作結束,你可以直接使用數據了。
接著上一節的例子進行類比,運維小哥升級了他的監控系統。此時,監控系統不光可以監控服務重啟狀態,還能把重啟結果整理好,發送給開發小哥。而運維小哥要做的事情就更簡單了,收收郵件,點點監控系統上的發布按鈕。然后就可以悠哉悠哉的繼續睡覺了,一天一天的就這么過去了。
### [](http://www.tianxiaobo.com/2018/02/08/IO%E6%A8%A1%E5%9E%8B%E7%AE%80%E8%BF%B0/#26-總結)2.6 總結
上面介紹了5種 I/O 模型,也通過舉例的形式對每種模型進行了補充說明,不知道大家看懂沒。拋開上面的 I/O 模型不談,如果某種 I/O 模型能讓進程的工作的時間大于等待的時間,那么這種模型就是高效的模型。在服務端請求量變大時,通過 I/O 復用模型可以讓進程進入繁忙的工作狀態中,減少忙等,進而提高了效率。
I/O 復用模型結果數次改進,目前性能已經很好了,也得到了廣泛應用。像 Nginx,lighttd 等服務器軟件都選用該模型。好了,關于 I/O 模型就說到這里。
最后附一張幾種 I/O 模型的對比圖:
[](http://www.coolblog.xyz/)
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊