目錄
一. epoll的實現原理
二.? epoll的相關接口
2.1 epoll_create --?創建epoll模型
2.2 epoll_ctl --?對epoll模型進行控制
2.3?epoll_wait --?等待epoll所關注的事件就緒
2.4?epoll相關接口的使用方法
三. Epoll服務器的模擬實現
3.1 EpollServer類的聲明
3.2?EpollServer類的實現
四.?總結
一. epoll的實現原理
在通過select和poll實現IO多路轉接時,都需要程序員來維護一個數組,用來隨時控制要被關心的事件(fd)。同時,使用select和poll實現IO多路轉接,都需要對用戶維護的數組進行遍歷,遍歷操作的時間復雜度為O(N),這會很大程度上消耗計算資源,降低效率。
為了解決poll和select的這些缺陷,epoll被提了出來,相比于select和poll,epoll在有事件就緒時,不需要逐個遍歷檢查每個被關注的文件描述符是否就緒,希望被關注的事件不需要程序員自己維護數組來控制,這提高了效率,降低了程序員的代碼編寫成本。
在使用epoll實現IO多路轉接之前,必須要先在OS內核中建立epoll模型。如圖1.1所示,epoll模型主要有兩部分組成:
- 一顆紅黑樹:用來維護用戶所關注的事件(fd),一個紅黑樹節點對應一個被關注的事件,節點中要記錄包括文件描述符,所關注事件的類型(讀/寫/異常)等。
- 就緒隊列:當有事件就緒的時候,會將已經就緒的事件添加到就緒隊列中去,從隊頭拿走就緒事件及其相關的屬性信息交給用戶層,就能對已經就緒的事件進行響應。
相比于直接遍歷數組查找某個節點O(N)的時間復雜度,使用紅黑樹查找的時間復雜度為O(logN),這樣就提高了OS內核管理事件的效率。同時,通過就緒隊列維護已經就緒的事件,避免了在wait成功之后在遍歷數組的過程中確定具體是哪個事件就緒,這進一步降低了資源的消耗,提高效率。

