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