本文介紹了網路IO模型,引入了epoll作為Linux系統中高性能網路編程的核心工具。通過分析epoll的特點與優勢,並給出使用epoll的註意事項和實踐技巧,該文章為讀者提供了寶貴的指導。 ...
本文分享自華為雲社區《高性能網路設計秘笈:深入剖析Linux網路IO與epoll》,作者: Lion Long 。
一、epoll簡介
epoll是Linux內核中一種可擴展的IO事件處理機制,可替代select和poll的系統調用。處理百萬級併發訪問性能更佳。
二、select的局限性
(1) 文件描述符越多,性能越差。 單個進程中能夠監視的文件描述符存在最大的數量,預設是1024(在linux內核頭文件中定義有 #define _FD_SETSIZE 1024),當然也可以修改,但是文件描述符數量越多,性能越差。
(2)開銷巨大 ,select需要複製大量的句柄數據結構,產生了巨大的開銷(內核/用戶空間記憶體拷貝問題)。
(3)select需要遍歷整個句柄數組才能知道哪些句柄有事件。
(4)如果沒有完成對一個已經就緒的文件描述符的IO操作,那麼每次調用select還是會將這些文件描述符通知進程,即水平觸發。
(5)poll使用鏈表保存監視的文件描述符,雖然沒有了監視文件數量的限制,但是其他缺點依舊存在。
由於以上缺點,基於select模型的伺服器程式,要達到十萬以上的併發訪問,是很難完成的。因此,epoll出場了。
三、epoll的優點
(1)不需要輪詢所有的文件描述符
(2)每次取就緒集合,都在固定位置
(3)事件的就緒和IO觸發可以非同步解耦
四、epoll函數原型
4.1、epoll_create(int size)
#include <sys/epoll.h> int epoll_create(int size);
功能:創建epoll的文件描述符。
參數說明:size表示內核需要監控的最大數量,但是這個參數內核已經不會用到,只要傳入一個大於0的值即可。 當size<=0時,會直接返回不可用,這是歷史原因保留下來的,最早的epoll_create是需要定義一次性就緒的最大數量;後來使用了鏈表以便便維護和擴展,就不再需要使用傳入的參數。
返回:返回該對象的描述符,註意要使用 close 關閉該描述符。
4.2、epoll_ctl
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl對應系統調用sys_epoll_ctl
功能:操作epoll的文件描述符,主要是對epoll的紅黑樹節點進行操作,比如節點的增刪改查。
參數說明:
4.2.1、event參數說明
struct epoll_event結構體原型
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成員代表要監聽的epoll事件類型
events成員:
data成員:
data 成員時一個聯合體類型,可以在調用 epoll_ctl 給 fd 添加/修改描述符監聽的事件時攜帶一些數據,方便後面的epoll_wait可以取出信息使用。
4.2.2、擴展說明:SYSCALL_DEFINE數字 的巨集定義
跟著的數字代表函數需要的參數數量,比如SYSCALL_DEFINE1代表函數需要一個參數、SYSCALL_DEFINE4代表函數需要4個參數。
4.2.3、註意
epoll_ctl是非阻塞的,不會被掛起。
4.3、epoll_wait
函數原型
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞一段時間,等待事件發生
返回:返回事件數量,事件集添加到events數組中。也就是遍歷紅黑樹中的雙向鏈表,把雙向鏈表中的節點數據拷貝出來,拷貝完畢後把節點從雙向鏈表中移除。
五、epoll使用步驟
step 1:創建epoll文件描述符
int epfd = epoll_create(1);
step 2:創建struct epoll_event結構體
struct epoll_event ev; ev.data.fd=listenfd;//保存監聽的fd,以便epoll_wait的後續操作 ev.events=EPOLLIN;//設置監聽fd的可讀事件
step 3:添加事件監聽
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:創建struct epoll_event結構體 struct epoll_event evt; evt.data.fd=confd;//保存監聽的fd,以便epoll_wait的後續操作 evt.events=EPOLLIN;//設置監聽fd的可讀事件 // step 3:添加事件監聽 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';//剔除干擾數據 printf("recv: %s\n",rbuffer); memcpy(wbuffer,rbuffer,MAX_BUFF);//拷貝數據,做回傳示例 //step 2:創建struct epoll_event結構體 struct epoll_event evt; evt.data.fd=clientfd;//保存監聽的fd,以便epoll_wait的後續操作 evt.events=EPOLLOUT;//設置監聽fd的可寫事件 // step 3:修改事件監聽 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:創建struct epoll_event結構體 struct epoll_event evt; evt.data.fd=clientfd;//保存監聽的fd,以便epoll_wait的後續操作 evt.events=EPOLLIN;//設置監聽fd的可讀事件 // step 3:修改事件監聽 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的缺點
讀寫使用相同的緩衝區。比如上述的示例中,wbuffer和rbuffer是使用同一個緩衝區的,所以需要rbuff[ret] = ‘\0’;去除雜數據。
八、水平觸發(LT)與邊沿觸發(ET)
8.1、兩者差異
1、水平觸發可以一次recv,邊沿觸發需要用迴圈來recv;
2、水平觸發可以使用阻塞模式,邊沿模式不能
3、兩者性能差異非常小,一般小數據使用水平觸發LT,大數據使用邊沿觸發ET
4、listen fd最好使用水平觸發,儘量不要邊沿觸發
5、噹噹recv的buffer小於接受的數據時:
(1)水平觸發是只要有數據就一直觸發,直到數據讀完;
(2)邊沿觸發是來一次連接觸發一次,如果接受數據的buffer不夠大,則數據會保留在緩衝區,下次觸發繼續從緩衝區讀出來;
6、一般,水平觸發只需要一個recv,邊沿觸發需要搭配while從緩衝區讀完數據
8.2、設置觸發模式
預設是水平觸發模式,在事件中設置中 | EPOLLET 就可以設置邊沿觸發,不設置則預設是水平觸發。
例如:
ev.events=EPOLL_IN | EPOLLET
九、常見疑惑問題
9.1、為什麼提前先定義一個事件?
我們需要註冊,內核才會有事件來的時候通知進程。比如生活中要退一個快遞,那麼我們需要註冊一個快遞公司的賬戶,然後發送一個退快遞請求時快遞公司才能找到你並取快遞。
9.2、epoll events超出EVENTS_LENGTH?
epoll會迴圈拷貝紅黑樹結構體中的雙向鏈表節點,讀取節點數據,直到沒有事件。
9.3、緩衝區有多大空間時才返回可讀/可寫?
只要緩衝區有空間就返回可讀、可寫,不管空間多少。比如緩衝區是1024,但是有1023有數據了,這種極端條件也會返回可讀、可寫。
9.4、recv和send放在一起時,有什麼問題?
發送給客戶端數據很大的時候(大於內核緩衝區),就可能出現send不全,客戶端recv不全,最好用EPOLLOUT單獨處理髮送數據事件。
總結
本文介紹了網路IO模型,引入了epoll作為Linux系統中高性能網路編程的核心工具。通過分析epoll的特點與優勢,並給出使用epoll的註意事項和實踐技巧,該文章為讀者提供了寶貴的指導。通過掌握這些知識,讀者能夠構建高效、可擴展和穩定的網路應用,提供出色的用戶體驗。