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