Linux網絡:多路轉接 epoll
- 一、epoll三個接口函數
- 1、epoll_create
- 2、epoll_ctl
- 3、epoll_wait
- 二、epoll的工作原理
- 三、epoll的echo_server
- 1、EpollServer類
- 2、構造函數
- 3、事件循環
- 4、事件派發
- 5、事件處理
- 6、測試
- 四、LT和ET模式
- 1、LT
- 2、ET
- 五、項目代碼
一、epoll三個接口函數
多路轉接是非常高效的一種IO模型,它可以在同一時間等待多個套接字,從而提高效率。Linux提供了三種系統調用實現多路轉接:select、poll、epoll
。本博客講解epoll。
epoll是經過改進的poll
,在Linux 2.5.44版本引入內核,并認為是Linux2.6最好的多路轉接實現方案
1、epoll_create
epoll_create
用于創建
一個epoll模型,需要頭文件<sys/epoll.h>,函數原型如下:
int epoll_create(int size);
此處的參數size已經被廢棄,可以填入大于0的任何值。
返回值是一個文件描述符,通過這個文件描述符,可以操控Linux底層創建的epoll(主要是那兩個模型)
2、epoll_ctl
epoll_ctl
用于控制
epoll模型,需要頭文件<sys/epoll.h>,函數原型如下:
int epoll_ctl(int epfd, int op, int fd,struct epoll_event *_Nullable event);
參數:
epfd
:通過epoll_create
獲取到的文件描述符op
:本次執行的操作,傳入宏fd
:要監聽的文件的文件描述符event
:對文件要執行的監聽類型
其中:op操作包括
EPOLL_CTL_ADD
:新增
一個文件描述符到epoll中EPOLL_CTL_MOD
:修改
一個epoll中的文件描述符EPOLL_CTL_DEL
:從epoll中刪除
一個文件描述符
其中event的類型是struct epoll_event*
,該結構體定義如下:
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
};
這個結構體中,包含events和data兩個字段:
events
:一個位圖,存儲要監聽的事件以及一些其它配置data
:當epoll返回時,攜帶的數據
此處的data
是一個聯合體
,它可以存儲四種類型
的數據:ptr指針,int文件描述符,uint32_t和uint64_t的整型。
其中events包括:
EPOLLIN
:監聽讀事件- EPOLLOUT:監聽寫事件
- EPOLLERR:監聽錯誤事件
- EPOLLHUP:文件描述符被關閉
- EPOLLONESHOT:只監聽一次事件,本次監聽完畢,文件描述符被從epoll中移除
當一個epoll返回已經就緒的文件時,用戶其實無法得知這個文件的描述符,那么就可以通過這個data.fd獲取到文件描述符,當然也可以通過其它的參數,傳遞更復雜的信息。
3、epoll_wait
epoll_wait
用于等待
epoll模型中的文件就緒,需要頭文件<sys/epoll.h>,函數原型如下:
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
參數:
epfd
:通過epoll_create獲取到的文件描述符events
:輸出型參數,指向一個epoll_event就緒數組
,獲取之前通過epoll_ctl傳入的eventsmaxevents
:用戶傳入的events數組的最大長度(就緒事件個數
)timeout
:超時時間,以ms為單位
此處用戶要傳入一個epoll_event
數組,這個數組用于存儲本次就緒的所有文件的epoll_event,為了防止越界,所以還要傳入maxevents。
就是說,epoll
的使用方式是通過epoll_wait
獲取就緒的文件,這些文件存儲到epoll_event數組
中。
函數返回,用戶可以遍歷數組,獲取到所有就緒的文件的epoll_event結構體。
這個結構體是在epoll_ctl
時傳入的,從它的events字段可以得知這個文件監聽的事件,從data字段可以獲取之前預設的其他信息,一般會預設data.fd
獲取這個文件的描述符
。
返回值:
0:超時
,指定時間內沒有文件就緒<0
:出現錯誤>0
:就緒的文件的個數
通過此處,已經可以看出epoll
相比于select的優勢
了:
epoll返回時,把已經就緒的文件
放到數組
中,后續遍歷數組,每一個元素
都是已經就緒的文件
在select中,就緒的事件通過一張位圖返回,用戶需要遍歷整個位圖所有元素,并判斷該元素是否就緒,那么就會浪費大量的時間在未就緒的文件上。
epoll返回時
,不會把已經加入epoll的文件刪除,而是繼續監聽該文件
這是另一大優勢,在select中,每次返回都會重置用戶傳入的位圖,因此用戶在每次輪詢都要重新把文件描述符設置到select。
當然,用戶也可以在epoll_ctl的時候,設置EPOLLONESHOT
,那么這個文件被epoll返回后
,就會從epoll中刪除
,也就是只監聽一次事件
。
二、epoll的工作原理
Epoll的工作原理:
一種特殊的數據結構:
雙層結構體:結構體嵌套結構體
三、epoll的echo_server
接下來使用epoll系統調用
,實現一個簡單的echo server
。
1、EpollServer類
// 多路轉接:事件循環、事件派發、事件處理!
class EpollServer
{
public:// 構造函數EpollServer(uint16_t port){}// 初始化函數void InitServer(){}// 轉換字符串std::string EventsToString(uint32_t events)// 事件處理:網絡套接字文件void Accepter(){}// 事件處理:普通文件void HandlerIO(int fd){}// 事件派發void HandlerEvent(int n){}// 事件循環void Loop(){}// 析構~EpollServer(){}
private:uint16_t _port;std::unique_ptr<Socket> _listensock;int _epfd; // epoll_create的返回值!epoll句柄struct epoll_event revs[num]; // 將內核epoll模型里面的就緒事件,存入revs(revs是epoll_wait的參數)
};
2、構造函數
構造函數代碼如下:
// 構造函數EpollServer(uint16_t port): _port(port), _listensock(std::make_unique<TcpSocket>()){_listensock->BuildListenSocket(port); // 根據傳來的port來創建監聽套接字!!!// 1、epoll_create創建成功,說明底層內核已經創建好了:紅黑樹+就緒隊列_epfd = ::epoll_create(size);// 這里的參數size>0即可if (_epfd < 0){LOG(FATAL, "epoll create fail\n");exit(-1);}LOG(INFO, "epoll create sucess ,epfd:%d\n", _epfd);}
3、事件循環
事件循環代碼如下:
開啟循環后,進入一個while(true)
死循環,每一輪循環通過epoll_wait
獲取本輪循環就緒的文件:
// 事件循環void Loop(){int timeout = 1000; // 設置時限!1swhile (true){int n = ::epoll_wait(_epfd, revs, num, timeout); // epoll_wait的返回值就是就緒事件的個數switch (n){case 0:LOG(INFO, "epoll time out...\n");break;case -1:LOG(ERROR, "epoll wait fail\n");break;default:LOG(INFO, "have event happend! n:%d\n", n);HandlerEvent(n);// 事件派發break;}}}
4、事件派發
事件派發就是判斷文件描述符是_listenfd還是普通的sockfd,調用不同的函數進行處理。
// 事件派發void HandlerEvent(int n){for (int i = 0; i < n; i++){int fd = revs[i].data.fd;uint32_t revents = revs[i].events; // 具體是哪一個fd里面的什么事件就緒了!?LOG(INFO, "%d 上面的有事件就緒了,具體事件是:%s\n", fd, EventsToString(revents).c_str());// 事件處理:封裝!// 1、listensock就緒if (fd = _listensock->Sockfd()){Accepter();}// 2、處理普通fdelse{HandlerIO(fd);}}}
5、事件處理
void Accepter(){InetAddr addr;int sockfd = _listensock->Accepter(&addr);if (sockfd < 0){LOG(ERROR, "獲取連接失敗\n");return ;}LOG(INFO, "得到一個新鏈接:%d,客戶端信息:%s:%d\n", sockfd, addr.Ip().c_str(), addr.Port());struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;// 必須將sockfd加入到epoll里面,這樣才能讓epoll對提取出來的sockfd進行處理!::epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);LOG(INFO, "add sucess to epoll,new sockfd:%d\n", sockfd);}void HandlerIO(int fd){char buffer[4096];int n = ::recv(fd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;std::cout << buffer;}else if (n == 0){LOG(INFO, "client,quit...\n");// 先將這個退出的fd從epoll中移除,再關閉fd::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);::close(fd);}else{LOG(ERROR, "client,fail...\n");::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);::close(fd);}}
6、測試
四、LT和ET模式
思考一個問題:如果用戶通過epoll
檢測到某個socket的事件已經就緒了,但是這個用戶沒有處理這個事情,下一次epoll_wait
還要不要返回這一個事件?
就是基于這個問題,衍生了兩種epoll
工作模式:LT模式與ET模式
1、LT
LT模式
下,當用戶沒有處理事件,那么事件就一直保留在就緒鏈表rdlink中,每次調用epoll_wait都會返回這個事件
這種模式是epoll的默認模式。
用戶接收到事件后,可能某個報文太長了,一次讀不完。那么LT模式下一次還會進行通知,用戶可以把剩下的報文讀完。但是這就可能導致一個報文,需要調用更多次的epoll_wait。
2、ET
ET模式
下,當用戶通過epoll_wait
拿到事件后,事件直接從rdlink
中刪除,下一次不再進行通知
這種模式比LT更加高效,這可以從兩個角度解讀:
- 這種模式下,一個報文只需要調用一次epolll_wait,因此效率高一點
- 這倒逼程序員必須一次性把報文讀完,那么就會更快的進行業務處理,報文響應速度也更快
這里主要是第二點比較重要,當一個報文太長了,但是ET模式下只進行一次通知。那么程序員收到通知后,就需要用一個while循環一直讀取套接字,直到讀不出數據為止。這樣一次通知程序員就能拿到完整報文,進而更早的進行業務處理,更早響應。而且提早把數據讀走,內核的緩沖區也會被空出來,接收更多的新數據。
默認情況下,從文件讀取文件是阻塞的,當最后一次while循環讀取不出內容了,程序就會阻塞住。因此這種情況下,要把文件讀取改為非阻塞讀取,如果讀不出內容直接返回。
但是這也導致ET的程序
會比LT更加復雜
,實際開發中需要進行權衡。
五、項目代碼
epoll和select代碼