• 正文
    • 1. poll詳解
    • 2. epoll詳解
  • 相關推薦
申請入駐 產業(yè)圖譜

IO多路轉接技術 | poll/epoll詳解

05/06 10:04 來源:mindtechnist
1208
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

1. poll詳解

函數原型

intpoll(struct?pollfd *fd,?nfds_t?nfds,?int?timeout);

函數參數

fd:數組的地址,struct pollfd all[120]; 其中struct pollfd結構體如下

structpollfd {
int? ?fd; ? ? ? ??/* 文件描述符 */
short?events; ? ??/* 等待的事件 */
short?revents; ? ?/* 實際發(fā)生的事件 */
? ? };

結構體紅各項含義如下:

文件描述符fd:表示要堅持測的fd,通過 open("a.txt", O_wronly | O_append); 獲得。

events:要等待的事件

revents:實際發(fā)生的事件,它是內核給的反饋,在select的時候,會有一個備份來供內核修改并傳出。

nfds:數組的最大長度, 數組中最后一個使用的元素下標+1

內核會輪詢檢測fd數組的每個文件描述符

timeout:

1:永久阻塞

0:調用完成立即返回

>0:等待的時長毫秒

函數返回值:IO發(fā)生變化的文件描述符的個數。

2. epoll詳解

(1)API介紹

intepoll_create(int?size);

函數功能:生成一個epoll專用的文件描述符,實際上就是生成一個epoll樹的根結點。

函數參數:size,epoll樹上能掛的最大文件描述符數量。表示我想在這個樹節(jié)點上掛size個節(jié)點,假如實際上的節(jié)點大于size的話epoll會自動擴展,所以這個大小可以隨便傳,不用太在意。但是這個擴展也是有上限的,如果電腦內存是1G,那么擴展的上限是10萬(2G就是20萬。。。通過加內存可以擴大上限)。

函數返回值:函數返回值是樹的根節(jié)點,在后面用到epft參數的時候,都是指這個返回值,也就是樹的根節(jié)點。

intepoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event *event);

函數功能:用于控制某個epoll文件描述符事件,可以注冊、修改、刪除。

函數參數:

epfd:epoll_create()函數生成的專用文件描述符。

op:

EPOLL_CTL_ADD ? ? ? —— ?注冊

EPOLL_CTL_MOD ? ? ?—— ?修改

EPOLL_CTL_DEL ? ? ? ?—— ?刪除

fd:關聯(lián)的文件描述符

event:告訴內核要監(jiān)聽什么事件

EPOLLIN ? ? —— 讀

EPOLLOUT —— 寫

EPOLLERR ?—— 異常

structepoll_event {/* 該結構體主要存放和fd有關的信息 */uint32_t? ? ?events; ? ? ? ? ? ? ? ? ? ? ? ? ?epoll_data_t?data;?? ? ? };
typedefunion epoll_data {void? ? ? ? ?*ptr;int? ? ? ? ? fd;uint32_t? ? ?u32;uint64_t? ? ?u64;? ? ? }?epoll_data_t;

epoll_data_t是一個聯(lián)合體union,四個成員共用同一塊內存,也就是說四個成員我們只能用一個,一般情況下我們用fd,這個fd實際上就是epoll_ctl()函數的第三個參數fd。

如果我們想在epoll樹上掛載更多信息,而不僅僅是fd文件描述符的話,我們可以把更多信息封裝在結構體中,并把該結構體傳給epoll_data_t結構體的ptr指針,這樣就可以在epoll樹上掛載和fd有關的更多信息。

structsockInfo? ? ? ? {int? ? ? ? ?fd;structsockaddr_inaddr;? ? ? ? };

比如說,要獲取發(fā)生變化的fd對應的client的IP和port,就可以利用指針ptr,這樣的話聯(lián)合epoll_data_t中的fd就不能用了,我們把文件描述符傳給sockInfo的fd即可完成fd信息的掛載。

intepoll_wait(int?epfd,struct?epoll_event* events, ?/* 結構體數組 */int?maxevents,int?timeout);

函數功能:等待IO事件發(fā)生(可以設置阻塞),epoll_wait()函數相當于前面講的select()或poll()函數,表示委托內核去進行檢測。epoll_event通過返回值和傳出參數events來實現(xiàn)把哪幾個fd發(fā)生變化告訴server進程的目的。首先,每當有fd變化,就把這個fd對應的樹節(jié)點拷貝到events數組中,最后,有幾個fd變化,就返回幾。這樣只要根據返回值和參數events就可以遍歷出所有變化的fd以及相關信息。

函數參數:

