優化poll進行拷貝的開銷
poll開銷過大
- 將整個 pollfd 數組拷貝到內核態,以便內核檢查 fd 是否就緒(從用戶態 → 內核態)。
- 內核檢查 fd 狀態,并填充 revents。
- 將 pollfd 數組從內核態拷貝回用戶態,讓應用程序可以讀取 revents 里的就緒狀態(從內核態 → 用戶態)。
這個拷貝過程涉及 所有 pollfd 結構體,而 pollfd 結構體的大小通常是 8~16字節(取決于架構),如果監聽 fd 數量很大(例如 上千個),拷貝數據量會很大,導致 系統調用的開銷上升。
優化遍歷開銷
- 優化 poll 遍歷查詢空位置的問題:poll 需要遍歷整個 pollfd 數組來找到空閑位置,以便管理 fd,當 fd 數量龐大時,維護成本很高。
- 優化遍歷查詢就緒 fd 的問題:poll 在返回時,仍然需要遍歷整個 pollfd 數組來尋找就緒的 fd,時間復雜度為 O(n)。
epoll 的優化點(epoll_ctl + epoll_wait 分離)
操作 | 說明 |
---|---|
epoll_ctl | 只在你要添加/修改/刪除監聽 fd 時才調用一次,相當于“注冊監聽列表” |
epoll_wait | 每次只等待事件,不關心你監聽了哪些 fd,內核直接返回“就緒事件” |
認識epoll的接口
poll 和select 都通過一個接口完成“注冊 + 等待”,每次調用都要傳遞和檢查全部 fd,效率低;
而epoll 將監聽與等待分離,通過epoll_ctl 注冊事件,通過epoll_wait 等待事件,減少了不必要的遍歷和數據拷貝
epoll_create()
作用
創建一個 epoll 實例,并返回一個 epoll 文件描述符(epfd),該 epfd 用于后續的 epoll_ctl() 和 epoll_wait() 操作。
函數原型
int epoll_create(int size);這個參數已經進行廢棄了,但是為了進行向前兼容沒有將該參數進行刪除。
返回值
- 成功:返回 epfd(epoll 實例的文件描述符)。
- 失敗:返回 -1,errno 指示錯誤類型。
epoll_ctl()
作用
管理 epoll 監控的文件描述符,對epoll模型進行操作,包括添加、修改、刪除操作。
函數原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數
- epfd:由 epoll_create() 生成的 epoll 句柄。
- op:操作類型,支持以下三種:
????????EPOLL_CTL_ADD:添加一個新的 FD 到 epoll 實例。
????????EPOLL_CTL_MOD:修改已有的 FD 監聽的事件。
????????EPOLL_CTL_DEL:從 epoll 實例中刪除 FD。
- fd:需要監聽的文件描述符。
- event:指向 struct epoll_event 結構體的指針,設置監聽的事件類型。
返回值
- 成功:返回 0。
- 失敗:返回 -1,errno 指示錯誤類型。
epoll_wait()
作用
等待被監聽的文件描述符發生事件,并返回就緒的文件描述符。
函數原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數
- epfd:epoll_create() 生成的 epoll 句柄。
- events:用于存儲發生事件的文件描述符,用戶需提供足夠的空間。
- maxevents:events 數組的大小,建議大于 0。
- timeout:超時時間(毫秒):
????????0:立即返回(非阻塞)。
????????-1:永遠阻塞,直到有事件發生。????????
????????>0:等待指定時間。
返回值
- 成功:返回就緒的文件描述符數量(nfds)。
- 失敗:返回 -1,errno 指示錯誤類型。
epoll的原理?
epoll服務器實現的框架
socket套接編程的封裝
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "err.hpp"class Sock
{const static int backlog = 32;public:static int Socket(){// 1. 創建socket文件套接字對象int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", sock);int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));return sock;}static void Bind(int sock, int port){// 2. bind綁定自己的網絡信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}static void Listen(int sock){// 3. 設置socket 為監聽狀態if (listen(sock, backlog) < 0) // 第二個參數backlog后面在填這個坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}static int Accept(int listensock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr *)&peer, &len);if (sock < 0)logMessage(ERROR, "accept error, next");else{logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}
};
epoll模型的封裝
epollServer.hpp
#include <iostream>
#include<unistd.h>
#include<sys/epoll.h>
#include<functional>
#include<string>
#include"sock.hpp"
#include"log.hpp"
using namespace std;using func_t = function<string(const string&)>;
#define SIZE 1024const static int defultvalue=-1;
const static int defultnum=64;
class EpollServer
{
public:EpollServer(int port,func_t func,int num=defultnum):_listen_sockfd(defultvalue),_port(port),_num(num),_revs(nullptr),_func(func){}void serverInit(){//1、創建套接字_listen_sockfd=Sock::Socket();//2、進行bind綁定Sock::Bind(_listen_sockfd,_port);//3、設置監聽狀態Sock::Listen(_listen_sockfd);//4、創建epoll模型//4、1 創建epoll模型實例_epfd=epoll_create(1);if(_epfd<0){logMessage(FATAL,"創建epoll實例失敗 :%s",strerror(errno));exit(EPOLL_CREATE_ERROR);}//4、2 管理epoll進行監控的文件描述符struct epoll_event events;events.events=EPOLLIN;events.data.fd=_listen_sockfd;epoll_ctl(_epfd,EPOLL_CTL_ADD,_listen_sockfd,&events);//5、進行開辟就是事件的空間_revs=new struct epoll_event[_num];logMessage(NORMAL,"進行epoll服務器初始化成功");}void serverStart(){int timeout=-1;for(;;){// 進行等待管理的文件描述符發生事件int n = epoll_wait(_epfd,_revs,_num,timeout);switch (n){case -1:logMessage(ERROR,"epoll_wait等待文件描述符發生事件失敗");break;case 0:logMessage(NORMAL,"outtime....");break;default://一定有事件進行就緒HandlerEven(n); //將有多少個就緒的事件進行傳入break;}}}void HandlerEven(int readyNum){for(int i=0;i<readyNum;i++){int sockfd=_revs[i].data.fd;uint32_t events=_revs[i].events;//處理監聽套接字---listen套接字就緒if(sockfd==_listen_sockfd && events&EPOLLIN){string clienip;uint16_t port;int fd=Sock::Accept(sockfd,&clienip,&port);if(fd<0){logMessage(FATAL,"accept 獲取鏈接失敗");continue;;}//建立連接成功,我們可以直接進行讀取嗎??????? 不可以!!!!//交給epollstruct epoll_event ev;ev.events=EPOLLIN;ev.data.fd=fd;epoll_ctl(_epfd,EPOLL_CTL_ADD,fd,&ev);}//處理普通套接字---普通套接字就緒else if(events&EPOLLIN){//進行讀取客戶端的消息char buffer[SIZE];ssize_t n=recv(sockfd,buffer,sizeof(buffer)-1,0);if(n>0){buffer[n]=0;logMessage(NORMAL,"client# %s",buffer);//通過回調方法進行處理客戶端的消息string resp=_func(buffer);//將處理過后的消息進行返回send(sockfd,resp.c_str(),resp.size(),0);}else if(n==0){epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr);close(sockfd);logMessage(NORMAL,"客戶端退出");}else{//細節:epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr);close(sockfd);logMessage(ERROR,"進行讀取客戶端消息失敗");}}else{}}}~EpollServer(){if(_listen_sockfd!=defultvalue){close(_listen_sockfd);}if(_epfd!=defultvalue){close(_epfd);}if(_revs){delete[] _revs;}}
private:int _listen_sockfd;int _port;int _epfd;struct epoll_event* _revs;int _num;func_t _func;
};