二.? epoll的相關接口
2.1 epoll_create --?創建epoll模型
函數原型:int epoll_create(size_t size)
頭文件:#include <sys/epoll.h>
函數參數:在Linux 2.6.8版本之后參數size就已經被棄用,這里是為了向前兼容,在調用接口時只需要傳一個大于0的參數即可。
返回值:如果創建成功,返回新創建的epoll模型的文件描述符epfd,如果創建失敗返回-1。
epoll_create函數所執行的工作,就是在操作系統內核中,創建一個如圖1.1所示的epoll模型,即:一顆紅黑樹和一個就緒隊列。epoll_create接口返回值表示被創建的epoll模型的對應fd值,可見OS是將epoll模型當做文件來處理的,這符合Linux下一切接文件的觀點。
2.2 epoll_ctl --?對epoll模型進行控制
函數原型:int epoll_create(int epfd, int op, int fd, struct epoll_event *event)
頭文件:#include <sys/epoll.h>
函數參數:
- epfd --?所要進行操作的epoll模型對應的文件描述符。
- op --?用于指定所要進行的操作。
- fd --?用于對epoll進行操作的文件描述符,如指明要添加關注的事件。
- event --?輸入型參數,告知OS要關注的事件的屬性信息。
返回值:如果函數執行成功返回0,失敗返回-1。
在該接口函數的參數中,epfd為通過epoll_create創建epoll模型獲取的文件描述符,op用于指定所進行的操作的類型,表2.1為op的可選值及其對應的意義。
op | 意義 |
---|---|
EPOLL_CTL_ADD | 將指定事件添加到epoll模型中進行關注 |
EPOLL_CTL_DEL | 刪除epoll模型中對某個事件的關注 |
EPOLL_CTL_MOD | 改變epoll模型中某個事件被關注的狀態(event) |
fd用于指定對epoll進行操作的文件描述符,可以為listen文件描述符,也可為普通文件描述符,假設op傳EPOLL_CTL_ADD,那么所epoll_ctl所進行的操作就是將fd加入到epoll模型中進行關注。
struct epoll_event?類型數據定義如下,該類型包含兩個成員,其中一個為uint32_t類型成員events,用于控制該事件的屬性是可讀、可寫還是異常等。events可以為表示4.2中宏的集合,還有一個為聯合自定義類型數據,其中該聯合類型可以傳四種不同的數據表達不同的意義,但一般使用fd來指定文件描述符。
struct epoll_event類型的定義:?
typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event
{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
events可選宏 | 含義 |
---|---|
EPOLLIN | 對應文件描述符可讀 |
EPOLLOUT | 對應文件描述符可寫 |
EPOLLPRI | 對應文件描述符帶有緊急數據可讀(TCP緊急指針置1的報文) |
EPOLLERR | 對應文件描述符異常 |
EPOLLHUP | 對應文件描述符被掛起 |
EPOLLSHOT | 對應文件描述符只被監視一次,監視完一次后會移出epoll模型 |
2.3?epoll_wait --?等待epoll所關注的事件就緒
函數原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
頭文件:#include <sys/epoll.h>
函數參數:
- epfd --?進行等待的epoll模型文件描述符。
- events --?輸出型參數,用于存放已經就緒了的事件相關的屬性信息,如文件描述符fd以及就緒事件的類型(可讀/可寫/...)等。
- maxevents --?一次wait所能獲取到的最大的就緒事件數量。
- timeout --?最長阻塞等待時間,傳-1表示一直阻塞等待,傳0表示完全非阻塞。
返回值:如果等待成功,返回已經就緒的事件的數量,返回0表示在設定的阻塞時間內沒有事件就緒,返回-1表示等待失敗。
相對于select和poll在等待成功后需要遍歷整個數組來確定具體哪一個文件描述符就緒,epoll_wait等待到的就緒事件相關屬性信息會被放置在events所指向的空間的前n個位置,其中n為就緒事件數量,即epoll_wait的返回值。因此,只需要遍歷events[0] ~ events[n-1]即可,events[0] ~ events[n-1]中記錄的事件一定是已經就緒了的。
如果已經就緒了的事件多于maxevents會發生什么情況呢?這時會先拿取maxevents個已經就緒的事件,剩余的等到下一輪epoll_wait再進行提取處理,并不會造成任何錯誤。
2.4?epoll相關接口的使用方法
通過epoll實現IO多路轉接,需要按照以下三步操作執行:
- 通過epoll_create創建epoll模型。
- 通過epoll_ctl添加對特定事件的關注。
- 通過epoll_wait等待所關注的事件的一個或多個就緒。
為了方便調用epoll相關的接口函數,代碼2.1將epoll的3個相關接口函數進行了封裝。在代碼2.1中包含了log.hpp頭文件,里面是日志打印函數的聲明和實現,詳見代碼2.2。
代碼2.1:對epoll接口的封裝(Epoll.hpp頭文件)
#pragma once#include "log.hpp"
#include <cstring>
#include <cerrno>
#include <sys/epoll.h>namespace Epoll
{static const int default_size = 1024;// 創建Epoll模型函數static int EpollCreate(int size = default_size){int epfd = epoll_create(size);if(epfd < 0)logMessage(FATAL, "Epoll create fail, %d:%s\n", errno, strerror(errno));elselogMessage(NORMAL, "Epoll create success, epfd:%d\n", epfd);return epfd;}// 進行Epoll控制函數static int EpollCtl(int epfd, int op, int fd, uint32_t events){struct epoll_event event;event.events = EPOLLIN;event.data.fd = fd;int ret = epoll_ctl(epfd, op, fd, &event);if(ret < 0) logMessage(ERROR, "Epoll control fail, %d:%s\n", errno, strerror(errno));else logMessage(NORMAL, "Epoll control success, ret:%d\n", ret);return ret;}// Epoll等待函數static int EpollWait(int epfd, struct epoll_event* events, int maxevents, int timeout){return epoll_wait(epfd, events, maxevents, timeout);}
}
代碼2.2:日志打印函數的聲明和實現(log.hpp頭文件)
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>#define DEBUG 0
#define NORMAL 1
#define WARING 2
#define ERROR 3
#define FATAL 4static const char* g_levelMap[5] =
{"DEBUG","NORMAL","WARING","ERROR","FATAL"
};static void logMessage(int level, const char *format, ...)
{// 1. 輸出常規部分time_t timeStamp = time(nullptr);struct tm *localTime = localtime(&timeStamp);printf("[%s] %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);// 2. 輸出用戶自定義部分va_list args;va_start(args, format);vprintf(format, args);va_end(args);
}
三. Epoll服務器的模擬實現
3.1 EpollServer類的聲明
在EpollServer中,要包含以下成員變量:epoll模型文件描述符、listen套接字、端口號、ip地址以及指向存放就緒事件相關屬性信息的空間的指針。
要有以下成員函數:構造函數和析構函數、服務器運行函數Start、就緒事件處理函數Handler、接收客戶端連接請求函數Accepter、對端數據讀取函數Reciever。
#pragma once#include "Sock.hpp"
#include "Epoll.hpp"
#include <unistd.h>static const int maxevents = 64;class EpollServer
{
public:EpollServer(uint16_t port = 8080, const std::string& ip = ""); // 構造函數void Start(); // 啟動運行函數~EpollServer(); // 析構函數private:void Handler(int n); // 就緒事件處理函數void Accepter(int listenSock); // 連接接收函數void Reciever(int fd); // 信息讀取函數int _listenSock; // 監聽套接字int _epfd; // epoll套接字uint16_t _port; // 服務進程端口號std::string _ip; // 服務器ip struct epoll_event* _ptr_events; // 指向就緒隊列的指針
};
3.2?EpollServer類的實現
- 構造函數:首先要進行關于tcp通信的常規準備工作,獲取listen套接字、綁定端口號、設置監聽狀態,之后要創建epoll模型、開辟一塊內存空間用于存放wait到的就緒事件并將listen套接字添加到epoll模型中去。
- 析構函數:判斷listen套接字和epoll模型文件描述符是否>=0,如果是,就close掉它們。再判斷_ptr_events是否為nullptr,如果否,要delete[]釋放動態申請的內存空間。
- 服務器運行函數Start:常駐進程,執行while死循環,每層while循環都調用epoll_wait檢測事件就緒的情況,如果epoll_wait返回值大于0,那么調用Handler函數處理就緒事件。
- 就緒事件處理函數Handler:接收一個參數n表示已就緒事件的數量,遍歷_ptr_events[0] ~?ptr_events[n-1],根據就緒的是listen文件描述符還是普通文件描述符,分類進行后續處理,listen文件描述符調用Accepter函數接收對端連接,普通文件描述符調用Reciever函數接收數據。
- 接收對端連接函數Accepter:獲取對端連接,分配文件描述符fd,并將fd添加到epoll模型中去。
- 數據讀取函數Reciever:調用read函數讀取客戶端發送的數據,如果read返回值>0,那么就執行對應操作處理讀取到的數據,如果read返回值為0,那么表示客戶端關閉,要將對應的文件描述符fd從epoll模型中刪除。
代碼3.2:EpollServer的實現(EpollServer.cc源文件)
#include "EpollServer.hpp"EpollServer::EpollServer(uint16_t port, const std::string& ip): _listenSock(-1), _epfd(-1), _port(port), _ip(ip), _ptr_events(nullptr)
{// 1.獲取監聽套接字_listenSock = Sock::Socket();if(_listenSock < 0) exit(1);// 2.綁定端口號if(Sock::Bind(_listenSock, _ip, _port) < 0) exit(2);// 3.設置監聽狀態if(Sock::Listen(_listenSock) < 0) exit(3);// 4.創建epoll模型_epfd = Epoll::EpollCreate();if(_epfd < 0) exit(4);// 5.為_events開辟內存空間并進行初始化_ptr_events = new epoll_event[maxevents];// 6.將listenSock添加到epoll模型中if(Epoll::EpollCtl(_epfd, EPOLL_CTL_ADD, _listenSock, EPOLLIN) < 0) exit(5);logMessage(NORMAL, "EpollServer init success!\n");
}// Epoll服務器啟動運行函數
void EpollServer::Start()
{while(1){// 對epoll進行等待int n = Epoll::EpollWait(_epfd, _ptr_events, maxevents, -1);switch(n){case 0:logMessage(DEBUG, "epoll wait time out!\n");break;case -1:logMessage(ERROR, "epoll wait error, %d:%s\n", errno, strerror(errno));break;default:Handler(n);break;}}
}// 析構函數
EpollServer::~EpollServer()
{if(_listenSock >= 0) close(_listenSock);if(_epfd >= 0) close(_epfd);if(_ptr_events) delete[] _ptr_events;
}// 就緒事件處理函數
void EpollServer::Handler(int n)
{// 遍歷_events,查找已經就緒的事件for(int i = 0; i < n; ++i){// 分listen套接字和普通套接字兩種情況討論if(_ptr_events[i].data.fd == _listenSock) Accepter(_listenSock);else Reciever(_ptr_events[i].data.fd);}
}// 連接接收函數
void EpollServer::Accepter(int listenSock)
{std::string cli_ip; // 發起連接的客戶端ipuint16_t cli_port; // 客戶端端口號int fd = Sock::Accept(listenSock, cli_ip, cli_port);if(fd < 0) return;// 將新增的fd添加到epoll模型中去Epoll::EpollCtl(_epfd, EPOLL_CTL_ADD, fd, EPOLLIN);logMessage(NORMAL, "Add a new fd to epoll success, fd:%d\n", fd);
}// 信息讀取函數
void EpollServer::Reciever(int fd)
{char buffer[1024];ssize_t n = read(fd, buffer, 1023);// 如果讀取成功if(n > 0){buffer[n - 1] = '\0';printf("Client# %s\n", buffer);}else if(n == 0){// 對端關閉,將fd從epoll模型中拿走Epoll::EpollCtl(_epfd, EPOLL_CTL_DEL, fd, EPOLLIN);if(n == 0){// logMessage(NORMAL, "Remove fd from epoll success, fd:%d\n", fd);printf("Remove fd from epoll success, fd:%d\n", fd);close(fd);}}else // 讀取數據失敗{logMessage(ERROR, "Read message fail, %d:%s\n", errno, strerror(errno));}
}
四.?總結
- 相比于通過select和poll實現IO多路轉接,epoll不需要程序員維護數組來控制關注的事件,在有事件就緒后也不需要遍歷數組查找具體哪個事件就緒,效率較高。
- epoll底層維護一顆紅黑樹存儲要關心的事件,維護一個就緒隊列表示已經就緒的事件。
- epoll實現IO多路轉接,要進行的操作為:epoll_create創建epoll模型 ->?epoll_ctl向epoll模型中添加受到關注的文件描述符 -> epoll_wait等待事件就緒。