1. 程式人生 > 實用技巧 >面試中的 IO 多路複用

面試中的 IO 多路複用

轉載:https://mp.weixin.qq.com/s/iVfLZJ89UMtu3Z5IgpoCoQ

1、什麼是IO多路複用

「定義」

  • IO多路複用是一種同步IO模型,實現一個執行緒可以監視多個檔案控制代碼;一旦某個檔案控制代碼就緒,就能夠通知應用程式進行相應的讀寫操作;沒有檔案控制代碼就緒時會阻塞應用程式,交出cpu。多路是指網路連線,複用指的是同一個執行緒

2、為什麼有IO多路複用機制?

沒有IO多路複用機制時,有BIO、NIO兩種實現方式,但有一些問題

同步阻塞(BIO)

  • 服務端採用單執行緒,當accept一個請求後,在recv或send呼叫阻塞時,將無法accept其他請求(必須等上一個請求處recv或send完),無法處理併發
//虛擬碼描述
while(1){
//accept阻塞
client_fd=accept(listen_fd)
fds.append(client_fd)
for(fdinfds){
//recv阻塞(會影響上面的accept)
if(recv(fd)){
//logic
}
}
}
  • 伺服器端採用多執行緒,當accept一個請求後,開啟執行緒進行recv,可以完成併發處理,但隨著請求數增加需要增加系統執行緒,大量的執行緒佔用很大的記憶體空間,並且執行緒切換會帶來很大的開銷,10000個執行緒真正發生讀寫事件的執行緒數不會超過20%,每次accept都開一個執行緒也是一種資源浪費
//虛擬碼描述
while(1){
//accept阻塞
client_fd=accept(listen_fd)
//開啟執行緒read資料(fd增多導致執行緒數增多)
newThreadfunc(){
//recv阻塞(多執行緒不影響上面的accept)
if(recv(fd)){
//logic
}
}
}

同步非阻塞(NIO)

  • 伺服器端當accept一個請求後,加入fds集合,每次輪詢一遍fds集合recv(非阻塞)資料,沒有資料則立即返回錯誤,每次輪詢所有fd(包括沒有發生讀寫事件的fd)會很浪費cpu
setNonblocking(listen_fd)
//虛擬碼描述
while(1){
//accept非阻塞(cpu一直忙輪詢)
client_fd=accept(listen_fd)
if(client_fd!=null){
//有人連線
fds.append(client_fd)
}else{
//無人連線
}
for(fdinfds){
//recv非阻塞
setNonblocking(client_fd)
//recv為非阻塞命令
if(len=recv(fd)&&len>0){
//有讀寫資料
//logic
}else{
無讀寫資料
}
}
}

IO多路複用(現在的做法)

  • 伺服器端採用單執行緒通過select/epoll等系統呼叫獲取fd列表,遍歷有事件的fd進行accept/recv/send,使其能支援更多的併發連線請求
fds=[listen_fd]
//虛擬碼描述
while(1){
//通過核心獲取有讀寫事件發生的fd,只要有一個則返回,無則阻塞
//整個過程只在呼叫select、poll、epoll這些呼叫的時候才會阻塞,accept/recv是不會阻塞
for(fdinselect(fds)){
if(fd==listen_fd){
client_fd=accept(listen_fd)
fds.append(client_fd)
}elseif(len=recv(fd)&&len!=-1){
//logic
}
}
}

3、IO多路複用的三種實現方式

  • select
  • poll
  • epoll

4、select函式介面

#include<sys/select.h>
#include<sys/time.h>

#defineFD_SETSIZE1024
#defineNFDBITS(8*sizeof(unsignedlong))
#define__FDSET_LONGS(FD_SETSIZE/NFDBITS)

//資料結構(bitmap)
typedefstruct{
unsignedlongfds_bits[__FDSET_LONGS];
}fd_set;

//API
intselect(
intmax_fd,
fd_set*readset,
fd_set*writeset,
fd_set*exceptset,
structtimeval*timeout
)//返回值就緒描述符的數目

FD_ZERO(intfd,fd_set*fds)//清空集合
FD_SET(intfd,fd_set*fds)//將給定的描述符加入集合
FD_ISSET(intfd,fd_set*fds)//判斷指定描述符是否在集合中
FD_CLR(intfd,fd_set*fds)//將給定的描述符從檔案中刪除

5、select使用示例