epfd:要檢測的句柄

events:用于回傳待處理事件的數組。它是一個傳出參數,需要提前分配內存,哪個fd發(fā)生變化了,就把哪個fd的樹節(jié)點(struct epoll_event)拷貝一份放到這個數組中。這樣epoll就能返回是哪個fd發(fā)生了變化。

maxevents:告訴內核events的大小,因為內核要把發(fā)生變化的fd對應的樹節(jié)點拷貝到數組中,所以要知道數組大小。

timeout:為超時時間

1:永久阻塞

0:立即返回

>0

函數返回值:有多少個fd發(fā)生了變化就返回幾(變化的fd信息存在events數組中)。

(2)epoll樹

(3)epoll模型

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<sys/socket.h>#include<arpa/inet.h>#include<ctype.h>#include<sys/epoll.h>
intmain(int?argc, constchar* argv[]){if(argc <?2)? ? {printf("eg: ./a.out portn");exit(1);? ? }structsockaddr_inserv_addr;socklen_t?serv_len =?sizeof(serv_addr);int?port =?atoi(argv[1]);?//字符串轉整形值
// 創(chuàng)建套接字int?lfd =?socket(AF_INET, SOCK_STREAM,?0);// 初始化服務器 sockaddr_in?memset(&serv_addr,?0, serv_len);? ? serv_addr.sin_family = AF_INET;?// 地址族?? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY);?// 監(jiān)聽本機所有的IP? ? serv_addr.sin_port =?htons(port);?// 設置端口?// 綁定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設置同時監(jiān)聽的最大個數? ??listen(lfd,?36);printf("Start accept ......n");
structsockaddr_inclient_addr;socklen_t?cli_len =?sizeof(client_addr);
// 創(chuàng)建epoll樹根節(jié)點int?epfd =?epoll_create(2000);// 初始化epoll樹structepoll_eventev;? ? ev.events = EPOLLIN;? ? ev.data.fd = lfd;? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);//存放發(fā)生變化的fd對應的樹節(jié)點structepoll_eventall[2000];while(1)? ? {// 使用epoll通知內核fd 文件IO檢測int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);
// 遍歷all數組中的前ret個元素 //ret表示有幾個變化的fd,變化的fd都存在all數組中for(int?i=0; i<ret; ++i)? ? ? ? {int?fd = all[i].data.fd;// 判斷是否有新連接if(fd == lfd)? ? ? ? ? ? {// 接受連接請求 // accept不阻塞,因為已經有連接int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);if(cfd ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("accept error");exit(1);? ? ? ? ? ? ? ? }// 將新得到的cfd掛到樹上structepoll_eventtemp;? ? ? ? ? ? ? ? temp.events = EPOLLIN;?//檢測cfd對應的讀緩沖區(qū),是否有數據傳入? ? ? ? ? ? ? ? temp.data.fd = cfd;? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客戶端信息char?ip[64] = {0};printf("New Client IP: %s, Port: %dn",? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),? ? ? ? ? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }else? ? ? ? ? ? {// 處理已經連接的客戶端發(fā)送過來的數據if(!all[i].events & EPOLLIN)?//只處理讀事件? ? ? ? ? ? ? ? {continue;? ? ? ? ? ? ? ? }/*? ? ? ? ? ? ? ? 假如說client發(fā)送過了100個數據,也就是serve的read緩沖區(qū)有100個數據,? ? ? ? ? ? ? ? 但是調用recv函數的時候只能讀50個數據,而本次循環(huán)只調用了一次recv,? ? ? ? ? ? ? ? 那么只能下次循環(huán)再讀剩余的50個數據,所以下次循環(huán)檢測的時候,? ? ? ? ? ? ? ? epoll_wait還是會返回,因為緩沖區(qū)還是剩余數據。這就是水平觸發(fā)模式。? ? ? ? ? ? ? ? 這樣的話雖然client只發(fā)了1次,但是epoll_wait會通知兩次server去讀數據。? ? ? ? */
// 讀數據char?buf[1024] = {0};int?len =?recv(fd, buf,?sizeof(buf),?0);if(len ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("recv error");exit(1);? ? ? ? ? ? ? ? }elseif(len ==?0)? ? ? ? ? ? ? ? {printf("client disconnected ....n");//close(fd);// fd從epoll樹上刪除? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);// 掛樹的時候需要ev,把ev掛在樹上刪除寫NULL就行了if(ret ==?-1)? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("epoll_ctl del error");exit(1);? ? ? ? ? ? ? }? ? ? ? ? ? ? ?close(fd);? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? {printf(" recv buf: %sn", buf);? ? ? ? ? ? ? ? ? ??write(fd, buf, len);? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

epoll維護的紅黑樹是存在一個共享內存中,內核和用戶都可以通過操作這個共享內存來操作樹,不需要內核態(tài)和用戶態(tài)的切換,也不需要兩種狀態(tài)之間的數據拷貝,所以效率更高。

(4)epoll的三種工作模式

水平觸發(fā)模式 ? - (根據讀來解釋)

只要fd對應的緩沖區(qū)有數據,epoll_wait就會返回

返回的次數與發(fā)送數據的次數沒有關系

epoll默認的工作模式

邊沿觸發(fā)模式 - ET

fd - 默認阻塞屬性

客戶端給server發(fā)數據:

發(fā)一次數據server 的 epoll_wait就返回一次

不在乎數據是否讀完

如果讀不完,如何把數據全部讀出來?

while(recv());

數據讀完之后recv會阻塞

解決阻塞問題 —— 設置非阻塞fd

對于epoll_wait()來說,epoll_wait 調用次數越多, 系統(tǒng)的開銷越大。

水平觸發(fā)模式會多次返回,只要server的read緩沖區(qū)有數據,epoll_wait就返回,也就會通知server去讀數據,那么在循環(huán)檢測的時候,只要server的read緩沖區(qū)有數據,epoll_wait就會多次調用,多次返回,并通知server去讀數據;假如說client發(fā)送過了100個數據,也就是serve的read緩沖區(qū)有100個數據,但是調用recv函數的時候只能讀50個數據,而本次循環(huán)只調用了一次recv,那么只能下次循環(huán)再讀剩余的50個數據,所以下次循環(huán)檢測的時候,epoll_wait還是會返回,因為緩沖區(qū)還是剩余數據。這就是水平觸發(fā)模式。這樣的話雖然client只發(fā)了1次,但是epoll_wait會通知兩次server去讀數據。

—— (printf函數是標準C庫函數,C庫函數都有一個默認緩沖區(qū),printf的大小是8K。printf函數是行緩沖,使用printf函數的時候,如果不加 n 會默認等到寫滿的時候才打印內容,加 n 會強制把緩沖區(qū)的內容打印出來。另外 表示結束,不加 就會一直輸出直到遇到 ,用write(STDOUT_FILENO)替代printf函數就可以解決這些問題。)

邊沿觸發(fā)模式,client發(fā)一次數據epoll_wait只返回一次,也就只讀一次,這樣的話server的read緩沖區(qū)可能會有很多數據堆積,server讀數據的時候可能讀到的是上一次剩余的數據,并且只有client發(fā)的時候,epoll_wait才會通知server去讀數據,邊沿觸發(fā)模式盡可能減少了epoll_wait的調用次數,缺點是數據有可能讀不完導致堆積;

邊沿非阻塞觸發(fā)

效率最高

如何設置非阻塞

open()

設置flags

必須 O_WDRW | O_NONBLOCK

終端文件: /dev/tty

 

fcntl

int flag = fcntl(fd, F_GETFL);

flag |= O_NONBLOCK;

fcntl(fd, F_SETFL, flag);

如何將緩沖區(qū)的全部數據都讀出?

while(recv() >?0)? ? ?{? ? ??printf();? ? ?}

當緩沖區(qū)數據讀完之后, 返回值是否為0?

阻塞狀態(tài)

數據讀完之后,recv阻塞

非阻塞狀態(tài)

強行讀了一個沒有數據的緩沖區(qū)(fd),數據已經被讀完了,因為是非阻塞,所以在while循環(huán)中recv還要繼續(xù)讀,導致返回-1

判斷 errno == EAGAIN

示例

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<sys/socket.h>#include<arpa/inet.h>#include<ctype.h>#include<sys/epoll.h>#include<fcntl.h>#include<errno.h>
intmain(int?argc, constchar* argv[]){if(argc <?2)? ? {printf("eg: ./a.out portn");exit(1);? ? }structsockaddr_inserv_addr;socklen_t?serv_len =?sizeof(serv_addr);int?port =?atoi(argv[1]);
// 創(chuàng)建套接字int?lfd =?socket(AF_INET, SOCK_STREAM,?0);// 初始化服務器 sockaddr_in?memset(&serv_addr,?0, serv_len);? ? serv_addr.sin_family = AF_INET; ? ? ? ? ? ? ? ? ??// 地址族?? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY); ? ?// 監(jiān)聽本機所有的IP? ? serv_addr.sin_port =?htons(port); ? ? ? ? ? ?// 設置端口?// 綁定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設置同時監(jiān)聽的最大個數? ??listen(lfd,?36);printf("Start accept ......n");
structsockaddr_inclient_addr;socklen_t?cli_len =?sizeof(client_addr);
// 創(chuàng)建epoll樹根節(jié)點int?epfd =?epoll_create(2000);// 初始化epoll樹structepoll_eventev;
// 設置邊沿觸發(fā)? ? ev.events = EPOLLIN;?//監(jiān)聽的文件描述符沒必要邊沿觸發(fā),主要是通信的cfd? ? ev.data.fd = lfd;? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
structepoll_eventall[2000];while(1)? ? {// 使用epoll通知內核fd 文件IO檢測int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);printf("================== epoll_wait =============n");
// 遍歷all數組中的前ret個元素for(int?i=0; i<ret; ++i)? ? ? ? {int?fd = all[i].data.fd;// 判斷是否有新連接if(fd == lfd)? ? ? ? ? ? {// 接受連接請求int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);if(cfd ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("accept error");exit(1);? ? ? ? ? ? ? ? }// 設置文件cfd為非阻塞模式int?flag =?fcntl(cfd, F_GETFL);? ? ? ? ? ? ? ? flag |= O_NONBLOCK;? ? ? ? ? ? ? ??fcntl(cfd, F_SETFL, flag);
// 將新得到的cfd掛到樹上structepoll_eventtemp;// 設置邊沿觸發(fā)? ? ? ? ? ? ? ? temp.events = EPOLLIN | EPOLLET;? ? ? ? ? ? ? ? temp.data.fd = cfd;? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客戶端信息char?ip[64] = {0};printf("New Client IP: %s, Port: %dn",? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }else? ? ? ? ? ? {// 處理已經連接的客戶端發(fā)送過來的數據if(!all[i].events & EPOLLIN)?? ? ? ? ? ? ? ? {continue;? ? ? ? ? ? ? ? }
// 讀數據char?buf[5] = {0};int?len;// 循環(huán)讀數據while( (len =?recv(fd, buf,?sizeof(buf),?0)) >?0?)? ? ? ? ? ? ? ? {// 數據打印到終端//不要用printf,因為printf如果找不到  n 字符會出現(xiàn)亂碼,打印不出來等問題? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);// 發(fā)送給客戶端? ? ? ? ? ? ? ? ? ??send(fd, buf, len,?0);? ? ? ? ? ? ? ? }if(len ==?0)? ? ? ? ? ? ? ? {printf("客戶端斷開了連接n");? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);if(ret ==?-1)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ??close(fd);? ? ? ? ? ? ? ? }elseif(len ==?-1)? ? ? ? ? ? ? ? {//數據已經被讀完了,因為是非阻塞,所以在while循環(huán)中recv還要繼續(xù)讀,導致返回-1if(errno == EAGAIN)? ? ? ? ? ? ? ? ? ? {printf("緩沖區(qū)數據已經讀完n");? ? ? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? ? ? {//這才是真正的recv錯誤printf("recv error----n");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }#if?0if(len ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("recv error");exit(1);? ? ? ? ? ? ? ? }elseif(len ==?0)? ? ? ? ? ? ? ? {printf("client disconnected ....n");// fd從epoll樹上刪除? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);if(ret ==?-1)? ? ? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");exit(1);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ??close(fd);
? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? {// printf(" recv buf: %sn", buf);? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);? ? ? ? ? ? ? ? ? ??write(fd, buf, len);? ? ? ? ? ? ? ? }#endif? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

5)文件描述符1024限制

對于select來說,無法突破文件描述符1024上限,因為select是通過數組實現(xiàn)的。poll和epoll可以突破1024限制,poll是內部鏈表實現(xiàn),而epoll是紅黑樹實現(xiàn)。

查看受計算機硬件限制的文件描述符上限可以通過下面命令

cat?/proc/sys/fs/file-max

同樣,我們也可以通過修改配置文件來修改這個上限,但是,我們在程序中設置的時候不能超過硬件限制的上限

vim /etc/security/limits.conf

- soft ?nofile ? ?8000 ? ? ?—— 也可以通過命令ulimit -n 2000來修改為2000

- hard ?nofile ? 8000 ? ? ?—— 硬件資源限制

修改后重啟系統(tǒng)即可起效。

相關推薦

登錄即可解鎖
  • 海量技術文章
  • 設計資源下載
  • 產業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

Linux、C、C++、Python、Matlab,機器人運動控制、多機器人協(xié)作,智能優(yōu)化算法,貝葉斯濾波與卡爾曼濾波估計、多傳感器信息融合,機器學習,人工智能。