高性能網路設計秘笈:深入剖析Linux網路IO與epoll

来源:https://www.cnblogs.com/huaweiyun/archive/2023/07/18/17562530.html
-Advertisement-
Play Games

本文介紹了網路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的紅黑樹節點進行操作,比如節點的增刪改查。

參數說明:

cke_16778.png

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成員:

cke_34833.png

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數組中。也就是遍歷紅黑樹中的雙向鏈表,把雙向鏈表中的節點數據拷貝出來,拷貝完畢後把節點從雙向鏈表中移除。

cke_50950.png

五、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的註意事項和實踐技巧,該文章為讀者提供了寶貴的指導。通過掌握這些知識,讀者能夠構建高效、可擴展和穩定的網路應用,提供出色的用戶體驗。

點擊關註,第一時間瞭解華為雲新鮮技術~

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文翻譯自國外論壇 medium,原文地址: # 什麼是數據複製? 數據複製是指將數據複製到一個或多個數據容器以確保可用性的過程。複製的數據通常存儲在不同的資料庫實例中,即使一個實例發生故障,我們也可以從其他實例獲取數據。 一種流行數據複製的實現架構是主從架構。 > 推薦博主開源的 H5 商城項目* ...
  • ## springboot下使用rabbitMQ之傳參及序列化(二) 消息參數傳遞在開發中也是個坑,不論使用內置的`SimpleMessageConverter`還是`Jackson2JsonMessageConverter`均無法讓Consumer接收動態參數 ### 一.序列化的問題 首先貼出具 ...
  • 容器鏡像是可執行的軟體包,包括運行應用程式所需的所有內容:代碼、運行時、系統工具、庫和設置。通過構建自定義鏡像,您可以在任何支持Docker的平臺上無縫地部署應用程式及其所有依賴項。 ### Dockerfile 構建容器鏡像的關鍵組件是 **`Dockerfile`**。它本質上是一個包含有關如何 ...
  • # 預處理 ## 動態庫和靜態庫 > 庫: 將源文件生成的二進位文件 只需要鏈接即可生成可執行文件 ## 製作靜態庫 ``` linux gcc -c fun.c -o fun.o ar rc libtestlib.a fun.o ``` ### 使用靜態庫 #### 庫和工程在同一目錄下 ``` ...
  • ## 概念 定義:給定數集 $S$,以異或運算張成的數集與 $S$ 相同的極大線性無關集,稱為原數集的一個線性基。 簡單地說,線性基是一個數的集合。每個序列都擁有至少一個線性基。取線性基中若幹個數異或起來可以得到原序列中的任何一個數。 ## 性質 - 性質一 > - 取線性基中若幹個數異或起來可以得 ...
  • # 面向對象編程 根據類來創建對象稱為實例化。這裡只過一下大概的面向對象的內容,不做細講。可以直接查閱資料。https://www.runoob.com/python3/python3-class.html ## 創建和使用類及實例 給出一個類的使用例子: ```python class Dog: ...
  • # if 語句 給出一個簡單的示例 ```python cars = ["audi", "bmw", "subaru", "toyota"] for car in cars: if car == "bmw": print(car.upper()) else: print(car.title()) ` ...
  • 知道要轉型,要建設數據中台,卻不知咋做,咋辦? 現在有很多講“如何建設數據中台”文章,觀點各不相同: - 數據中台是數據建設方法論,按照數據中台設計方法和規範實施就可建成數據中台 - 數據中台背後是數據部門組織架構變更,把原先分散的組織架構形成一個統一中台部門,就建成數據中台 - 一些大數據公司說, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...