

**前引:**在Linux系统的高并发领域,I/O处理效率直接决定了服务的性能上限。当我们面对每秒数万甚至数十万的连接请求时,传统的"一连接一线程"模型会因线程切换开销暴增而迅速崩溃,而早期的I/O多路转接技术如select和poll,也早已暴露出身法笨重的短板------select受限于FD_SETSIZE的1024文件描述符限制,poll虽突破了数量约束,却需在用户态与内核态间频繁拷贝事件数组,在高并发场景下性能损耗呈指数级上升!
目录
【一】epoll介绍
用来关心你设置的文件描述符:与select和epoll功能类似,但结构完全不同
【二】接口使用说明
epoll 的使用需要和前面的 select 与 poll 区别,它一般情况下由三个接口组成!如下讲解:
cpp
头文件都是:<sys/epoll.h>
【二】epoll模型
epoll 模型其实涉及到:红黑树+就绪队列+回调机制
**红黑树:**用来高效管理需要监听的文件描述符,节点通常是一个结构体类型
**就绪队列:**将已经就绪的文件事件放入就绪队列
**回调机制:**用来管理红黑树和运行队列,不用用户自己操作,只需要添加关心的文件描述符即可
比如:将网卡拿到的数据交给TCP运行队列;节点的增删...

【四】epoll_create
(1)函数原型
cpp
int epoll_create(int size);
(2)参数
说明:要监控的 fd 数量,系统会动态调整,可以任意传一个正数
(3)返回值
创建一个 epoll 模型,并返回 epoll 模型对应的文件描述符,理解返回值需要先看 epoll 模型
【五】epoll_ctl
(1)函数原型
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
(2)参数
(1)第一个参数
说明:epoll_create 创建的 epoll 模型文件描述符
(2)第二个参数
说明:三种操作选项
EPOLL_CTL_ADD:把 fd 加入监控列表(新安排任务)EPOLL_CTL_MOD:修改已监控 fd 的事件类型(调整任务)EPOLL_CTL_DEL:把 fd 从监控列表移除(取消任务)
(3)第三个参数
说明:要添加的目标文件描述符
(4)第四个参数
说明:epoll_event类型的结构体指针,对添加的文件描述符作说明
cpp
struct epoll_event
{
uint32_t events; // 要监控的事件类型(比如"能读了""能写了")
epoll_data_t data;// 关联数据(用来标识这个fd,方便后续处理)
};
// data是个联合体(可以存不同类型的数据):
typedef union epoll_data
{
void *ptr; // 指针(比如存fd对应的业务数据)
int fd; // 直接存fd本身(最常用)
uint32_t u32; // 32位整数
uint64_t u64; // 64位整数(比如位图标记)
} epoll_data_t;
events常用取值 :
EPOLLIN:fd 有数据可以读(比如 socket 收到了客户端消息)EPOLLOUT:fd 可以写数据(比如 socket 可以给客户端发消息了)EPOLLERR:fd 发生了错误- 可以用 "或运算" 组合(比如
EPOLLIN | EPOLLOUT表示同时监控读写)
(3)返回值
说明:返回0:说明事件添加成功;返回-1:errno
【六】epoll_wait
(1)函数原型
cpp
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
(2)参数
(1)第一个参数
说明:epoll 模型文件描述符(epoll_create的返回值)
(2)第二个参数
说明:epoll_event类型的数组,用来存储信息(即当有事件就绪,操作系统自动给你填)
cpp
struct epoll_event
{
uint32_t events; // 要监控的事件类型(比如"能读了""能写了")
epoll_data_t data;// 关联数据(用来标识这个fd,方便后续处理)
};
// data是个联合体(可以存不同类型的数据):
typedef union epoll_data
{
void *ptr; // 指针(比如存fd对应的业务数据)
int fd; // 直接存fd本身(最常用)
uint32_t u32; // 32位整数
uint64_t u64; // 64位整数(比如位图标记)
} epoll_data_t;
(3)第三个参数
说明:最多关心的事件个数(比如:应用层的我最多只关心2个,剩余事件下次会继续报告给你)
(4)第四个参数
说明:超时时间设置(毫秒)
-1:一直阻塞,直到有事件发生0:立即返回(不管有没有事件)- 正数:最多等这么久,没事件就超时返回
(3)返回值
说明:触发事件的 fd 数量
返回0:没有事件就绪
返回>0:事件就绪数量
返回-1:出错,设置errno
【七】epoll模型使用
如果有问题,可以私信我!"功能实现"中的思路和前面章节中的select思路讲解,一模一样!
(1)封装epoll接口
封装上面三个接口,这里没什么好说的。声明:里面的size是用来判断当前套接字个数的
cpp
#include"TCP_Network.h"
#define max_num_size 10
class Epoll
{
public:
//初始化结构体
void Install()
{
memset(events, 0, sizeof(events));
for(int i=0;i<max_num_size;i++)
{
events[i].data.fd=-1;
}
}
//创建epoll模型
const int Epoll_creat(int size)
{
if (size <= 0)
{
log_message(LOG_LEVEL_ERROR, __FILE__, __LINE__, "epoll_create参数非法:size必须>0");
exit(1);
}
//初始化结构体
Install();
int epfd = epoll_create(size);
if(epfd==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
exit(1);
}
return epfd;
}
//设置关心事件
const int Epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
{
//先检查size是否已满
if (op == EPOLL_CTL_ADD)
{
if (size >= max_num_size)
{
std::cout << "事件满了,添加失败" << std::endl;
return -1;
}
}
//先检查size是否为空
else if (op == EPOLL_CTL_DEL)
{
if (size <= 0)
{
std::cout << "事件删除失败" << std::endl;
return -1;
}
}
int ct = epoll_ctl(epfd, op, fd, event);
if(ct==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
exit(1);
}
//更新size
if (op == EPOLL_CTL_ADD)
{
size++;
}
else if (op == EPOLL_CTL_DEL)
{
size--;
}
return ct;
}
//收集就绪事件
const int Epoll_wait(int epfd, int maxevents, int timeout)
{
int wa = epoll_wait(epfd, events, maxevents, timeout);
if(wa==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误信息:%s",errno,strerror(errno));
}
return wa;
}
//返回信息
struct epoll_event* Message()
{
return events;
}
private:
//存储信息的结构体
struct epoll_event events[max_num_size];
//关心的事件个数
int size =0;
};
(2)功能实现
epoll与poll、select的功能实现思路都是一样的,只需要知道epoll的三个接口的功能:
epoll_create:创建一个epoll模型
epoll_ctl:对epoll模型进行操作(增删)
epoll_wait:返回就绪事件
核心思路:先创建--->再添加套接字--->事件就绪就进入任务函数--->listen套接字还是recv进行判断
cpp
#include "Epoll.h"
class Media
{
public:
void Inital()
{
_S.Socket();
_S.Bind();
_S.Listen();
}
void Recv(int fd, int epfd)
{
char buffer[1024] = {0};
ssize_t d = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (d > 0)
{
buffer[d] = 0;
std::cout << "客户端发送了数据:";
std::cout << buffer << std::endl;
}
else if (d == 0)
{
std::cout << "关闭了文件描述符:" << fd << std::endl;
// 对方断开了连接
_E.Epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
// 关闭当前的文件描述符
close(fd);
}
else
{
// 读取错误
_E.Epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
// 关闭当前的文件描述符
}
}
// 处理事件
void Handle(int epfd, int wa)
{
// 获取准备就绪的事件
struct epoll_event *events = _E.Message();
// 遍历事件
for (int i = 0; i < wa; i++)
{
if (events[i].data.fd == -1)
continue;
if (events[i].data.fd == _S.Fd())
{
std::cout<<"Accept"<<std::endl;
int d = _S.Accept();
// 添加读端事件
struct epoll_event v;
v.data.fd = d;
v.events = EPOLLIN;
int ctl = _E.Epoll_ctl(epfd, EPOLL_CTL_ADD, d, &v);
if (ctl == 0)
{
std::cout << "事件:" << d << "添加成功" << std::endl;
}
}
else if (events[i].events == EPOLLIN) // 读取数据
{
std::cout<<"Recv"<<std::endl;
Recv(events[i].data.fd, epfd);
}
// 写端
// 监听
}
}
// 关心事件
void Install()
{
// 创建epoll模型
const int epfd = _E.Epoll_creat(max_num_size);
// 将listen套接字添加进epoll模型
int fd = _S.Fd();
struct epoll_event v;
v.data.fd = fd;
v.events = EPOLLIN;
int ctl = _E.Epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &v);
if (ctl == 0)
{
std::cout << "事件:" << fd << "添加成功" << std::endl;
}
for (;;)
{
// 等待事件
int wa = _E.Epoll_wait(epfd, max_num_size, 2000);
switch (wa)
{
case 0:
{
std::cout << "暂时没有事件....HHHHHHH" << std::endl;
continue;
}
case -1:
{
if (errno != EINTR)
{
std::cout << "Epoll_wait is fatal errno" << std::endl;
}
break;
}
default:
{
Handle(epfd, wa);
}
}
}
}
private:
Server _S;
Epoll _E;
};
【八】epoll的两种工作模式
(1)LT模式(默认)
说明:只要目标套接字(fd)处于 "有事件可处理" 的状态,就会一直通知你
特点:安全、逻辑简单、但是重复提醒同一个套接字效率低,不强制搭配非阻塞IO
(2)ET模式
说明:只要目标套接字(fd)状态发送变化时,才会通知你,且只通知一次
特点:只通知一次,减少不必要的触发,效率极高,但是需要一次性读完数据,必须搭配非阻塞IO
