一、Linux 基礎(chǔ)知識回顧
1. 用戶空間和內(nèi)核空間
現(xiàn)在操作系統(tǒng)都采用虛擬尋址际歼,處理器先產(chǎn)生一個虛擬地址惶翻,通過地址翻譯成物理地址(內(nèi)存的地址),再通過總線的傳遞鹅心,最后處理器拿到某個物理地址返回的字節(jié)维贺。
對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)巴帮。
操作系統(tǒng)的核心是內(nèi)核溯泣,獨立于普通的應(yīng)用程序虐秋,可以訪問受保護的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限垃沦。
為了保證用戶進程不能直接操作內(nèi)核(kernel)客给,保證內(nèi)核的安全,操心系統(tǒng)將虛擬空間劃分為兩部分肢簿,一部分為內(nèi)核空間靶剑,一部分為用戶空間。
針對linux操作系統(tǒng)而言:
將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF)池充,供內(nèi)核使用桩引,稱為內(nèi)核空間。
而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF)收夸,供各個進程使用坑匠,稱為用戶空間。
2. 直接I/O和緩存I/O
文件系統(tǒng)IO 分為 DirectIO(直接I/O)和 BufferIO(緩存 I/O)卧惜,其中 BufferIO 也叫Normal IO(標(biāo)準(zhǔn) I/O)厘灼。
大多數(shù)文件系統(tǒng)的默認 I/O 操作都是緩存 I/O。
緩存 I/O
讀操作:操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù)咽瓷,如果已經(jīng)緩存了设凹,那么就直接從緩存中返回;否則從磁盤中讀取茅姜,然后緩存在操作系統(tǒng)的緩存中闪朱。
寫操作:將數(shù)據(jù)從用戶空間復(fù)制到內(nèi)核空間的緩存中。這時對用戶程序來說寫操作就已經(jīng)完成钻洒,至于什么時候再寫到磁盤中由操作系統(tǒng)決定监透,除非顯示地調(diào)用了sync同步命令。
以 write 為例航唆,數(shù)據(jù)會先被拷貝進程緩沖區(qū)胀蛮,在拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會寫到存儲設(shè)備中糯钙。
直接 I/O(少了拷貝到應(yīng)用進程緩沖區(qū)這一步)
3.阻塞與同步
1 ) 阻塞(Block) / 非租塞(NonBlock)
阻塞和非阻塞是進程在訪問數(shù)據(jù)的時候粪狼,數(shù)據(jù)是否準(zhǔn)備就緒的一種處理方式,比如當(dāng)數(shù)據(jù)沒有準(zhǔn)備就緒的時候
阻塞:往往需要等待緩沖區(qū)中的數(shù)據(jù)準(zhǔn)備好過后才處理其他的事情任岸,否則一直等待在那里再榄。
非阻塞:當(dāng)我們的進程訪問我們的數(shù)據(jù)緩沖區(qū)的時候,如果數(shù)據(jù)沒有準(zhǔn)備好則直接返回享潜,不會等待困鸥。如果數(shù)據(jù)已經(jīng)準(zhǔn)備好,也直接返回。
阻塞和非阻塞關(guān)注的是程序在等待結(jié)果(消息疾就,返回值)時的狀態(tài)澜术。
2 ) 同步(Synchronization) / 異步(Asynchronization)
同步和異步都是基于應(yīng)用程序私操作系統(tǒng)處理IO事件所采用的方式,比如
同步:是應(yīng)用程序要直接參與IO讀寫的操作猬腰。
異步:所有的IO讀寫交給操作系統(tǒng)去處理鸟废,應(yīng)用程序只需要等待通知。
同步方式在處理IO事件的時候姑荷,必須阻塞在某個方法上面等待我們的IO事件完成(阻塞IO事件或者通過輪詢IO事件的方式)盒延。
對于異步來說,所有的IO讀寫都交給了操作系統(tǒng)鼠冕。這個時候添寺,我們可以去做其他的事情,并不需要去完成真正的IO操作懈费,當(dāng)操作完成IO后.會給我們的應(yīng)用程序一個通知计露。
同步和異步關(guān)注的是消息通信機制。
二楞捂、常見 IO 模型
對于一次IO訪問,它會經(jīng)歷兩個階段:
- 等待數(shù)據(jù)準(zhǔn)備就緒 (Waiting for the data to be ready)
- 操作:將數(shù)據(jù)從內(nèi)核拷貝到進程中 (Copying the data from the kernel to the process)
舉例來說:
讀函數(shù):分為等待系統(tǒng)可讀和真正的讀趋厉。
寫函數(shù):分為等待網(wǎng)卡可以寫和真正的寫寨闹。
說明:
等待就緒的阻塞是不使用 CPU 的,是在“空等”君账。
而真正的讀寫操作的阻塞是使用 CPU 的繁堡,真正在“干活”,而且這個過程非诚缡快椭蹄,屬于memory copy,寬帶通常在 1GB/s 級別以上净赴,可以理解為基本不耗時绳矩。
下圖是幾種常見I/O模型的對比:
以socket.read()為例子:
傳統(tǒng)的BIO里面socket.read(),如果TCP RecvBuffer里沒有數(shù)據(jù)玖翅,函數(shù)會一直阻塞翼馆,直到收到數(shù)據(jù),返回讀到的數(shù)據(jù)金度。
對于NIO应媚,如果TCP RecvBuffer有數(shù)據(jù),就把數(shù)據(jù)從網(wǎng)卡讀到內(nèi)存猜极,并且返回給用戶中姜;反之則直接返回0,永遠不會阻塞跟伏。
最新的AIO(Async I/O)里面會更進一步:不但等待就緒是非阻塞的丢胚,就連數(shù)據(jù)從網(wǎng)卡到內(nèi)存的過程也是異步的翩瓜。
換句話說,BIO里用戶最關(guān)心“我要讀”嗜桌,NIO里用戶最關(guān)心"我可以讀了"奥溺,在AIO模型里用戶更需要關(guān)注的是“讀完了”。
NIO一個重要的特點是:socket主要的讀骨宠、寫浮定、注冊和接收函數(shù),在等待就緒階段都是非阻塞的层亿,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)桦卒。
三、什么是 BIO匿又、NIO训柴、AIO
1. 同步阻塞I/O(BIO)
同步阻塞I/O朵锣,服務(wù)器實現(xiàn)模式為一個連接一個線程,即客戶端有連接請求時服務(wù)器就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷火邓,可以通過線程池機制來改善。
BIO方式適用于連接數(shù)目比較小且固定的架構(gòu)塑顺,這種方式對服務(wù)端資源要求比較高须误,并發(fā)局限于應(yīng)用中,在jdk1.4以前是唯一的io選擇旭绒,但程序直觀簡單易理解鸟妙。
BIO圖解:
偽異步模型IO
也被成為M:N客戶服務(wù)模型。即通過線程池模型的形式用M個線程來服務(wù)N個客戶端的連接挥吵;
其中M的大小可以根據(jù)服務(wù)器的配置來設(shè)置最大值重父,而可服務(wù)客戶端個數(shù)N則可以遠遠的大于M.
這樣來提高服務(wù)器的服務(wù)效率,提高線程利用率忽匈。
同BIO模型類似房午,只不過,Acceptor接受客戶端請求后丹允,不再獨立啟動線程來處理歪沃,而是將客戶請求交給線程池來處理,從而減少線程的創(chuàng)建數(shù)量嫌松,提高線程利用率沪曙,增加服務(wù)器的處理能力;
偽異步IO圖解
2. 同步非阻塞I/O(NIO)
同步非阻塞I/O萎羔,服務(wù)器實現(xiàn)模式為一個請求一個線程液走,即客戶端發(fā)送的連接請求都會注冊到多路復(fù)用器上,多路復(fù)用器輪詢到連接有IO請求時才啟動一個線程進行處理。
NIO方式適用于連接數(shù)目多且連接比較短(輕操作)的架構(gòu)缘眶,比如聊天服務(wù)器嘱根,并發(fā)局限于應(yīng)用中,編程比較復(fù)雜巷懈,jdk1.4開始支持该抒。
I/O多路復(fù)用模型
I/O多路復(fù)用:I/O就是指的我們網(wǎng)絡(luò)I/O,多路指多個TCP連接(或多個Channel),復(fù)用指復(fù)用一個或少量線程顶燕。
串起來理解就是很多個網(wǎng)絡(luò)I/O復(fù)用一個或少量的線程來處理這些連接凑保。
多路復(fù)用的優(yōu)勢并不是單個連接處理的更快,而是在于能處理更多的連接涌攻。
目前的常用的IO復(fù)用模型有三種:select欧引,poll,epoll恳谎。
I/O多路復(fù)用就是通過一種機制芝此,一個進程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒)因痛,能夠通知程序進行相應(yīng)的讀寫操作婚苹。
但select,poll鸵膏,epoll本質(zhì)上都是同步I/O膊升,因為他們都需要在讀寫事件就緒后自己負責(zé)進行讀寫,也就是說這個讀寫過程是阻塞的较性,而異步I/O則無需自己負責(zé)進行讀寫用僧,異步I/O的實現(xiàn)會負責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間结胀。
jdk1.4 是使用的 select/poll 模型
jdk1.5 以后把 select/poll 改為了epoll模型
1) select 模型
各個客戶端連接的文件描述符(fd)也就是套接字赞咙,都被放到了一個集合中,調(diào)用select函數(shù)之后會一直監(jiān)視這些文件描述符中有哪些可讀糟港,如果有可讀的描述符那么我們的工作進程就去讀取資源攀操。
我們在select函數(shù)中告訴內(nèi)核需要監(jiān)聽的不同狀態(tài)的文件描述符以及能接受的超時時間,函數(shù)會返回所有狀態(tài)下就緒的描述符的個數(shù)秸抚,并且可以通過遍歷fdset速和,來找到就緒的文件描述符。
存在的問題:
- 每次調(diào)用select剥汤,都需要把待監(jiān)控的fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)颠放,當(dāng)fd很大時,開銷很大吭敢。
- 每次調(diào)用select碰凶,都需要輪詢一遍所有的fd,查看就緒狀態(tài)。這個開銷在fd很多時也很大欲低。
- select支持的最大文件描述符數(shù)量有限辕宏,默認是1024
2) poll 模型
相對 于select,poll 已不存在最大文件描述符限制砾莱。
3) epoll 模型
epoll在Linux2.6內(nèi)核正式提出瑞筐,是基于事件驅(qū)動的I/O方式
相對于select來說,epoll沒有描述符個數(shù)限制腊瑟,使用一個文件描述符管理多個描述符聚假,將用戶關(guān)心的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次(可以理解為一塊公共內(nèi)存扫步,該內(nèi)存既不屬于用戶態(tài)也不屬于內(nèi)核態(tài))魔策。
select/poll的增強版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率河胎。原因就是獲取事件的時候闯袒,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊列的描述符集合就行了游岳。
epoll 的好處:
- 避免內(nèi)存級拷貝
- 事件驅(qū)動(不是輪詢)
但是也并不是所有情況下 epoll 都比 select/poll 好政敢,比如在如下場景:
在大多數(shù)客戶端都很活躍的情況下,系統(tǒng)會把所有的回調(diào)函數(shù)都喚醒胚迫,所以會導(dǎo)致負載較高喷户。既然要處理這么多的連接,那倒不如 select/poll 遍歷簡單有效访锻。
4)select & poll & epoll 比較
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍歷 | 遍歷 | 回調(diào) |
底層實現(xiàn) | 數(shù)組 | 鏈表 | 哈希表 |
IO 效率 | 每次調(diào)用都進行線性遍歷褪尝,時間復(fù)雜度為O(n) | 每次調(diào)用都進行線性遍歷,時間復(fù)雜度為O(n) | 事件通知方式期犬,每當(dāng)fd就緒河哑,系統(tǒng)注冊的回調(diào)函數(shù)就會被調(diào)用,將就緒fd放到readyList里面龟虎,時間復(fù)雜度O(1) |
最大連接數(shù) | 1024 | 無上限 | 無上限 |
fd 拷貝 | 每次調(diào)用select璃谨,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 調(diào)用epoll_ctl時拷貝進內(nèi)核并保存鲤妥,之后每次epoll_wait不拷貝 |
NIO的3個核心概念
1) 緩沖區(qū)Buffer
Buffer是一個對象佳吞。它包含一些要寫入或者讀出的數(shù)據(jù)。在面向流的I/O中棉安,可以將數(shù)據(jù)寫入或者將數(shù)據(jù)直接讀到Stream對象中底扳。
在NIO中,所有的數(shù)據(jù)都是用緩沖區(qū)處理贡耽。IO是面向流的衷模,NIO是面向緩沖區(qū)的羡滑。
最常用的緩沖區(qū)是ByteBuffer,一個ByteBuffer提供了一組功能用于操作byte數(shù)組算芯。除了ByteBuffer柒昏,還有其他的一些緩沖區(qū),事實上熙揍,每一種Java基本類型(除了Boolean)都對應(yīng)一種緩沖區(qū)职祷,具體如下:
- ByteBuffer:字節(jié)緩沖區(qū)
- CharBuffer:字符緩沖區(qū)
- ShortBuffer:短整型緩沖區(qū)
- IntBuffer:整型緩沖區(qū)
- LongBuffer:長整型緩沖區(qū)
- FloatBuffer:浮點型緩沖區(qū)
- DoubleBuffer:雙精度浮點型緩沖區(qū)
2) 通道Channel
Channel是一個通道,可以通過它讀取和寫入數(shù)據(jù)届囚,他就像自來水管一樣有梆,網(wǎng)絡(luò)數(shù)據(jù)通過Channel讀取和寫入。
通道和流不同之處在于通道是雙向的意系,流只是在一個方向移動泥耀,而且通道可以用于讀,寫或者同時用于讀寫蛔添。
因為Channel是全雙工的痰催,所以它比流更好地映射底層操作系統(tǒng)的API,特別是在UNIX網(wǎng)絡(luò)編程中迎瞧,底層操作系統(tǒng)的通道都是全雙工的夸溶,同時支持讀和寫。
Channel有四種實現(xiàn):
- FileChannel:是從文件中讀取數(shù)據(jù)凶硅。
- DatagramChannel:從UDP網(wǎng)絡(luò)中讀取或者寫入數(shù)據(jù)缝裁。
- SocketChannel:從TCP網(wǎng)絡(luò)中讀取或者寫入數(shù)據(jù)。
- ServerSocketChannel:允許你監(jiān)聽來自TCP的連接足绅,就像服務(wù)器一樣捷绑。每一個連接都會有一個SocketChannel產(chǎn)生。
3) 多路復(fù)用器Selector
Selector選擇器可以監(jiān)聽多個Channel通道感興趣的事情(read氢妈、write粹污、accept(服務(wù)端接收)、connect允懂,實現(xiàn)一個線程管理多個Channel厕怜,節(jié)省線程切換上下文的資源消耗衩匣。
Selector只能管理非阻塞的通道蕾总,F(xiàn)ileChannel是阻塞的,無法管理琅捏。
關(guān)鍵對象
Selector:選擇器對象生百,通道注冊、通道監(jiān)聽對象和Selector相關(guān)柄延。
SelectorKey:通道監(jiān)聽關(guān)鍵字蚀浆,通過它來監(jiān)聽通道狀態(tài)缀程。
監(jiān)聽注冊 監(jiān)聽注冊在Selector
socketChannel.register(selector, SelectionKey.OP_READ);
監(jiān)聽的事件
- OP_ACCEPT:接收就緒,serviceSocketChannel使用的
- OP_READ:讀取就緒市俊,socketChannel使用
- OP_WRITE:寫入就緒杨凑,socketChannel使用
- OP_CONNECT:連接就緒, socketChannel使用
NIO的應(yīng)用和框架
1) NIO的應(yīng)用
Java NIO成功的應(yīng)用在了各種分布式摆昧、即時通信和中間件Java系統(tǒng)中撩满,充分的證明了基于NIO構(gòu)建的通信基礎(chǔ),是一種高效绅你,且擴展性很強的通信架構(gòu)伺帘。
例如:Dubbo(服務(wù)框架),就默認使用Netty作為基礎(chǔ)通信組件忌锯,用于實現(xiàn)各進程節(jié)點之間的內(nèi)部通信伪嫁。
Jetty、Mina偶垮、Netty张咳、Dubbo、ZooKeeper等都是基于NIO方式實現(xiàn)似舵。
Mina出身于開源界的大牛Apache組織 Netty出身于商業(yè)開源大亨Jboss Dubbo阿里分布式服務(wù)框架
2) NIO框架
特別是Netty是目前最流行的一個Java開源框架NIO框架晶伦,Netty提供異步的、事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架和工具啄枕,用以快速開發(fā)高性能婚陪、高可靠性的網(wǎng)絡(luò)服務(wù)器和客戶端程序。
相比JDK原生NIO频祝,Netty提供了相對十分簡單易用的API泌参,非常適合網(wǎng)絡(luò)編程。
Mina和Netty這兩個NIO框架的創(chuàng)作者是同一個人Trustin Lee 常空。Netty從某種程度上講是Mina的延伸和擴展沽一,解決了一些Mina上的設(shè)計缺陷,也優(yōu)化了一下Mina上面的設(shè)計理念漓糙。
另一方面Netty相比較Mina的優(yōu)勢:
- 更容易學(xué)習(xí)
- API更簡單
- 詳細的范例源碼和API文檔
- 更活躍的論壇和社區(qū)
- 更高的代碼更新維護速度
Netty無疑是NIO框架的首選铣缠,它的健壯性、功能昆禽、性能蝗蛙、可定制性和可擴展性在同類框架都是首屈一指的,后續(xù)將重點詳細談Netty的實現(xiàn)原理以及實戰(zhàn)場景醉鳖。
異步非阻塞I/O(AIO)
服務(wù)器實現(xiàn)模式為一個有效請求一個線程捡硅,客戶端的I/O請求都是由OS先完成了再通知服務(wù)器應(yīng)用去啟動線程進行處理。
AIO方式適用于連接數(shù)目多且連接比較長(重操作)的架構(gòu)盗棵,比如相冊服務(wù)器壮韭,充分調(diào)用OS參與并發(fā)操作北发,編程比較復(fù)雜。
AIO又稱為NIO2喷屋,在JDK7才開始支持琳拨。