计算机IO系列(二)BIO/NIO/多路复用实现
一、什么是IO?
我們都知道Liux世界里、一切皆文件、而文件是什么呢?文件就是一串二進(jìn)制流而已、不管socket、還是FIFO、管道、終端、對(duì)我們來(lái)說(shuō)、一切都是文件、一切都是流、在信息交換的過(guò)程中、我們都是對(duì)這些流進(jìn)行數(shù)據(jù)的收發(fā)操作、簡(jiǎn)稱為I/O操作(input and output)、往流中讀出數(shù)據(jù)、系統(tǒng)調(diào)用read、寫入數(shù)據(jù)、系統(tǒng)調(diào)用write、不過(guò)話說(shuō)回來(lái)了、計(jì)算機(jī)里有這么多的流、我怎么知道要操作哪個(gè)流呢?做到這個(gè)的就是文件描述符、即通常所說(shuō)的fd、一個(gè)fd就是一個(gè)整數(shù)、所以對(duì)這個(gè)整數(shù)的操作、就是對(duì)這個(gè)文件(流)的操作、我們創(chuàng)建一個(gè)socket、通過(guò)系統(tǒng)調(diào)用會(huì)返回一個(gè)文件描述符、那么剩下對(duì)socket的操作就會(huì)轉(zhuǎn)化為對(duì)這個(gè)描述符的操作、不能不說(shuō)這又是一種分層和抽象的思想。
二、IO交互
通常用戶進(jìn)程中的一個(gè)完整IO分為兩個(gè)階段
用戶空間<------------->內(nèi)核空間、
?內(nèi)核空間<------------->設(shè)備空間、
?內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù)、而進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)、不管是內(nèi)核空間還是用戶空間、它們都處于虛擬空間中、Linux使用兩級(jí)保護(hù)機(jī)制:0級(jí)供內(nèi)核使用、3級(jí)供用戶程序使用、操作系統(tǒng)和驅(qū)動(dòng)程序運(yùn)行在內(nèi)核空間、應(yīng)用程序運(yùn)行在用戶空間、兩者不能簡(jiǎn)單地使用指針傳遞數(shù)據(jù)、因?yàn)長(zhǎng)inux使用的虛擬內(nèi)存機(jī)制、其必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求kernel來(lái)協(xié)助完成IO動(dòng)作、內(nèi)核會(huì)為每個(gè)IO設(shè)備維護(hù)一個(gè)緩沖區(qū)、用戶空間的數(shù)據(jù)可能被換出、當(dāng)內(nèi)核空間使用用戶空間指針時(shí)、對(duì)應(yīng)的數(shù)據(jù)可能不在內(nèi)存中。
對(duì)于一個(gè)輸入操作來(lái)說(shuō)、進(jìn)程IO系統(tǒng)調(diào)用后、內(nèi)核會(huì)先看緩沖區(qū)中有沒(méi)有相應(yīng)的緩存數(shù)據(jù)、沒(méi)有的話再到設(shè)備中讀取、因?yàn)樵O(shè)備IO一般速度較慢、需要等待、內(nèi)核緩沖區(qū)有數(shù)據(jù)則直接復(fù)制到進(jìn)程空間、
所以、對(duì)于一個(gè)網(wǎng)絡(luò)輸入操作通常包括兩個(gè)不同階段:
(1)等待網(wǎng)絡(luò)數(shù)據(jù)到達(dá)網(wǎng)卡 –> 讀取到內(nèi)核緩沖區(qū)
(2)從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù) –> 用戶空間
IO有內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤IO三種、通常我們說(shuō)的IO指的是后兩者。
阻塞IO(blocking I/O)
我們運(yùn)行一段服務(wù)端socket監(jiān)聽程序(典型的阻塞IO場(chǎng)景):
我們知道server.accept是阻塞的,如果沒(méi)有連接連上來(lái)就會(huì)一直等待不會(huì)往下執(zhí)行。
同時(shí)我們是道reader.readLIne也是阻塞的,不寫入東西也不會(huì)往下執(zhí)行。所以我們new了個(gè)線程,可以達(dá)到同時(shí)監(jiān)聽多個(gè)連接的目的。
其實(shí)網(wǎng)絡(luò)通信過(guò)程中的系統(tǒng)調(diào)用:前面兩個(gè)函數(shù)的阻塞的根因是因?yàn)閮?nèi)核的accept和recv的系統(tǒng)調(diào)用是阻塞調(diào)用,所以會(huì)有BIO。
這段程序中涉及到的系統(tǒng)調(diào)用如下:
java的bio對(duì)應(yīng)的包是:java.io.*
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream;2.非阻塞IO(noblocking I/O)
java的nio對(duì)應(yīng)的:java.nio.*(jdk1.4之后才有)
import java.nio.ByteBuffer; import java.nio.channels.SocketChannel;下面是一段典型的java nio服務(wù)端的代碼:其中
ss.configureBlocking(false)//很重要,表示設(shè)置為非阻塞ioss.accept在非阻塞模式下不會(huì)阻塞,
- 非阻塞模式:在調(diào)用accept方法后,如果無(wú)連接建立,則返回null(實(shí)際上系統(tǒng)調(diào)用的返回時(shí)-1,java返回時(shí)null);如果有連接,則返回SocketChannel。
我們就達(dá)到一個(gè)線程監(jiān)聽多個(gè)請(qǐng)求的作用。前面的BIO需要多個(gè)線程才能同時(shí)監(jiān)聽到多個(gè)請(qǐng)求。
SocketChannel簡(jiǎn)述:ServerSocketChannel簡(jiǎn)述_weixin_33951761的博客-CSDN博客
? ? ?注意for循環(huán)需要遍歷所有連接,向內(nèi)核發(fā)送recv系統(tǒng)調(diào)用,系統(tǒng)調(diào)用會(huì)產(chǎn)生軟中斷造成用戶態(tài)內(nèi)核態(tài)上下文切換,有很多無(wú)效系統(tǒng)調(diào)用。那怎么很容易想到減少系統(tǒng)調(diào)用的次數(shù)。
所以我們有了多路復(fù)用器,進(jìn)程發(fā)生系統(tǒng)調(diào)用前,先去查下有多少個(gè)可以讀:
IO多路復(fù)用
?????目前支持I/O多路復(fù)用的系統(tǒng)調(diào)用有?select,pselect,poll,epoll,I/O多路復(fù)用就是通過(guò)一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。epoll跟select都能提供多路I/O復(fù)用的解決方案。在現(xiàn)在的Linux內(nèi)核里有都能夠支持,其中epoll是Linux所特有,而select則應(yīng)該是POSIX所規(guī)定,一般操作系統(tǒng)均有實(shí)現(xiàn)。關(guān)于io多路復(fù)用可參考此文:網(wǎng)絡(luò)通信 --> IO多路復(fù)用之select、poll、epoll詳解 - 螞蟻吃大象、 - 博客園
對(duì)于 select 這種?式,需要進(jìn)? 2 次「遍歷」?件描述符集合,?次是在內(nèi)核態(tài)?,?個(gè)次是在?戶態(tài)? ,?且還會(huì)發(fā)? 2 次「拷?」?件描述符集合,先從?戶空間傳?內(nèi)核空間,由內(nèi)核修改后,再傳出到?戶空間中。
select 使?固定?度的 BitsMap,表示?件描述符集合,?且所?持的?件描述符的個(gè)數(shù)是有限制的,在Linux 系統(tǒng)中,由內(nèi)核中的 FD_SETSIZE 限制, 默認(rèn)最?值為 1024 ,只能監(jiān)聽 0~1023 的?件描述符。poll 不再? BitsMap 來(lái)存儲(chǔ)所關(guān)注的?件描述符,取?代之?動(dòng)態(tài)數(shù)組,以鏈表形式來(lái)組織,突破了 select 的?件描述符個(gè)數(shù)限制,當(dāng)然還會(huì)受到系統(tǒng)?件描述符限制。 但是 poll 和 select 并沒(méi)有太?的本質(zhì)區(qū)別,都是使?「線性結(jié)構(gòu)」存儲(chǔ)進(jìn)程關(guān)注的 Socket 集合,因此都需要遍歷?件描述符集合來(lái)找到可讀或可寫的 Socket,時(shí)間復(fù)雜度為 O(n),?且也需要在?戶態(tài)與內(nèi)核態(tài)之間拷??件描述符集合,這種?式隨著并發(fā)數(shù)上來(lái),性能的損耗會(huì)呈指數(shù)級(jí)增?高版本的jdk主要是用的是epoll系統(tǒng)調(diào)用:epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強(qiáng)版本。相對(duì)于select和poll來(lái)說(shuō),epoll更加靈活,沒(méi)有描述符限制。epoll使用一個(gè)文件描述符管理多個(gè)描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
基本原理:epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點(diǎn)在于邊緣觸發(fā),它只告訴進(jìn)程哪些fd剛剛變?yōu)榫途w態(tài),并且只會(huì)通知一次。還有一個(gè)特點(diǎn)是,epoll使用“事件”的就緒通知方式,通過(guò)epoll_ctl注冊(cè)fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來(lái)激活該fd,epoll_wait便可以收到通知。Epoll對(duì)于多核來(lái)說(shuō)很友好,相對(duì)于前面兩個(gè)系統(tǒng)調(diào)用,多了create和ctl,意味這不用每次都傳很多的文件描述符,但是在內(nèi)核里面增加了兩塊空間。空間換時(shí)間的做法。
java實(shí)際上用的是操作系統(tǒng)的系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)的自己的多路復(fù)用,也就是操作系統(tǒng)的多路復(fù)用是java的多路復(fù)用的基礎(chǔ)。
Reactor模型
剛剛的select和epoll都是關(guān)注某個(gè)io連接產(chǎn)生的事件。但是實(shí)際上,我們?cè)谔幚韎o的時(shí)候往往最關(guān)注的有相關(guān)業(yè)務(wù)的處理,并且我們關(guān)注的也不是網(wǎng)絡(luò)io的處理,而是之關(guān)心某個(gè)事件觸發(fā)然后執(zhí)行相應(yīng)的業(yè)務(wù)邏輯。所以我們需要封裝一層,將事件和IO多路復(fù)用抽象出來(lái),我們自己可以選擇自己的實(shí)現(xiàn),來(lái)方便我們進(jìn)行IO編程。使得的IO編程更加靈活。
組成:阻塞IO+IO多路復(fù)用
特征:以事件循環(huán)、事件驅(qū)動(dòng)、事件回調(diào)來(lái)實(shí)現(xiàn)業(yè)務(wù)邏輯處理
Reactor模式的抽象:
a, Handle表示句柄,文件描述符、socket等; 實(shí)際上就是對(duì)IO事件的抽象。實(shí)際上就是對(duì)fd進(jìn)行了包裝。
b, EventDemultiplexer表示多路分發(fā)機(jī)制,調(diào)用系統(tǒng)提供的多IO路復(fù)用,比如select,epoll。 程序先將關(guān)注的句柄注冊(cè)到EventDemultiplexer,當(dāng)有相關(guān)事件到來(lái)觸發(fā)EventDemultiplexer通知程序。
c, EventHandler定義事件處理方法,
d, Reactor是事件管理的接口,注冊(cè)和銷毀事件,并運(yùn)行事件循環(huán),當(dāng)EventDemultiplexer返回Handle有事件"就緒",將其分發(fā)給EventHandler上對(duì)應(yīng)的方法。
e, ConcreteEventhandler實(shí)現(xiàn)每個(gè)事件的處理邏輯。
Netty是典型的Reactor模型結(jié)構(gòu),關(guān)于Reactor的詳盡闡釋,本文站在巨人的肩膀上,借助 Doug Lea(就是那位讓人無(wú)限景仰的大爺)的“Scalable IO in Java”中講述的Reactor模式。
“Scalable IO in Java”的地址是:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
?
????????
此文來(lái)自于網(wǎng)課記錄一下
歷程:
總結(jié)
以上是生活随笔為你收集整理的计算机IO系列(二)BIO/NIO/多路复用实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CPU分时、中断和上下文切换
- 下一篇: springboot不能加载https的