Title:BIO\NIO\多路復用 區別和淺解
技術博客已遷移至個人頁,歡迎查看 yloopdaed.icu
您也可以關注 JPP - 這是一個Java養成計劃,需要您的加入。
前言
網絡通信中BIO、NIO、多路復用相關的知識點非常多,網絡上也有很多相關的技術文章。但是每個人的角度和切入點不同,知識涵蓋的內容太多了,很難整理出一條清晰的思路。
我這兩天嘗試著脫離代碼(因為加入代碼Demo之后,篇幅增長,很多API也不是很熟悉,增加閱讀的難度),單純從操作系統網絡通信的流程去梳理BIO、NIO,再到多路復用的優缺點。最終達到了解這些技術的聯系和發展的目的。
BIO
此流程基于Java示例代碼,代碼可以查看 JPP /IOTest 類
主線程:
1 服務端等待客戶端連接,阻塞進程
2 檢測到客戶端連接,返回對應的文件描述符fd,并將連接移動到子線程
3 開始下一次循環,跳回第1步
子線程:
1 子線程中服務端等待客戶端響應,阻塞進程
2 接收到客戶端響應,子線程處理
3 開始下一次循環,跳回第1步
優勢:可以讓每一個連接專注于自己的I/O并且編程模型簡單,也不用過多考慮系統的過載、限流等問題。
問題:
1 線程的創建和銷毀成本很高
2 線程本身占用較大內存
3 線程的切換成本是很高的
4 容易造成鋸齒狀的系統負載
為了解決BIO的缺點,進入NIO
NIO
此流程基于Java示例代碼,代碼可以查看 JPP /NIOTest 類
本文介紹的NIO是操作系統網絡通信中的NON-BLOCKING IO,是Java NIO的基礎。
上圖中的兩個 主線程 循環 指的是沒有開辟子線程。意思是服務端接收客戶端連接和處理客戶端請求都在同一個線程中。
流程:
1 服務端請求客戶端連接,非阻塞。如果沒有直接返回null并向下執行
2 如果有客戶端連接,請求消息響應,非阻塞。如果沒有響應直接返回null并繼續遍歷客戶端
3 遍歷完所有客戶端后,會重新開始循環,跳回第1步
優勢:
1 規避多線程的問題
2 單線程解決多任務
問題:
客戶端循環遍歷時,不斷進行用戶態和內核態的切換,系統調用開銷非常大
什么是用戶態和內核態?
我的理解就是權限不同。用戶態的進程能夠訪問的資源受操作系統的控制,而運行在內核態的進程才可以訪問系統中的硬件設別,例如網卡。所以上面客戶端循環遍歷詢問消息響應,會不斷進行用戶態和內核態的切換。
開銷大在哪?
- 1 保護現場
- 2 恢復現場
- 3 軟中斷
- 4 尋找中斷向量表
- 5 找回調函數
等
多路復用
在NIO的基礎上,通過一次系統調用將連接客戶端響應詢問移動到內核處理,而不是反復進行用戶態和內核態的切換。
如果把每次系統調用理解成一條通路,那么這種把多次系統調用合并成一次的方式,就叫做多路復用。
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//讀,寫或者連接
}
//IO線程主循環:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//選擇就緒的事件和對應的連接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新連接,則注冊一個新的讀寫處理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以寫,則執行寫事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以讀,則執行讀事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的對應事件處理器
}
Selector中的select函數會執行系統內核的調用:Linux 2.6之前是select、poll,2.6之后是epoll。
此流程基于Java示例代碼,代碼可以查看 JPP /NIOEpollTest 類
多路復用的實現方式
select
流程:
1 客戶端建立連接后返回fd
2 用二進制位表bitmap標記fds對應的位置,并將這個bitmap從用戶態拷貝到內核態
3 收到客戶端響應,將對應的bitmap位標記為1
4 程序處理消息,重新創建bitmap,循環下一次
缺點:
1 bitmap默認最大限制1024位
2 bitmap不可重用,每次循環重新創建
3 用戶態和內核態切換開銷
4 輪詢所有的客戶端處理消息 O(n)
poll
流程:
與select相似,select中用bitmap標記fds,在poll中自己聲明了結構體
struct pollfd{
int fd;
short events;
short revents
}
傳輸pollfd數組,解決了select中fds限制1024位的問題
其次,內核將響應客戶端對應的pollfd結構體中的revnets標記為1,說明這個客戶端有消息響應
處理消息時將這個pollfd的revnets重置即可,不用在重新創建數組,所以select中每次循環重新創建bitmap的問題也被解決了
遺留問題:
1 由于fds文件描述符存儲在用戶空間,拷貝到內存空間處理一定會涉及到用戶態和內核態的切換。這個系統調用有一定開銷
2 每次內核態返回的信息是全量信息,要輪詢處理,時間復雜度是 O(n),如果連接數過多,也會有很多無意義的開銷
這兩個問題留給 epoll
epoll
流程:
1 建立epoll對象時在內核分配資源,其數據結構是紅黑樹。添加和檢索的時間復雜度 O(lgn)
int epoll_create(int size);
2 建立連接時,在紅黑樹中存儲 epoll_event結構體,其中包含fd和events等信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3 調用epoll_wait收集響應的連接,放入一個單向鏈表
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
到此為止,上面提到的多路復用的所有缺點都得以解決。