epoll:
Linux下性能最高的多路轉接模型
epoll 有3個相關的系統調用.
epoll_create
功能:創建epoll,在內核中創建eventpoll結構體,size決定了epoll最多監控多少個描述符,在Linux2.6.8之后被忽略,但是必須>0。返回一個文件描述符,作為epoll的操作句柄
struct eventpoll{
...rb_root rbr(紅黑樹)...struct list_head rdlist(雙向鏈表)...
}
int epoll_create(int size)
創建一個epoll的句柄.
- 自從linux2.6.8之后,size參數是被忽略的.
- 用完之后, 必須調用close()關閉
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:對內核中的eventpoll 結構體進行操作:epoll采用事件結構方式對描述符進行事件監控;用戶定義struct epoll_event描述符事件結構信息;將事件信息可以拷貝到內核添加到eventpoll結構體中的紅黑數中
參數說明
- 它不同于select()是在監聽事件時告訴內核要監聽什么類型的事件, 而是在這里先注冊要監聽的事件類型.
- 第一個參數是epoll_create()的返回值(epoll的句柄).
- 第二個參數表示動作,用三個宏來表示. 、
- 第三個參數是需要監聽的fd.
- 第四個參數是告訴內核需要監聽什么事. 描述符對應的事件結構信息
第二個參數的取值:
EPOLL_CTL_ADD
:注冊新的fd到epfd中;向紅黑數中添加描述符的監控事件結構信息eventEPOLL_CTL_MOD
:修改已經注冊的fd的監聽事件;修改描述符在紅黑數中的對應事件結構信息eventEPOLL_CTL_DEL
:從epfd中刪除一個fd,從紅黑數中移除描述符的監控事件結構信息event
struct epoll_event結構如下
struct epoll_event {
uint32_t events; /* 用戶對描述符進行監控的事件 */
epoll_data_t data; /* User data variable */
};typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
events可以是以下幾個宏的集合:
- EPOLLIN : 表示對應的文件描述符可以讀 (包括對端SOCKET正常關閉);
- EPOLLOUT : 表示對應的文件描述符可以寫;
- EPOLLPRI : 表示對應的文件描述符有緊急的數據可讀 (這里應該表示有帶外數據到來);
- EPOLLERR : 表示對應的文件描述符發生錯誤;
- EPOLLHUP : 表示對應的文件描述符被掛斷;
- EPOLLET : 將EPOLL設為邊緣觸發(Edge Triggered)模式, 這是相對于水平觸發(Level Triggered)來說的.
- EPOLLONESHOT:只監聽一次事件, 當監聽完這次事件之后, 如果還需要繼續監聽這個socket的話, 需要 再次把這個socket加入到EPOLL隊列里.
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
- epfd: epoll 操作句柄
- events:epoll_event 事件結構信息數組的結點數量
- maxevents : epoll_event事件結構信息數組的結點數量
- timeout:epoll_wait 監控的等待超時時間
- 返回值:<0----監控出錯 ==0----監控超時 >0----就緒的描述符事件個數
收集在epoll監控的事件中已經發送的事件
- 參數events是分配好的epoll_event結構體數組.
- epoll將會把發生的事件賦值到events數組中 (events不可以是空指針,內核只負責把數據復制到這個 events數組中,不會去幫助我們在用戶態中分配內存).
- maxevents告之內核這個events有多大,這個 maxevents的值不能大于創建epoll_create()時的size.
- 參數timeout是超時時間 (毫秒,0會立即返回,-1是永久阻塞).
- 如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時, 返回小于0表示函 數失敗
epoll_wait 會將就緒的描述符對應事件結構信息拷貝到events結構數組中;相當于直接告訴用戶哪個描述符就緒;用戶直接就從epoll_event 結構體數組中取出信息,對描述符直接進行相應事件操作
epoll監控流程
- epoll對描述符的事件監控是一個異步操作;epoll_wait發起調用,讓操作系統對描述符進行相應事件監控
- 操作系統對每個要監控的描述符都定義了就緒事件回調函數;當描述符相應事件就緒的時候,觸發事件,調用回調函數(將描述符事件結構信息指針添加到eventpoll的雙向鏈表中)
- 但是epoll_wait并沒有直接返回(是一個阻塞操作),每隔一會就看一下eventpoll中雙向鏈表是否為空;來判斷是否有描述符就緒;若為空,則沒有描述符就緒;則等待一會,重新查看
- 若雙向鏈表不為空------表示有描述符事件就緒;將這個描述符對應的事件結構信息,拷貝到epoll_wait傳入的事件結構數組中后調用返回。
epoll工作原理
struct eventpoll{
...rb_root rbr(紅黑樹)...struct list_head rdlist(雙向鏈表)...
}
當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成 員與epoll的使用方式密切相關.
- 每一個epoll對象都有一個獨立的eventpoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加進來 的事件.
- 這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插 入時間效率是lgn,其中n為樹的高度).
- 而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當響應的事件發生時 會調用這個回調方法.
- 這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中.
- 在epoll中,對于每一個事件,都會建立一個epitem結構體.
- 當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem 元素即可.
- 如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶. 這個操作的時間復雜度 是O(1).
/* * 這個程序完成epoll接口的基本封裝* bool Init()* bool Add(TcpSocket &sock)* bool Del(TcpSocket &sock)* bool Wait(std::vector<TcpSocket>&list,int timeout_msec)*/#include<iostream>
#include<vector>
#include<sys/epoll.h>
#include"tcpsocket.hpp"#define MAX_EVENTS 10
class Epoll
{public:bool Init(){//int epoll_create(int size)_epfd = epoll_create(1);if(_epfd < 0){ perror("epoll create error");return false;} return true;} bool Add(TcpSocket &sock){//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) int fd = sock.GetFd();struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN;int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd , &ev);if(ret < 0){ perror("epoll add error");return false;} return true;} bool Del(TcpSocket &sock){int fd = sock.GetFd();int ret =epoll_ctl(_epfd, EPOLL_CTL_DEL, fd,NULL);if(ret < 0){perror("epoll del error");return false;}return true;}bool Wait(std::vector<TcpSocket>&list,int timeout_msec = 3000){//int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) struct epoll_event evs[MAX_EVENTS];int nfds = epoll_wait(_epfd, evs, MAX_EVENTS, timeout_msec);if(nfds < 0){perror("epoll wait error");}else if(nfds == 0){std::cerr<<"epoll wait timeout";return false;}for(int i =0 ; i < nfds; i++ ){TcpSocket sock;sock.SetFd(evs[i].data.fd);list.push_back(sock);}return true;}private:int _epfd;};int main()
{TcpSocket lst_sock;CHECK_RET(lst_sock.Socket());CHECK_RET(lst_sock.Bind("0.0.0.0",9000));CHECK_RET(lst_sock.Listen());Epoll epoll;CHECK_RET(epoll.Init());CHECK_RET(epoll.Add(lst_sock));while(1){std::vector<TcpSocket>list;bool ret = epoll.Wait(list);if(ret == false){ continue;}for(int i =0 ;i < list.size();i++){if(lst_sock.GetFd() == list[i].GetFd()){TcpSocket cli_sock;std::string cli_ip;uint16_t cli_port;ret = lst_sock.Accept(cli_sock,cli_ip,cli_port);if(ret == false){continue;}epoll.Add(cli_sock);}else{std::string buf;ret = list[i].Recv(buf);if(ret == false){//接收出錯epoll.Del(list[i]);list[i].Close();continue;}std::cout<< "client-say:"<< buf <<std::endl;}}}lst_sock.Close();return 0;
}
epoll事件觸發方式:
水平觸發–EPOLLT/邊緣觸發EPOLLET
水平觸發方式
可讀事件就緒:接受緩沖區數據大小,大于低水位標記(默認1字節)
可寫事件就緒:發送緩沖區中空閑大小,大于低水位標記(默認1字節)
只要接受/發送緩沖區中數據/剩余空間大小大于低水平標記就會一直觸發事件
邊緣觸發方式
可讀事件就緒:接受緩沖區中,只有新數據到來的時候才會觸發一次
可寫事件就緒:發送緩沖區中,只有從剩余空間大小從0變為大于0的時候才會觸發
注意事項
邊緣觸發方式中,只有新數據到來的時候,可讀事件才會觸發一次
需要用戶在這一次事件觸發中將緩沖區中的數據全部讀取完畢(循環讀,直到不能讀為止)
但是套接字默認recv沒有數據的時候會阻塞;為了避免循環讀取數據導致程序流程因為阻塞而無法繼續推進,因此需要將描述符設置為非阻塞
fcntl
int fcntl(int fd,int cmd, .../*arg*/);fd : 要設置的描述符cmd : 對描述符要進行的操作F_SETFL 通過arg參數設置描述符屬性狀態F_GETFL 返回描述符的屬性狀態信息 ,arg被忽略arg:要設置的描述符屬性狀態信息O_NONBLOCK 將描述符設置為非阻塞
epoll優缺點分析
- epoll采用事件結構方式對描述符進行監控,簡化了select集合操作的流程
- epoll描述符監控數量無上限
- 每個epoll監控的描述符事件信息,只需要向內核拷貝一次
- epoll_wait使用異步阻塞操作在內核中完成事件監控;事件監控是操作系統通過事件回調的方式就緒描述符事件信息添加到雙向鏈表中;而epoll_wait只是每隔一段時間看一下雙向鏈表是否為空判斷是否有描述符就緒(并非輪詢遍歷)性能不會隨著描述符增多而降低
- epoll直接通過epoll_wait傳入的時間結構數組向用戶返回就緒的事件信息;可以直接告訴用戶哪些描述符就緒,不需要用戶進行空遍歷查找
缺點
- 不能跨平臺
IO多路轉接模型的適用場景
對大量描述符進行監控,但是同一時間只有少量描述符活躍的場景