本文分享自華為云社區(qū)《高性能網(wǎng)絡(luò)設(shè)計(jì)秘笈:深入剖析Linux網(wǎng)絡(luò)IO與epoll》,作者: Lion Long 。
一、epoll簡(jiǎn)介
epoll是Linux內(nèi)核中一種可擴(kuò)展的IO事件處理機(jī)制,可替代select和poll的系統(tǒng)調(diào)用。處理百萬級(jí)并發(fā)訪問性能更佳。
二、select的局限性
(1)?文件描述符越多,性能越差。?單個(gè)進(jìn)程中能夠監(jiān)視的文件描述符存在最大的數(shù)量,默認(rèn)是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需要遍歷整個(gè)句柄數(shù)組才能知道哪些句柄有事件。
(4)如果沒有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符的IO操作,那么每次調(diào)用select還是會(huì)將這些文件描述符通知進(jìn)程,即水平觸發(fā)。
(5)poll使用鏈表保存監(jiān)視的文件描述符,雖然沒有了監(jiān)視文件數(shù)量的限制,但是其他缺點(diǎn)依舊存在。
由于以上缺點(diǎn),基于select模型的服務(wù)器程序,要達(dá)到十萬以上的并發(fā)訪問,是很難完成的。因此,epoll出場(chǎng)了。
三、epoll的優(yōu)點(diǎn)
(1)不需要輪詢所有的文件描述符
(2)每次取就緒集合,都在固定位置
(3)事件的就緒和IO觸發(fā)可以異步解耦
四、epoll函數(shù)原型
4.1、epoll_create(int size)
#include <sys/epoll.h> int epoll_create(int size);
功能:創(chuàng)建epoll的文件描述符。
參數(shù)說明:size表示內(nèi)核需要監(jiān)控的最大數(shù)量,但是這個(gè)參數(shù)內(nèi)核已經(jīng)不會(huì)用到,只要傳入一個(gè)大于0的值即可。?當(dāng)size<=0時(shí),會(huì)直接返回不可用,這是歷史原因保留下來的,最早的epoll_create是需要定義一次性就緒的最大數(shù)量;后來使用了鏈表以便便維護(hù)和擴(kuò)展,就不再需要使用傳入的參數(shù)。
返回:返回該對(duì)象的描述符,注意要使用 close 關(guān)閉該描述符。
4.2、epoll_ctl
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl對(duì)應(yīng)系統(tǒng)調(diào)用sys_epoll_ctl
功能:操作epoll的文件描述符,主要是對(duì)epoll的紅黑樹節(jié)點(diǎn)進(jìn)行操作,比如節(jié)點(diǎn)的增刪改查。
參數(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 成員時(shí)一個(gè)聯(lián)合體類型,可以在調(diào)用 epoll_ctl 給 fd 添加/修改描述符監(jiān)聽的事件時(shí)攜帶一些數(shù)據(jù),方便后面的epoll_wait可以取出信息使用。
4.2.2、擴(kuò)展說明:SYSCALL_DEFINE數(shù)字 的宏定義
跟著的數(shù)字代表函數(shù)需要的參數(shù)數(shù)量,比如SYSCALL_DEFINE1代表函數(shù)需要一個(gè)參數(shù)、SYSCALL_DEFINE4代表函數(shù)需要4個(gè)參數(shù)。
4.2.3、注意
epoll_ctl是非阻塞的,不會(huì)被掛起。
4.3、epoll_wait
函數(shù)原型
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞一段時(shí)間,等待事件發(fā)生
返回:返回事件數(shù)量,事件集添加到events數(shù)組中。也就是遍歷紅黑樹中的雙向鏈表,把雙向鏈表中的節(jié)點(diǎn)數(shù)據(jù)拷貝出來,拷貝完畢后把節(jié)點(diǎn)從雙向鏈表中移除。
五、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;i<nready;i++) { int clientfd=events[i].data.fd; if(clientfd==listenfd) { struct sockaddr_in client; int len=sizeof(client); int confd=accept(listenfd,(struct sockaddr*)&client,&len); //step 2:創(chuàng)建struct epoll_event結(jié)構(gòu)體 struct epoll_event evt; evt.data.fd=confd;//保存監(jiān)聽的fd,以便epoll_wait的后續(xù)操作 evt.events=EPOLLIN;//設(shè)置監(jiān)聽fd的可讀事件 // step 3:添加事件監(jiān)聽 epoll_ctl(epfd,EPOLL_CTL_ADD,confd,&evt); } else if(events[i].events &EPOLLIN) { int ret = recv(clientfd,rbuffer,MAX_BUFF,0); if(ret>0) { 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 <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <fcntl.h> #include <unistd.h> #include <pthread.h> #include <sys/epoll.h> #include <string.h> #define BUFFER_LENGTH 128 #define EVENTS_LENGTH 128 char rbuff[BUFFER_LENGTH] = { 0 }; char wbuff[BUFFER_LENGTH] = { 0 }; int main() { // block int listenfd = socket(AF_INET, SOCK_STREAM, 0); // if (listenfd == -1) return -1; // listenfd struct 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 // nonblock int flag = fcntl(listenfd, F_GETFL, 0); flag |= O_NONBLOCK; fcntl(listenfd, F_SETFL, flag); #endif listen(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) { // accept struct 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的缺點(diǎn)
讀寫使用相同的緩沖區(qū)。比如上述的示例中,wbuffer和rbuffer是使用同一個(gè)緩沖區(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ù)時(shí):
(1)水平觸發(fā)是只要有數(shù)據(jù)就一直觸發(fā),直到數(shù)據(jù)讀完;
(2)邊沿觸發(fā)是來一次連接觸發(fā)一次,如果接受數(shù)據(jù)的buffer不夠大,則數(shù)據(jù)會(huì)保留在緩沖區(qū),下次觸發(fā)繼續(xù)從緩沖區(qū)讀出來;
6、一般,水平觸發(fā)只需要一個(gè)recv,邊沿觸發(fā)需要搭配while從緩沖區(qū)讀完數(shù)據(jù)
8.2、設(shè)置觸發(fā)模式
默認(rèn)是水平觸發(fā)模式,在事件中設(shè)置中 | EPOLLET 就可以設(shè)置邊沿觸發(fā),不設(shè)置則默認(rèn)是水平觸發(fā)。
例如:
ev.events=EPOLL_IN | EPOLLET
九、常見疑惑問題
9.1、為什么提前先定義一個(gè)事件?
我們需要注冊(cè),內(nèi)核才會(huì)有事件來的時(shí)候通知進(jìn)程。比如生活中要退一個(gè)快遞,那么我們需要注冊(cè)一個(gè)快遞公司的賬戶,然后發(fā)送一個(gè)退快遞請(qǐng)求時(shí)快遞公司才能找到你并取快遞。
9.2、epoll events超出EVENTS_LENGTH?
epoll會(huì)循環(huán)拷貝紅黑樹結(jié)構(gòu)體中的雙向鏈表節(jié)點(diǎn),讀取節(jié)點(diǎn)數(shù)據(jù),直到?jīng)]有事件。
9.3、緩沖區(qū)有多大空間時(shí)才返回可讀/可寫?
只要緩沖區(qū)有空間就返回可讀、可寫,不管空間多少。比如緩沖區(qū)是1024,但是有1023有數(shù)據(jù)了,這種極端條件也會(huì)返回可讀、可寫。
9.4、recv和send放在一起時(shí),有什么問題?
發(fā)送給客戶端數(shù)據(jù)很大的時(shí)候(大于內(nèi)核緩沖區(qū)),就可能出現(xiàn)send不全,客戶端recv不全,最好用EPOLLOUT單獨(dú)處理發(fā)送數(shù)據(jù)事件。
總結(jié)
本文介紹了網(wǎng)絡(luò)IO模型,引入了epoll作為L(zhǎng)inux系統(tǒng)中高性能網(wǎng)絡(luò)編程的核心工具。通過分析epoll的特點(diǎn)與優(yōu)勢(shì),并給出使用epoll的注意事項(xiàng)和實(shí)踐技巧,該文章為讀者提供了寶貴的指導(dǎo)。通過掌握這些知識(shí),讀者能夠構(gòu)建高效、可擴(kuò)展和穩(wěn)定的網(wǎng)絡(luò)應(yīng)用,提供出色的用戶體驗(yàn)。
點(diǎn)擊關(guān)注,第一時(shí)間了解華為云新鮮技術(shù)~文章來源:http://www.zghlxwxcb.cn/news/detail-594369.html
?文章來源地址http://www.zghlxwxcb.cn/news/detail-594369.html
到了這里,關(guān)于高性能網(wǎng)絡(luò)設(shè)計(jì)秘笈:深入剖析Linux網(wǎng)絡(luò)IO與epoll的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!