网络与IO知识扫盲(五):从 NIO 到多路复用器
NIO 的優(yōu)劣
優(yōu)勢(shì):相比 BIO 來說,NIO 可以通過1個(gè)或幾個(gè)線程,來解決 N 個(gè) IO 連接的處理
弊端:當(dāng)有大量文件描述符存在時(shí),不管你用多少個(gè)線程,都是O(n)復(fù)雜度的recv調(diào)用,需要用戶態(tài)內(nèi)核態(tài)切換才能實(shí)現(xiàn),而這些調(diào)用有很多是無(wú)意義的(有數(shù)據(jù)返回?cái)?shù)據(jù),無(wú)數(shù)據(jù)返回-1),浪費(fèi)資源。
read是無(wú)罪的,大量無(wú)效的read被調(diào)用才是性能損耗的關(guān)鍵。
下圖:左側(cè)是 NIO,右側(cè)是多路復(fù)用器。
多路復(fù)用器的實(shí)現(xiàn)
常問的幾個(gè)概念
(我們先只關(guān)注IO,不關(guān)注IO之后的處理)
同步:application 自己讀寫內(nèi)容
異步:由 kernel 完成 IO 內(nèi)容的讀寫,寫到進(jìn)程的一個(gè) buffer 區(qū)域里,看起來好像程序沒有訪問 IO,只是訪問了 buffer 就能拿到數(shù)據(jù)(實(shí)際上是在IO注冊(cè)了一些回調(diào)),只有 windows 上的 iocp 是純異步的
阻塞:Blocking,如果沒有則等待
非阻塞:Non-Blocking,一定能拿到返回值,就算沒有數(shù)據(jù),也會(huì)返回-1
目前來說,在Linux以及主流成熟框架中,我們常用的是同步阻塞、同步非阻塞的組合。
通過多路復(fù)用器只能獲取狀態(tài),最終還是需要由程序?qū)τ袪顟B(tài)的IO進(jìn)行讀/寫。
只要程序自己讀寫,那么你的IO模型就是同步的。(而不是你讀完了IO數(shù)據(jù)之后的處理的同步或異步)
多路復(fù)用器:select, poll, epoll 都是多路復(fù)用器,都屬于同步狀態(tài)下非阻塞的模型。
select 是在不同操作系統(tǒng)中很容易實(shí)現(xiàn)的,不依賴特定的軟硬件的一個(gè)系統(tǒng)調(diào)用。
epoll 要求內(nèi)核當(dāng)中要求一定的實(shí)現(xiàn),在linux上是epoll,在unix上是kqueue。技術(shù)是隨著問題的產(chǎn)生一步步發(fā)展起來的。
這兩者都是基于IO事件的一種通知行為。
異步阻塞是沒有意義的。異步都是用非阻塞。
關(guān)于為什么linux目前沒有通用的內(nèi)核異步處理方案,因?yàn)檫@樣不安全,會(huì)讓linux的內(nèi)核做的事情太多,容易出bug。windows敢于這么做,是因?yàn)閣indows的市場(chǎng)比較廣,一方面是用戶市場(chǎng),一方面是服務(wù)器市場(chǎng),它的市場(chǎng)比較廣,況且windows比較注重用戶市場(chǎng),所以敢于把內(nèi)核做的胖一些,也是因此雖然現(xiàn)在已經(jīng)win10了,但是藍(lán)屏啊,死機(jī)啊,掛機(jī)啊這些問題也還是會(huì)出現(xiàn)。Linux現(xiàn)在6.x版本當(dāng)中,對(duì)異步也開始上心了。
select
man select 幫助文檔中的描述:
翻譯:select()和pselect()允許程序監(jiān)視多個(gè)文件描述符,直到其中一個(gè)或多個(gè)文件描述符為某種I/O操作(如輸入可能)“準(zhǔn)備好”。如果文件描述符可以不阻塞地執(zhí)行相應(yīng)的I/O操作(如read(2)),則認(rèn)為它已經(jīng)準(zhǔn)備好了。
select在linux中有一個(gè)FD_SETSIZE(大小為1024)的限制,所以現(xiàn)在一般不用select了
其實(shí),無(wú)論是NIO,還是SELECT,還是POLL,這些多路復(fù)用器都是要遍歷所有的IO詢問狀態(tài)。
只不過,在NIO中,這個(gè)遍歷的成本在用戶態(tài)到內(nèi)核態(tài)的切換。
但是在SELECT、POLL的模型下,遍歷的過程觸發(fā)了一次系統(tǒng)調(diào)用(用戶態(tài)到內(nèi)核態(tài)的切換),過程中把很多的fd文件描述符傳遞給內(nèi)核,內(nèi)核重新根據(jù)用戶這次調(diào)用傳過來的所有fd,遍歷并修改狀態(tài)。每次都要重新重復(fù)傳遞fd。
所以多路復(fù)用器在這個(gè)時(shí)期,就已經(jīng)比NIO快了。
SELECT、POLL的弊端在于,每次都要重新傳遞fd,造成每次內(nèi)核被調(diào)用之后,針對(duì)這次調(diào)用都要觸發(fā)一個(gè)fd的全量遍歷的復(fù)雜度。
這里插入一個(gè)概念
在內(nèi)存中,有 kernel,有 app 等等的這些程序
軟中斷: trap int 80 等
硬中斷:時(shí)鐘中斷(晶振)
IO中斷:網(wǎng)卡、硬盤、鼠標(biāo)
關(guān)于網(wǎng)絡(luò)IO中斷
最開始的時(shí)候,網(wǎng)卡來了IO數(shù)據(jù)包的時(shí)候,是可以產(chǎn)生中斷的,這時(shí)候就會(huì)打斷CPU,將輸入的數(shù)據(jù)存到內(nèi)存中。
后來經(jīng)過改進(jìn),網(wǎng)卡是有buffer的,在內(nèi)存中開辟一個(gè)DMA區(qū)域,專門給網(wǎng)卡用,網(wǎng)卡可以收集很多數(shù)據(jù)之后積攢起來,積攢到一定量之后一起發(fā)給DMA。
中斷會(huì)產(chǎn)生callback回調(diào)函數(shù)
event有事件,就要去處理
在epoll之前的callback,只是完成了將網(wǎng)卡發(fā)來的數(shù)據(jù),走一下內(nèi)核的網(wǎng)絡(luò)協(xié)議棧(2鏈路,3網(wǎng)絡(luò),4傳輸層),最終關(guān)聯(lián)到fd的buffer里面
所以你在某一時(shí)間,如果從application詢問內(nèi)核某一個(gè)或者某些fd是否可讀/可寫,會(huì)有狀態(tài)返回。
如果內(nèi)核在回調(diào)的處理中,再加入(?紅黑樹、list),就有了多selector
epoll
epoll 規(guī)避了遍歷的問題。
幫助手冊(cè):
翻譯:
epoll API執(zhí)行與poll(2)類似的任務(wù):監(jiān)視多個(gè)文件描述符,看看其中任何一個(gè)文件描述符上是否有I/O。epoll API既可以用作邊緣觸發(fā)接口,也可以用作級(jí)別觸發(fā)接口,可以很好地?cái)U(kuò)展到大量監(jiān)視的文件描述符。提供以下系統(tǒng)調(diào)用來創(chuàng)建和管理一個(gè)epoll實(shí)例:
- epoll_create(2)創(chuàng)建一個(gè)epoll實(shí)例,并返回引用該實(shí)例的文件描述符。(最近的epoll_create1(2)擴(kuò)展了epoll_create(2)的功能。)
- 然后通過epoll_ctl(2)注冊(cè)對(duì)特定文件描述符的興趣。當(dāng)前在epoll實(shí)例上注冊(cè)的文件描述符集有時(shí)稱為epoll集。
- epoll_wait(2)等待I/O事件,如果當(dāng)前沒有可用的事件,則阻塞調(diào)用線程。
epoll_create
在內(nèi)核中開辟一塊空間,用來放紅黑樹
epoll_ctl
添加、修改、刪除某一個(gè)文件描述符,并記錄關(guān)注它的哪些事件(如read事件)
epoll_wait
epoll_wait 在等待從紅黑樹復(fù)制過來的一個(gè)鏈表
下圖:epoll(左) 與 select/poll 的本質(zhì)區(qū)別(右):
epoll 已經(jīng)悄悄地將結(jié)果集給你準(zhǔn)備好了,你需要有狀態(tài)的結(jié)果集fds的時(shí)候,直接取就可以了。它不傳遞 fds, 也不觸發(fā)內(nèi)核遍歷。
講了這么多操作系統(tǒng)內(nèi)核提供的多路復(fù)用器,最終我們都要回歸到并受制于Java對(duì)于這些系統(tǒng)調(diào)用的包裝:Selector
回歸到 Java 代碼
SocketMultiplexingSingleThreadv1.java
package com.bjmashibing.system.io;import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;public class SocketMultiplexingSingleThreadv1 {//這個(gè)代碼看不懂的話,可以去看馬老師的坦克 一、二期(netty)private ServerSocketChannel server = null;private Selector selector = null; //linux 多路復(fù)用器(select poll epoll kqueue) nginx event{}int port = 9090;public void initServer() {try {server = ServerSocketChannel.open();server.configureBlocking(false); // 設(shè)置成非阻塞server.bind(new InetSocketAddress(port)); // 綁定監(jiān)聽的端口號(hào)//如果在epoll模型下,Selector.open()其實(shí)完成了epoll_create,可能給你返回了一個(gè) fd3selector = Selector.open(); // 可以選擇 select poll *epoll,在linux中會(huì)優(yōu)先選擇epoll 但是可以在JVM使用-D參數(shù)修正//server 約等于 listen 狀態(tài)的 fd4/*register 初始化過程如果在select,poll的模型下,是在jvm里開辟一個(gè)數(shù)組,把fd4放進(jìn)去如果在epoll的模型下,調(diào)用了epoll_ctl(fd3,ADD,fd4,關(guān)注的是EPOLLIN*/server.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {e.printStackTrace();}}public void start() {initServer();System.out.println("服務(wù)器啟動(dòng)了。。。。。");try {while (true) { //死循環(huán)Set<SelectionKey> keys = selector.keys();System.out.println(keys.size() + " size");//1,調(diào)用多路復(fù)用器(select,poll or epoll(實(shí)質(zhì)上是調(diào)用的epoll_wait))/*java中的select()是啥意思:1,如果用select,poll 模型,其實(shí)調(diào)的是內(nèi)核的select方法,并傳入?yún)?shù)(fd4),或者poll(fd4)2,如果用epoll模型,其實(shí)調(diào)用的是內(nèi)核的epoll_wait()注意:參數(shù)可以帶時(shí)間。如果沒有時(shí)間,或者時(shí)間是0,代表阻塞。如果有時(shí)間,則設(shè)置一個(gè)超時(shí)時(shí)間。方法selector.wakeup()可以外部控制讓它不阻塞。這時(shí)select的結(jié)果返回是0。*/while (selector.select(500) > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys(); //拿到返回的有狀態(tài)的fd集合Iterator<SelectionKey> iter = selectionKeys.iterator(); // 轉(zhuǎn)成迭代器//所以,不管你是啥多路復(fù)用器,你只能告訴我fd的狀態(tài),我還得一個(gè)一個(gè)的去處理他們的R/W。同步好辛苦!!!//我們之前用NIO的時(shí)候,需要自己對(duì)著每一個(gè)fd調(diào)用系統(tǒng)調(diào)用,浪費(fèi)資源,那么你看,這里是不是調(diào)用了一次select方法,知道具體的那些可以R/W了?是不是很省力?while (iter.hasNext()) {SelectionKey key = iter.next();iter.remove(); //這時(shí)一個(gè)set,不移除的話會(huì)重復(fù)循環(huán)處理if (key.isAcceptable()) { //我前邊強(qiáng)調(diào)過,socket分為兩種,一種是listen的,一種是用于通信 R/W 的//這里是重點(diǎn),如果要去接受一個(gè)新的連接//語(yǔ)義上,accept接受連接且返回新連接的FD,對(duì)吧?//那新的FD怎么辦?//如果使用select,poll的時(shí)候,因?yàn)樗麄儍?nèi)核沒有空間,那么在jvm中保存,和前邊的fd4那個(gè)listen的放在一起//如果使用epoll的話,我們希望通過epoll_ctl把新的客戶端fd注冊(cè)到內(nèi)核空間acceptHandler(key);} else if (key.isReadable()) {readHandler(key);//在當(dāng)前線程,這個(gè)方法可能會(huì)阻塞,如果阻塞了十年,其他的IO早就沒電了。。。//所以,為什么提出了 IO THREADS,我把讀到的東西扔出去,而不是現(xiàn)場(chǎng)處理//你想,redis是不是用了epoll?redis是不是有個(gè)io threads的概念?redis是不是單線程的?//你想,tomcat 8,9版本之后,是不是也提出了一種異步的處理方式?是不是也在 IO 和處理上解耦?//這些都是等效的。}}}}} catch (IOException e) {e.printStackTrace();}}public void acceptHandler(SelectionKey key) {try {ServerSocketChannel ssc = (ServerSocketChannel) key.channel();SocketChannel client = ssc.accept(); //來啦,目的是調(diào)用accept接受客戶端 fd7client.configureBlocking(false);ByteBuffer buffer = ByteBuffer.allocate(8192); //前邊講過了// 0.0 我類個(gè)去//你看,調(diào)用了register/*select,poll: jvm里開辟一個(gè)數(shù)組 fd7 放進(jìn)去epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN*/client.register(selector, SelectionKey.OP_READ, buffer);System.out.println("-------------------------------------------");System.out.println("新客戶端:" + client.getRemoteAddress());System.out.println("-------------------------------------------");} catch (IOException e) {e.printStackTrace();}}public void readHandler(SelectionKey key) {SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();buffer.clear();int read = 0;try {while (true) {read = client.read(buffer);if (read > 0) {buffer.flip();while (buffer.hasRemaining()) {client.write(buffer);}buffer.clear();} else if (read == 0) {break;} else {client.close();break;}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();service.start();} }總結(jié)
以上是生活随笔為你收集整理的网络与IO知识扫盲(五):从 NIO 到多路复用器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络与IO知识扫盲(四):C10K问题、
- 下一篇: 网络与IO知识扫盲(六):多路复用器