intmain(){
/*
*這裡進行一些初始化的設定,
*包括socket建立,地址的設定等,
*/

fd_setread_fs,write_fs;
structtimevaltimeout;
intmax=0;//用於記錄最大的fd,在輪詢中時刻更新即可

//初始化位元位
FD_ZERO(&read_fs);
FD_ZERO(&write_fs);

intnfds=0;//記錄就緒的事件,可以減少遍歷的次數
while(1){
//阻塞獲取
//每次需要把fd從使用者態拷貝到核心態
nfds=select(max+1,&read_fd,&write_fd,NULL,&timeout);
//每次需要遍歷所有fd,判斷有無讀寫事件發生
for(inti=0;i<=max&&nfds;++i){
if(i==listenfd){
--nfds;
//這裡處理accept事件
FD_SET(i,&read_fd);//將客戶端socket加入到集合中
}
if(FD_ISSET(i,&read_fd)){
--nfds;
//這裡處理read事件
}
if(FD_ISSET(i,&write_fd)){
--nfds;
//這裡處理write事件
}
}
}

6、select缺點

  • 單個程序所開啟的FD是有限制的,通過FD_SETSIZE設定,預設1024
  • 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
  • 對socket掃描時是線性掃描,採用輪詢的方法,效率較低(高併發時)

7、poll函式介面

poll與select相比,只是沒有fd的限制,其它基本一樣

#include<poll.h>
//資料結構
structpollfd{
intfd;//需要監視的檔案描述符
shortevents;//需要核心監視的事件
shortrevents;//實際發生的事件
};

//API
intpoll(structpollfdfds[],nfds_tnfds,inttimeout);

8、poll使用示例

//先巨集定義長度
#defineMAX_POLLFD_LEN4096

intmain(){
/*
*在這裡進行一些初始化的操作,
*比如初始化資料和socket等。
*/

intnfds=0;
pollfdfds[MAX_POLLFD_LEN];
memset(fds,0,sizeof(fds));
fds[0].fd=listenfd;
fds[0].events=POLLRDNORM;
intmax=0;//佇列的實際長度,是一個隨時更新的,也可以自定義其他的
inttimeout=0;

intcurrent_size=max;
while(1){
//阻塞獲取
//每次需要把fd從使用者態拷貝到核心態
nfds=poll(fds,max+1,timeout);
if(fds[0].revents&POLLRDNORM){
//這裡處理accept事件
connfd=accept(listenfd);
//將新的描述符新增到讀描述符集合中
}
//每次需要遍歷所有fd,判斷有無讀寫事件發生
for(inti=1;i<max;++i){
if(fds[i].revents&POLLRDNORM){
sockfd=fds[i].fd
if((n=read(sockfd,buf,MAXLINE))<=0){
//這裡處理read事件
if(n==0){
close(sockfd);
fds[i].fd=-1;
}
}else{
//這裡處理write事件
}
if(--nfds<=0){
break;
}
}
}
}

9、poll缺點

  • 每次呼叫poll,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
  • 對socket掃描時是線性掃描,採用輪詢的方法,效率較低(高併發時)

10、epoll函式介面

#include<sys/epoll.h>

//資料結構
//每一個epoll物件都有一個獨立的eventpoll結構體
//用於存放通過epoll_ctl方法向epoll物件中新增進來的事件
//epoll_wait檢查是否有事件發生時,只需要檢查eventpoll物件中的rdlist雙鏈表中是否有epitem元素即可
structeventpoll{
/*紅黑樹的根節點,這顆樹中儲存著所有新增到epoll中的需要監控的事件*/
structrb_rootrbr;
/*雙鏈表中則存放著將要通過epoll_wait返回給使用者的滿足條件的事件*/
structlist_headrdlist;
};

//API

intepoll_create(intsize);//核心中間加一個ep物件,把所有需要監聽的socket都放到ep物件中
intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);//epoll_ctl負責把socket增加、刪除到核心紅黑樹
intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);//epoll_wait負責檢測可讀佇列,沒有可讀socket則阻塞程序

11、epoll使用示例

intmain(intargc,char*argv[])
{
/*
*在這裡進行一些初始化的操作,
*比如初始化資料和socket等。
*/

//核心中建立ep物件
epfd=epoll_create(256);
//需要監聽的socket放到ep中
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

while(1){
//阻塞獲取
nfds=epoll_wait(epfd,events,20,0);
for(i=0;i<nfds;++i){
if(events[i].data.fd==listenfd){
//這裡處理accept事件
connfd=accept(listenfd);
//接收新連線寫到核心物件中
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}elseif(events[i].events&EPOLLIN){
//這裡處理read事件
read(sockfd,BUF,MAXLINE);
//讀完後準備寫
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}elseif(events[i].events&EPOLLOUT){
//這裡處理write事件
write(sockfd,BUF,n);
//寫完後準備讀
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return0;
}

12、epoll缺點

  • epoll只能工作在linux下

13、epoll LT 與 ET模式的區別

  • epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是預設的模式,ET是“高速”模式。
  • LT模式下,只要這個fd還有資料可讀,每次 epoll_wait都會返回它的事件,提醒使用者程式去操作
  • ET模式下,它只會提示一次,直到下次再有資料流入之前都不會再提示了,無論fd中是否還有資料可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀完,或者遇到EAGAIN錯誤

14、epoll應用

  • redis
  • nginx

15、select/poll/epoll之間的區別

selectpollepoll
資料結構 bitmap 陣列 紅黑樹
最大連線數 1024 無上限 無上限
fd拷貝 每次呼叫select拷貝 每次呼叫poll拷貝 fd首次呼叫epoll_ctl拷貝,每次呼叫epoll_wait不拷貝
工作效率 輪詢:O(n) 輪詢:O(n) 回撥:O(1)

16、完整程式碼示例

https://github.com/caijinlin/learning-pratice/tree/master/linux/io

17、高頻面試題

  • 什麼是IO多路複用?
  • nginx/redis 所使用的IO模型是什麼?
  • select、poll、epoll之間的區別
  • epoll 水平觸發(LT)與 邊緣觸發(ET)的區別?