云计算百科
云计算领域专业知识百科平台

《epoll深度解析:从原理到使用,解锁Linux高并发I/O的核心能力(终篇)》

前引:在Linux系统的高并发领域,I/O处理效率直接决定了服务的性能上限。当我们面对每秒数万甚至数十万的连接请求时,传统的“一连接一线程”模型会因线程切换开销暴增而迅速崩溃,而早期的I/O多路转接技术如select和poll,也早已暴露出身法笨重的短板——select受限于FD_SETSIZE的1024文件描述符限制,poll虽突破了数量约束,却需在用户态与内核态间频繁拷贝事件数组,在高并发场景下性能损耗呈指数级上升!

目录

【一】epoll介绍

【二】接口使用说明

【二】epoll模型

【四】epoll_create

(1)函数原型

(2)参数

(3)返回值

【五】epoll_ctl

(1)函数原型

(2)参数

(1)第一个参数

(2)第二个参数

(3)第三个参数

(4)第四个参数

(3)返回值

【六】epoll_wait

(1)函数原型

(2)参数

(1)第一个参数

(2)第二个参数

(3)第三个参数

(4)第四个参数

(3)返回值

【七】epoll模型使用

(1)封装epoll接口

(2)功能实现

【八】epoll的两种工作模式

(1)LT模式(默认)

(2)ET模式


【一】epoll介绍

用来关心你设置的文件描述符:与select和epoll功能类似,但结构完全不同

【二】接口使用说明

epoll 的使用需要和前面的 select 与 poll 区别,它一般情况下由三个接口组成!如下讲解:

头文件都是:<sys/epoll.h>

【二】epoll模型

epoll 模型其实涉及到:红黑树+就绪队列+回调机制

红黑树:用来高效管理需要监听的文件描述符,节点通常是一个结构体类型

就绪队列:将已经就绪的文件事件放入就绪队列

回调机制:用来管理红黑树和运行队列,不用用户自己操作,只需要添加关心的文件描述符即可

                  比如:将网卡拿到的数据交给TCP运行队列;节点的增删…

【四】epoll_create

(1)函数原型

int epoll_create(int size);

(2)参数

说明:要监控的 fd 数量,系统会动态调整,可以任意传一个正数

(3)返回值

创建一个 epoll 模型,并返回 epoll 模型对应的文件描述符,理解返回值需要先看 epoll 模型

【五】epoll_ctl

(1)函数原型

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类型的结构体指针,对添加的文件描述符作说明

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)函数原型

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

(2)参数
(1)第一个参数

说明:epoll 模型文件描述符(epoll_create的返回值)

(2)第二个参数

说明:epoll_event类型的数组,用来存储信息(即当有事件就绪,操作系统自动给你填)

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是用来判断当前套接字个数的

#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进行判断

#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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 《epoll深度解析:从原理到使用,解锁Linux高并发I/O的核心能力(终篇)》
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!