IO多路複用epoll

2022-07-10 12:00:23

0 why: 問題來源

0.1 網路程式設計流程

//建立socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//繫結IP地址和埠號port
bind(s, ...)
//監聽使用者端連線
listen(s, ...)
//接受使用者端連線
int c = accept(s, ...)
//接收使用者端資料
recv(c, ...);
//處理資料
operation(...)

0.2 核心接收網路資料過程

建立socket時,作業系統會建立一個由檔案系統管理的socket物件。這個socket物件包含了傳送緩衝區、接收緩衝區、等待佇列等成員。等待佇列指向所有需要等待該socket事件的程序。

新的檔案描述符fd都會插入等待佇列中,等到有資料到來時,等待序列會喚醒一個程序來處理資料。

0.3 問題來源

如何同時監視多個socket的資料?

1 解決方案之select模式

預先傳入一個socket列表,如果列表中的socket都沒有資料,掛起程序,直到有一個socket收到資料,喚醒程序。

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...)

int fds[] =  ;//存放需要監聽的socket

while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的資料處理
        }
    }
}

這裡需要注意的問題是:select檢視是否有資料輸入,需要進行遍歷所有的socket;而在程序喚醒後,程序一臉懵逼,只知道有資料來了,卻不知道是誰的資料,因此需要再次進行一次遍歷,找到資料來自於哪個socket。這種多次遍歷,每次都要將整個fds列表傳遞給核心,開銷很大,因此我們需要改進一下。

2 what: 解決方案之epoll模式

epoll模式相比於select模式,最大的改進在於增加了一箇中間環節「就緒列表」,還有就是分離了「socket插入到等待列表」和「阻塞等待事件到來」這兩個過程。

2.1 功能分離

每次呼叫select都需要這兩步操作,然而大多數應用場景中,需要監視的socket相對固定,並不需要每次都修改。epoll將這兩個操作分開,先用epoll_ctl維護等待佇列,再呼叫epoll_wait阻塞程序。
注意:epoll_wait方法不是使用迴圈的方式看是否有就緒時間,而是epoll_wait()一直阻塞直到:fd產生事件 / 被訊號處理常式打斷 / 超時。

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //將所有需要監聽的socket新增到epfd中

while(1){
    int n = epoll_wait(...)
    for(接收到資料的socket){
        //處理
    }
}

2.2 增加中間環節「就緒列表」

select低效的另一個原因在於程式不知道哪些socket收到資料,只能一個個遍歷。如果核心維護一個「就緒列表」,參照已就緒資料的socket,就能避免遍歷。

3 how: 如何用?

3.1 建立epoll

int epoll_create(int size);
在最初的epoll_create()實現中,size引數將呼叫者希望新增到的檔案描述符的數量告知核心。epoll範例。核心使用該資訊作為內部資料結構初始分配空間的提示,事件。 (如果有必要,如果呼叫方的使用超出了大小提示,核心將分配更多空間。)如今,此提示不再必需(核心無需提示即可動態調整所需資料結構的大小),但是大小必須仍大於零,以便當新的epoll應用程式在較舊的核心上執行時,請確保向後相容。

3.2 操作事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
向 epfd 對應的核心epoll 範例新增、修改或刪除對 fd 上事件 event 的監聽。op 可以為 EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL 分別對應的是新增新的事件,修改檔案描述符上監聽的事件型別,從範例上刪除一個事件。

3.3 監聽事件

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
當 timeout 為 0 時,epoll_wait 永遠會立即返回。而 timeout 為 -1 時,epoll_wait 會一直阻塞直到任一已註冊的事件變為就緒。當 timeout 為一正整數時,epoll 會阻塞直到計時 timeout 毫秒終了或已註冊的事件變為就緒。因為核心排程延遲,阻塞的時間可能會略微超過 timeout 毫秒。

4 參考

https://www.cnblogs.com/Hijack-you/p/13057792.html
https://www.agedcat.com/programming_language/cpp/525.html
https://blog.csdn.net/zhoumuyu_yu/article/details/112472419