优化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;
};