IO多路復用——select,poll,epoll
IO多路復用是一種操作系統技術,旨在提高系統處理多個輸入輸出操作的性能和資源利用率。與傳統的多線程或多進程模型相比,IO多路復用避免了因阻塞IO而導致的資源浪費和低效率問題。它通過將多個IO操作合并到一個系統調用中,允許程序同時等待多個文件描述符(如sockets、文件句柄等)變為可讀或可寫狀態,然后再執行實際的IO操作。
在IO多路復用的實現中,常用的系統調用包括select()
、poll()
和epoll()
。這些機制允許程序監視多個描述符,一旦某個描述符就緒(通常是讀就緒或寫就緒),程序就會被通知進行相應的讀寫操作。這個過程通常涉及兩個階段:
等待數據到達:程序等待數據從IO設備傳輸到內核空間。在這個階段,IO多路復用的系統調用會阻塞,直到至少有一個描述符準備好進行IO操作。 數據復制:當一個或多個描述符就緒時,程序負責將數據從內核空間復制到用戶空間(進程或線程的緩沖區)。這第二個階段是實際的讀寫操作,它在IO多路復用的上下文中是同步的,因為程序需要自己執行數據的讀寫。
盡管select()
、poll()
和epoll()
都是同步IO操作,但它們提供了一種有效的方式來處理并發IO,降低了系統開銷,并提高了并發處理能力。與此不同,異步IO(AIO)模型進一步簡化了IO操作,因為它允許操作系統自動處理數據從內核到用戶空間的復制過程,無需程序顯式調用讀寫操作。這意味著在異步IO模型中,讀寫操作由操作系統在后臺完成,從而進一步提高了應用程序的效率和響應性。
select
概述
系統提供了select函數來實現多路復用輸入/輸出模型 select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的 程序會停在select函數等待,直到被監視的文件描述符有一個或者多個發生了狀態改變。
函數
intselect(intnfds,fd_set*readfds,fd_set*writefds,
fd_set*exceptfds,structtimeval*timeout);
函數參數:
參數 | 說明 |
---|---|
nfds | 是需要監視的最大的文件描述符值+1 |
readfds | 需要檢測的可讀文件描述符的集合 |
writefds | 需要檢測的可寫文件描述符的集合 |
exceptfds | 需要檢測的異常文件描述符的集合 |
timeout | 當timeout等于NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個文件描述符上發生了事件; 當timeout為0:僅檢測描述符集合的狀態,然后立即返回,并不等待外部事件的發生。 當timeout為特定的時間值:如果在指定的時間段里沒有事件發生,select將超時返回。 |
返回 | —— |
> 0 | 返回文件描述詞狀態已改變的個數 |
== 0 | 代表在描述詞狀態改變前已超過timeout時間,沒有返回 |
< 0 | 錯誤原因存于errno,此時參數readfds,writefds, exceptfds和timeout的值變成不可預測,錯誤值可能為: EBADF:文件描述詞為無效的或該文件已關閉 EINTR:此調用被信號所中斷 EINVAL:參數n 為負值 ENOMEM:核心內存不足 |
其中:可讀,可寫,異常文件描述符的集合是一個fd_set類型,fd_set是系統提供的位圖類型,位圖的位置是否是1,表示是否關系該事件。例如:
輸入時:假如我們要關心0123文件描述符
00000000->00001111比特位的位置,表示文件描述符的編號
比特位的內容0or1表示是否需要內核關心
輸出時:
00000100->此時表示文件描述符的編號
比特位的內容0or1哪些用戶關心的fd上面的讀事件已經就緒了,這里表示2描述符就緒了
系統提供了關于fd_set的接口,便于我們使用位圖:
voidFD_CLR(intfd,fd_set*set);//用來清除描述詞組set中相關fd的位
intFD_ISSET(intfd,fd_set*set);//用來測試描述詞組set中相關fd的位是否為真
voidFD_SET(intfd,fd_set*set);//用來設置描述詞組set中相關fd的位
voidFD_ZERO(fd_set*set);//用來清除描述詞組set的全部位
執行流程:
執行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。 若fd=5,執行FD_SET(fd,&set);后set變為0001,0000(第5位置為1) 。 若再加入fd=2,fd=1,則set變為0001,0011 。 執行select(6,&set,0,0,0)阻塞等待,表示最大文件描述符+1是6,監控可讀事件,立即返回。 若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。
優缺點
優點:
可監控的文件描述符個數取決與sizeof(fd_set)的值。一般大小是1024,但是fd_set的大小可以調整。 將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd。①是用于再select 返回后,array作為源數據和fd_set進行FD_ISSET判斷。②是select返回后會把以前加入的但并無事件發生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用于select的第一個參數。
每次調用select, 都需要手動設置fd集合, 從接口使用角度來說也非常不便。 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大。 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大。 select支持的文件描述符數量太小。
實例
#include
#include
#include
#include
#include
#include
conststaticintMAXLINE=1024;
conststaticintSERV_PORT=10001;
intmain()
{
inti,maxi,maxfd,listenfd,connfd,sockfd;
/*nready描述字的數量*/
intnready,client[FD_SETSIZE];
intn;
/*創建描述字集合,由于select函數會把未有事件發生的描述字清零,所以我們設置兩個集合*/
fd_setrset,allset;
charbuf[MAXLINE];
socklen_tclilen;
structsockaddr_incliaddr,servaddr;
/*創建socket*/
listenfd=socket(AF_INET,SOCK_STREAM,0);
/*定義sockaddr_in*/
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,100);
/*listenfd是第一個描述字*/
/*最大的描述字,用于select函數的第一個參數*/
maxfd=listenfd;
/*client的數量,用于輪詢*/
maxi=-1;
/*init*/
for(i=0;iclient[i]=-1;
FD_ZERO(&allset);
FD_SET(listenfd,&allset);
for(;;)
{
rset=allset;
/*只select出用于讀的描述字,阻塞無timeout*/
nready=select(maxfd+1,&rset,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&rset))
{
clilen=sizeof(cliaddr);
connfd=accept(listenfd,(structsockaddr*)&cliaddr,&clilen);
/*尋找第一個能放置新的描述字的位置*/
for(i=0;i{
if(client[i]<0)
{
client[i]=connfd;
break;
}
}
/*找不到,說明client已經滿了*/
if(i==FD_SETSIZE)
{
printf("Toomanyclients,overstack.\n");
return-1;
}
FD_SET(connfd,&allset);//設置fd
/*更新相關參數*/
if(connfd>maxfd)maxfd=connfd;
if(i>maxi)maxi=i;
if(nready<=1)continue;
elsenready--;
}
for(i=0;i<=maxi?;?i++)
{
if(client[i]<0)continue;
sockfd=client[i];
if(FD_ISSET(sockfd,&rset))
{
n=read(sockfd,buf,MAXLINE);
if(n==0)
{
/*當對方關閉的時候,server關閉描述字,并將set的sockfd清空*/
close(sockfd);
FD_CLR(sockfd,&allset);
client[i]=-1;
}
else
{
buf[n]='\0';
printf("Socket%dsaid:%s\n",sockfd,buf);
write(sockfd,buf,n);//Writebacktoclient
}
nready--;
if(nready<=0)break;
}
}
}
return0;
}
poll
概述
poll和select實現原理基本類似 poll只為了解決select的兩個硬傷:①等待的fd是有上限的,(底層類似鏈表儲存實現,而不是位圖)。②每次要對關心的fd進行事件重置,(pollfd結構包含了要監視的event和發生的event,使用前后不用初始化fd_set)。
函數
intpoll(structpollfd*fds,nfds_tnfds,inttimeout);
//pollfd結構
structpollfd{
intfd;/*filedescriptor*/
shortevents;/*requestedevents*/
shortrevents;/*returnedevents*/
};
函數參數:
參數 | 說明 |
---|---|
fds | 是一個poll函數監聽的結構列表. 每一個元素中, 包含了三部分內容: 文件描述符, 監聽的事件集合, 返回的事件集合 |
nfds | 表示fds數組的長度 |
timeout | 表示poll函數的超時時間, 單位是毫秒(ms) |
返回 | —— |
> 0 | 表示poll由于監聽的文件描述符就緒而返回 |
== 0 | 表示poll函數等待超時 |
< 0 | 表示出錯 |
優缺點
優點:
pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式. 接口使用比 select更方便。 poll并沒有最大數量限制 (但是數量過大后性能也是會下降)。
和select函數一樣,poll返回后,需要輪詢pollfd來獲取就緒的描述符。 每次調用poll都需要把大量的pollfd結構從用戶態拷貝到內核中。 同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態, 因此隨著監視的描述符數量的增長, 其效率也會線性下降。
實例
#include
#include
#include
#include
#include
#include
#defineMAXLINE1024
#defineOPEN_MAX16//一些系統會定義這些宏
#defineSERV_PORT10001
intmain()
{
inti,maxi,listenfd,connfd,sockfd;
intnready;
intn;
charbuf[MAXLINE];
socklen_tclilen;
structpollfdclient[OPEN_MAX];
structsockaddr_incliaddr,servaddr;
listenfd=socket(AF_INET,SOCK_STREAM,0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,10);
client[0].fd=listenfd;
client[0].events=POLLRDNORM;
for(i=1;i{
client[i].fd=-1;
}
maxi=0;
for(;;)
{
nready=poll(client,maxi+1,INFTIM);
if(client[0].revents&POLLRDNORM)
{
clilen=sizeof(cliaddr);
connfd=accept(listenfd,(structsockaddr*)&cliaddr,&clilen);
for(i=1;i{
if(client[i].fd<0)
{
client[i].fd=connfd;
client[i].events=POLLRDNORM;
break;
}
}
if(i==OPEN_MAX)
{
printf("toomanyclients!\n");
}
if(i>maxi)maxi=i;
nready--;
if(nready<=0)continue;
}
for(i=1;i<=maxi;i++)
{
if(client[i].fd<0)continue;
sockfd=client[i].fd;
if(client[i].revents&(POLLRDNORM|POLLERR))
{
n=read(client[i].fd,buf,MAXLINE);
if(n<=0)
{
close(client[i].fd);
client[i].fd=-1;
}
else
{
buf[n]='\0';
printf("Socket%dsaid:%s\n",sockfd,buf);
write(sockfd,buf,n);//Writebacktoclient
}
nready--;
if(nready<=0)break;//nomorereadabledescriptors
}
}
}
return0;
}
epoll
概述
epoll:是為處理大批量句柄而作了改進的poll(真的是大改進) epoll是IO多路復用技術,在實現上維護了一個用于返回觸發事件的Socket的鏈表和一個記錄監聽事件的紅黑樹,epoll的高效體現在:
對監聽事件的修改是 logN(紅黑樹)。 用戶程序無需遍歷所有的Socket(發生事件的Socket被放到鏈表中直接返回)。 內核無需遍歷所有的套接字,內核使用回調函數在事件發生時直接轉到對應的處理函數。
函數
epoll_create:創建一個epoll的句柄,用完之后, 必須調用close()關閉。
intepoll_create(intsize);
epoll_ctl:它不同于select()是在監聽事件時告訴內核要監聽什么類型的事件, 而是在這里先注冊要監聽的事件類型。
intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);
typedefunionepoll_data
{
void*ptr;
intfd;
uint32_tu32;
uint64_tu64;
}epoll_data_t;
structepoll_event
{
uint32_tevents;
epoll_data_tdata;
}EPOLL_PACKED;
events參數的宏集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)。
EPOLLOUT :表示對應的文件描述符可以寫。
EPOLLPRI :表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來)。
EPOLLERR :表示對應的文件描述符發生錯誤。
EPOLLHUP :表示對應的文件描述符被掛斷。
EPOLLET :將EPOLL設為邊緣觸發(Edge Triggered)模式, 這是相對于水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件, 當監聽完這次事件之后, 如果還需要繼續監聽這個socket的話, 需要再次把這個socket加入到EPOLL隊列里
函數參數:
參數 | 說明 |
---|---|
epfd | epoll_create()的返回值(epoll的句柄) |
op | 表示動作,用三個宏來表示: EPOLL_CTL_ADD :注冊新的fd到epfd中 EPOLL_CTL_MOD :修改已經注冊的fd的監聽事件 EPOLL_CTL_DEL :從epfd中刪除一個fd |
fd | 需要監聽的fd |
event | 內核需要監聽的事件 |
epoll_wait:收集在epoll監控的事件中已經發送的事件
intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);
參數 | 說明 |
---|---|
epfd | epoll_create()的返回值(epoll的句柄) |
events | 是分配好的epoll_event結構體數組。epoll將會把發生的事件賦值到events數組中 (events不可以是空指針,內核只負責把數據復制到這個events數組中,不會去幫助我們在用戶態中分配內存) |
maxevents | 通知內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size |
timeout | 超時時間 (毫秒,0會立即返回,-1是永久阻塞) |
返回 | —— |
> 0 | 返回對應I/O上已準備好的文件描述符數目 |
== 0 | 表示已超時 |
< 0 | 表示失敗 |
執行流程:
當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。 每一個epoll對象都有一個獨立的eventpoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加進來的事件。 這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。 而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當響應的事件發生時會調用這個回調方法。 這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。 在epoll中,對于每一個事件,都會建立一個epitem結構體。 當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。 如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶. 這個操作的時間復雜度是O(1)。
優缺點
優點:
接口使用方便: 雖然拆分成了三個函數,但是反而使用起來更方便高效,不需要每次循環都設置關注的文件描述符,也做到了輸入輸出參數分離開。 數據拷貝輕量: 只在合適的時候調用 EPOLL_CTL_ADD 將文件描述符結構拷貝到內核中,這個操作并不頻繁(而select/poll都是每次循環都要進行拷貝)。 事件回調機制: 避免使用遍歷,而是使用回調函數的方式,將就緒的文件描述符結構加入到就緒隊列中,epoll_wait 返回直接訪問就緒隊列就知道哪些文件描述符就緒,這個操作時間復雜度O(1),即使文件描述符數目很多,效率也不會受到影響。 沒有數量限制: 文件描述符數目無上限。
不能跨平臺,epoll 是 Linux 特有的 API,不太容易移植到其他操作系統上
實例
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#defineMAXLINE1024
#defineOPEN_MAX16//一些系統會定義這些宏
#defineSERV_PORT10001
intmain()
{
inti,maxi,listenfd,connfd,sockfd,epfd,nfds;
intn;
charbuf[MAXLINE];
structepoll_eventev,events[20];
socklen_tclilen;
structpollfdclient[OPEN_MAX];
structsockaddr_incliaddr,servaddr;
listenfd=socket(AF_INET,SOCK_STREAM,0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,10);
epfd=epoll_create(256);
ev.data.fd=listenfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
for(;;)
{
nfds=epoll_wait(epfd,events,20,500);
for(i=0;i{
if(listenfd==events[i].data.fd)
{
clilen=sizeof(cliaddr);
connfd=accept(listenfd,(structsockaddr*)&cliaddr,&clilen);
if(connfd0)
{
perror("connfd0");
exit(1);
}
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
elseif(events[i].events&EPOLLIN)
{
if((sockfd=events[i].data.fd)0)
continue;
n=recv(sockfd,buf,MAXLINE,0);
if(n<=?0)
{
close(sockfd);
events[i].data.fd=-1;
}
else
{
buf[n]='\0';
printf("Socket%dsaid:%s\n",sockfd,buf);
ev.data.fd=sockfd;
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,connfd,&ev);
}
}
elseif(events[i].events&EPOLLOUT)
{
sockfd=events[i].data.fd;
send(sockfd,"Hello!",7,0);
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else
{
printf("Thisisnotavaible!");
}
}
}
close(epfd);
return0;
}
總結
select
和poll
是兩種傳統的 I/O 多路復用技術,它們允許服務器應用程序同時監控多個網絡連接,以便在連接準備就緒時進行讀寫操作。盡管這兩種技術在處理大量并發連接時非常有用,但隨著連接數的增加,它們的性能會逐漸下降,因為它們需要在每次調用時遍歷整個文件描述符集合,這在連接數非常多時會導致效率問題。為了解決這個問題,
epoll
作為select
和poll
的一種改進方案,在 Linux 系統中被引入。epoll
提供了一種更為高效的事件驅動模型,它可以顯著提高處理大量并發連接的性能。與select
和poll
不同,epoll
不會對整個文件描述符集合進行線性遍歷,而是使用一組特殊的數據結構來跟蹤哪些文件描述符已經準備好 I/O 操作。這種機制使得epoll
能夠快速地通知應用程序哪些連接是活躍的,而無需對所有連接進行不必要的檢查。epoll
的另一個優點是它能夠處理大量文件描述符而不會顯著增加資源消耗,這使得它非常適合需要處理成千上萬甚至更多并發連接的高性能網絡服務器。因此,在 Linux 系統上,epoll
常被視為select
和poll
的替代方案,特別是在構建高性能網絡應用程序時。
- END -
往期推薦:點擊圖片即可跳轉閱讀
《YY3568 Debian11+RT-Thread混合內核部署》
《YY3568多核異構(Linux+RT-Thread)--啟動流程》
原文標題:Linux--IO多路復用(select,poll,epoll)
文章出處:【微信公眾號:Rice 嵌入式開發技術分享】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
什么是多路復用器?它有哪些作用和應用?
頻分多路復用和時分多路復用的區別有哪些
多路復用技術主要有幾種類型?它們各有什么特點?
頻分多路復用的原理 頻分多路復用方式的分類
![頻分<b class='flag-5'>多路復用</b>的原理 頻分<b class='flag-5'>多路復用</b>方式的分類](https://file1.elecfans.com/web2/M00/C3/98/wKgaomXmtwGAbECoAAGsM23fXbE656.jpg)
評論