本文分享自華為云社區(qū)《高性能網(wǎng)絡(luò)設(shè)計秘笈:深入剖析Linux網(wǎng)絡(luò)IO與epoll》,作者: Lion Long 。
一、epoll簡介
epoll是Linux內(nèi)核中一種可擴展的IO事件處理機制,可替代select和poll的系統(tǒng)調(diào)用。處理百萬級并發(fā)訪問性能更佳。
二、select的局限性
(1)文件描述符越多,性能越差。單個進程中能夠監(jiān)視的文件描述符存在最大的數(shù)量,默認是1024(在linux內(nèi)核頭文件中定義有 #define _FD_SETSIZE 1024),當(dāng)然也可以修改,但是文件描述符數(shù)量越多,性能越差。
(資料圖)
(2)開銷巨大,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生了巨大的開銷(內(nèi)核/用戶空間內(nèi)存拷貝問題)。
(3)select需要遍歷整個句柄數(shù)組才能知道哪些句柄有事件。
(4)如果沒有完成對一個已經(jīng)就緒的文件描述符的IO操作,那么每次調(diào)用select還是會將這些文件描述符通知進程,即水平觸發(fā)。
(5)poll使用鏈表保存監(jiān)視的文件描述符,雖然沒有了監(jiān)視文件數(shù)量的限制,但是其他缺點依舊存在。
由于以上缺點,基于select模型的服務(wù)器程序,要達到十萬以上的并發(fā)訪問,是很難完成的。因此,epoll出場了。
三、epoll的優(yōu)點
(1)不需要輪詢所有的文件描述符
(2)每次取就緒集合,都在固定位置
(3)事件的就緒和IO觸發(fā)可以異步解耦
四、epoll函數(shù)原型
4.1、epoll_create(int size)
#includeint epoll_create(int size);
功能:創(chuàng)建epoll的文件描述符。
參數(shù)說明:size表示內(nèi)核需要監(jiān)控的最大數(shù)量,但是這個參數(shù)內(nèi)核已經(jīng)不會用到,只要傳入一個大于0的值即可。當(dāng)size<=0時,會直接返回不可用,這是歷史原因保留下來的,最早的epoll_create是需要定義一次性就緒的最大數(shù)量;后來使用了鏈表以便便維護和擴展,就不再需要使用傳入的參數(shù)。
返回:返回該對象的描述符,注意要使用 close 關(guān)閉該描述符。
4.2、epoll_ctl
#includeint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);// epoll_ctl對應(yīng)系統(tǒng)調(diào)用sys_epoll_ctl
功能:操作epoll的文件描述符,主要是對epoll的紅黑樹節(jié)點進行操作,比如節(jié)點的增刪改查。
參數(shù)說明:
4.2.1、event參數(shù)說明
struct epoll_event結(jié)構(gòu)體原型
typedef union epoll_data{void* ptr;int fd;uint32_t u32;uint64_t u64};struct epoll_event{uint32_t events;epoll_data_t data;}
events成員代表要監(jiān)聽的epoll事件類型
events成員:
data成員:
data 成員時一個聯(lián)合體類型,可以在調(diào)用 epoll_ctl 給 fd 添加/修改描述符監(jiān)聽的事件時攜帶一些數(shù)據(jù),方便后面的epoll_wait可以取出信息使用。
4.2.2、擴展說明:SYSCALL_DEFINE數(shù)字 的宏定義
跟著的數(shù)字代表函數(shù)需要的參數(shù)數(shù)量,比如SYSCALL_DEFINE1代表函數(shù)需要一個參數(shù)、SYSCALL_DEFINE4代表函數(shù)需要4個參數(shù)。
4.2.3、注意
epoll_ctl是非阻塞的,不會被掛起。
4.3、epoll_wait
函數(shù)原型
#includeint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞一段時間,等待事件發(fā)生
返回:返回事件數(shù)量,事件集添加到events數(shù)組中。也就是遍歷紅黑樹中的雙向鏈表,把雙向鏈表中的節(jié)點數(shù)據(jù)拷貝出來,拷貝完畢后把節(jié)點從雙向鏈表中移除。
五、epoll使用步驟
step 1:創(chuàng)建epoll文件描述符
int epfd = epoll_create(1);
step 2:創(chuàng)建struct epoll_event結(jié)構(gòu)體
struct epoll_event ev;ev.data.fd=listenfd;//保存監(jiān)聽的fd,以便epoll_wait的后續(xù)操作ev.events=EPOLLIN;//設(shè)置監(jiān)聽fd的可讀事件
step 3:添加事件監(jiān)聽
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
step 4:等待事件
struct epoll_event events[EVENTS_LENGTH];char rbuffer[MAX_BUFF]={ 0 };char wbuffer[MAX_BUFF]={ 0 };while(1){int nready = epoll_wait(epfd,events,EVENTS_LENGTH,-1);//-1表示阻塞等待int i=0;for(i=0;i0){rbuffer[ret]="\0";//剔除干擾數(shù)據(jù)printf("recv: %s\n",rbuffer);memcpy(wbuffer,rbuffer,MAX_BUFF);//拷貝數(shù)據(jù),做回傳示例//step 2:創(chuàng)建struct epoll_event結(jié)構(gòu)體struct epoll_event evt;evt.data.fd=clientfd;//保存監(jiān)聽的fd,以便epoll_wait的后續(xù)操作evt.events=EPOLLOUT;//設(shè)置監(jiān)聽fd的可寫事件// step 3:修改事件監(jiān)聽epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&evt);}}else if(events[i].events &EPOLLOUT){int ret = send(clientfd,wbuffer,MAX_BUFF,0);printf("send: %s\n",wbuffer);//step 2:創(chuàng)建struct epoll_event結(jié)構(gòu)體struct epoll_event evt;evt.data.fd=clientfd;//保存監(jiān)聽的fd,以便epoll_wait的后續(xù)操作evt.events=EPOLLIN;//設(shè)置監(jiān)聽fd的可讀事件// step 3:修改事件監(jiān)聽epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&evt);}}}
六、完整示例代碼
#include#include #include #include #include #include #include #include #include #define BUFFER_LENGTH 128#define EVENTS_LENGTH 128char rbuff[BUFFER_LENGTH] = { 0 };char wbuff[BUFFER_LENGTH] = { 0 };int main() {// blockint listenfd = socket(AF_INET, SOCK_STREAM, 0); //if (listenfd == -1) return -1;// listenfdstruct sockaddr_in servaddr;servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(9999);if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {return -2;}#if 0 // nonblockint flag = fcntl(listenfd, F_GETFL, 0);flag |= O_NONBLOCK;fcntl(listenfd, F_SETFL, flag);#endiflisten(listenfd, 10);int epfd = epoll_create(1);struct epoll_event ev, events[EVENTS_LENGTH];ev.events = EPOLLIN;ev.data.fd = listenfd;epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);printf("epfd : %d\n", epfd);while (1){int nready = epoll_wait(epfd, events, EVENTS_LENGTH, -1);printf("nready --> %d\n",nready);int i;for (i = 0; i < nready;i++){int clientfd = events[i].data.fd;if (listenfd == clientfd){// acceptstruct sockaddr_in client;int len = sizeof(client);int conffd = accept(clientfd, (struct sockaddr*)&client,&len);printf("conffd --> %d\n",conffd);ev.events = EPOLLIN;ev.data.fd = conffd;epoll_ctl(epfd, EPOLL_CTL_ADD, conffd, &ev);}else if(events[i].events & EPOLLIN)//client{int ret=recv(clientfd, rbuff, BUFFER_LENGTH, 0);if (ret > 0){rbuff[ret] = "\0";printf("recv buffer: %s\n", rbuff);/*int j;for (j = 0; j < BUFFER_LENGTH;j++){buff[j] = "a" + (j % 26);}send(clientfd, buff, BUFFER_LENGTH, 0);*/memcpy(wbuff, rbuff, BUFFER_LENGTH);ev.events = EPOLLOUT;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);}}else if (events[i].events & EPOLLOUT){send(clientfd, wbuff, BUFFER_LENGTH, 0);printf("send --> %s\n",wbuff);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);}}}return 0;}
七、epoll的缺點
讀寫使用相同的緩沖區(qū)。比如上述的示例中,wbuffer和rbuffer是使用同一個緩沖區(qū)的,所以需要rbuff[ret] = ‘\0’;去除雜數(shù)據(jù)。
八、水平觸發(fā)(LT)與邊沿觸發(fā)(ET)
8.1、兩者差異
1、水平觸發(fā)可以一次recv,邊沿觸發(fā)需要用循環(huán)來recv;
2、水平觸發(fā)可以使用阻塞模式,邊沿模式不能
3、兩者性能差異非常小,一般小數(shù)據(jù)使用水平觸發(fā)LT,大數(shù)據(jù)使用邊沿觸發(fā)ET
4、listen fd最好使用水平觸發(fā),盡量不要邊沿觸發(fā)
5、當(dāng)當(dāng)recv的buffer小于接受的數(shù)據(jù)時:
(1)水平觸發(fā)是只要有數(shù)據(jù)就一直觸發(fā),直到數(shù)據(jù)讀完;
(2)邊沿觸發(fā)是來一次連接觸發(fā)一次,如果接受數(shù)據(jù)的buffer不夠大,則數(shù)據(jù)會保留在緩沖區(qū),下次觸發(fā)繼續(xù)從緩沖區(qū)讀出來;
6、一般,水平觸發(fā)只需要一個recv,邊沿觸發(fā)需要搭配while從緩沖區(qū)讀完數(shù)據(jù)
8.2、設(shè)置觸發(fā)模式
默認是水平觸發(fā)模式,在事件中設(shè)置中 | EPOLLET 就可以設(shè)置邊沿觸發(fā),不設(shè)置則默認是水平觸發(fā)。
例如:
ev.events=EPOLL_IN | EPOLLET
九、常見疑惑問題
9.1、為什么提前先定義一個事件?
我們需要注冊,內(nèi)核才會有事件來的時候通知進程。比如生活中要退一個快遞,那么我們需要注冊一個快遞公司的賬戶,然后發(fā)送一個退快遞請求時快遞公司才能找到你并取快遞。
9.2、epoll events超出EVENTS_LENGTH?
epoll會循環(huán)拷貝紅黑樹結(jié)構(gòu)體中的雙向鏈表節(jié)點,讀取節(jié)點數(shù)據(jù),直到?jīng)]有事件。
9.3、緩沖區(qū)有多大空間時才返回可讀/可寫?
只要緩沖區(qū)有空間就返回可讀、可寫,不管空間多少。比如緩沖區(qū)是1024,但是有1023有數(shù)據(jù)了,這種極端條件也會返回可讀、可寫。
9.4、recv和send放在一起時,有什么問題?
發(fā)送給客戶端數(shù)據(jù)很大的時候(大于內(nèi)核緩沖區(qū)),就可能出現(xiàn)send不全,客戶端recv不全,最好用EPOLLOUT單獨處理發(fā)送數(shù)據(jù)事件。
總結(jié)
本文介紹了網(wǎng)絡(luò)IO模型,引入了epoll作為Linux系統(tǒng)中高性能網(wǎng)絡(luò)編程的核心工具。通過分析epoll的特點與優(yōu)勢,并給出使用epoll的注意事項和實踐技巧,該文章為讀者提供了寶貴的指導(dǎo)。通過掌握這些知識,讀者能夠構(gòu)建高效、可擴展和穩(wěn)定的網(wǎng)絡(luò)應(yīng)用,提供出色的用戶體驗。
點擊關(guān)注,第一時間了解華為云新鮮技術(shù)~
關(guān)鍵詞: