目录
1、epoll的接口
#include <sys/epoll.h>
1、int epoll_create(int size) :创建epoll模型
返回值是一个文件描述符,创建一个struct file结构体,指向epoll模型,返回的是在进程文件描述符表中的一个fd,通过这个fd就能找到epoll模型。
2、int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout)
:监听文件描述符上的事件并返回就绪的事件列表
是在关联就绪队列
作用:事件等待:
epoll_wait
可以让程序在不使用忙等待的情况下,高效地等待多个文件描述符上的 I/O 事件。通过设置合适的timeout
参数,程序可以根据需求进行阻塞等待或非阻塞等待。事件收集:一旦有文件描述符就绪,epoll_wait
会将这些就绪事件的详细信息填充到events
数组中。events
数组的每个元素是一个struct epoll_event
结构体,包含了事件的类型(如EPOLLIN
表示可读,EPOLLOUT
表示可写)以及与之关联的数据(通常是文件描述符)。函数返回值表示就绪事件的数量,通过遍历events
数组,程序可以获取每个就绪文件描述符的具体信息,并进行相应的处理 。高效处理大量并发连接:在处理大量并发连接的场景中,如网络服务器,epoll_wait
的优势尤为明显。相比于传统的select
和poll
函数,epoll
使用了事件驱动的机制,采用红黑树和链表的数据结构,使得在处理大量文件描述符时,其时间复杂度为 O (1),而select
和poll
的时间复杂度为 O (n)。这意味着epoll_wait
可以更高效地处理大量并发连接,减少 CPU 资源的消耗。第一个参数:epoll_create 的返回值
第二个:它是一个指向
struct epoll_event
结构体数组的指针。当epoll_wait
检测到有事件发生时,会将这些事件的相关信息填充到这个数组中。 每个struct epoll_event
结构体包含了事件的类型和与之关联的数据(通常是文件描述符)第三个:
events
数组的最大长度,也就是epoll_wait
最多能返回的事件数量第四个:
- 当
timeout
为-1
时,epoll_wait
会一直阻塞,直到有事件发生才会返回。- 当
timeout
为0
时,epoll_wait
会立即返回,即使没有事件发生。- 当
timeout
为一个正整数时,epoll_wait
会在该时间内阻塞等待事件发生,如果在超时时间内没有事件发生,函数将返回0
。返回值:已经就绪的fd的个数
3、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
:新增一个文件描述符,及修改其要关心的事件
是在对红黑树增删改
第一个:epoll_create 的返回值
第二个:选项
第三、四个:哪个文件描述符,上的哪些事件
struct epoll_event结构:
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;
n = epoll_ctl(_epfd,oper,sock,&ev);
event可以使用几个宏的集合:
2、epoll原理
OS在硬件层面,怎么知道网卡上有数据呢? 硬件中断
1、在内部维护一颗红黑树
struct rb_node
{
int fd;//内核要关心的fduint32_t event;// 要关心的事件
//链接字段
}
2、就绪队列(双链表)某个节点的事件就绪了,就将他链入就绪对列中。
struct list_node
{
int fd;//已经就绪的fduint32_t event; //已经就绪的事件
}
3、OS内部提供回调函数 底层网卡有数据就绪了,自动调用回调
1、向上交付 2、将数据交付给tcp的接收缓冲区 3、查找rb_tree, 看fd,并且看有没有关心EPOLLIN或者EPOLLOUT 4、如果有,构建就绪节点,插入到就绪队列中
用户只需要从就绪队列中获取就绪节点即可!!
上面三套就是epoll模型。
Epoller.hpp
#pragma once
#include "nocopy.hpp"
#include <cerrno>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include "Log.hpp"
class Epoller : public nocopy //类Epoller继承自nocopy类,类Epoller对象不能被拷贝和复值
{
static const int size = 128;
public:
Epoller()
{
_epfd = epoll_create(size); //创建epoll模型 返回一个文件描述符
if(_epfd == -1)
{
lg(Error,"epoll_create error: %s", strerror(errno));
}
else
{
lg(Info,"epoll_create success: %d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num) //传一个数组和数组大小就ok
{
int n = epoll_wait(_epfd,revents, num, _timeout);//等待epoll实例_epfd上的事件发生,将发生的事件存储在revents数组中,
//最多存储num个事件 ,超时事件为_timeout毫秒 返回事件发生的数量
//---解释---第二个参数是指向结构体数组的指针 当检测到有事件发生时,就会将事件的信息:fd、事件类型填充到结构体中
return n;
}
int EpollerUpdate(int oper,int sock,uint32_t event) //修改epoll实例上的文件描述符的监听事件
{
int n = 0;
if(oper == EPOLL_CTL_DEL)
{
n = epoll_ctl(_epfd, oper, sock, nullptr);//从epoll实例中删除指定文件描述符sock
if(n != 0)
{
lg(Error, "epoll_ctl delete error!");
}
}
else
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;
n = epoll_ctl(_epfd,oper,sock,&ev);//通过新增调用 向红黑树中新增节点 让节点和底层队列产生关联
if(n != 0)
{
lg(Error, "epoll_ctl error!");
}
}
return 0;
}
~Epoller()
{
if(_epfd >= 0)
close(_epfd);
}
private:
int _epfd;
int _timeout{3000};
};
EpollerServer.hpp
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <memory>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
class EpollServer : public nocopy
{
static const int num = 64;
public:
EpollServer(uint16_t port)
:_port(port),
_listsocket_ptr(new Sock()),
_epller_ptr(new Epoller())
{}
void Init()
{
_listsocket_ptr->Socket();//创建套接字
_listsocket_ptr->Bind(_port);//将套接字绑定到指定的端口
_listsocket_ptr->Listen();//将套接字设置为监听状态
lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
}
void Accepter() //处理新的客户端连接
{
//获取新连接
std::string clientip;
uint16_t clientport;
int sock = _listsocket_ptr->Accept(&clientip,&clientport);
if(sock > 0)
{
//不需要阻塞 因为这些是已经就绪的
//但是不能立马读 建立连接了并不代表有数据
//这个文件描述符上到底有没有新数据到来,只有epoll最清楚
_epller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
}
}
void Recver(int fd)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) -1);
if(n > 0)//读取成功
{
buffer[n] = 0; //在缓冲区末尾加字符串结束符
std::cout << "get a message:" << buffer << std::endl;
//write
std::string echo_str = "server echo $";
echo_str += buffer;
write(fd,echo_str.c_str(), echo_str.size());
}
else if(n == 0) //读取长度为0,客户端已经将连接关闭了
{
_epller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);//从epoll中移除时必须保证这是一个合法的文件描述符 所以先移除再关
close(fd);
}
else
{
_epller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);//从epoll中移除时必须保证这是一个合法的文件描述符 所以先移除再关
close(fd);
}
}
void Dispatcher(struct epoll_event revs[], int num) //传入的是所有已经就绪的文件描述符和对应的事件
{
for(int i = 0; i < num; i++)
{
uint32_t events = revs[i].events;
int fd = revs[i].data.fd; //拿到是哪个文件描述符就绪了
if(events & EVENT_IN) //当前事件是可读事件
{
//将监听套接字的文件描述符添加到 epoll 实例中,并关注其可读事件(EPOLLIN)。
//当有新的客户端连接请求到达时,监听套接字就会变得可读,
//epoll 会检测到这个事件并将其作为就绪事件返回给服务器
if(fd == _listsocket_ptr->Fd())//当前文件描述符是监听套接字的描述符,表示有新的客户端连接请求
{
//服务器根据文件描述符判断是监听套接字可读是,就知道有新连接到来
//当有新连接到来,监听套接字就会变的可读,epoll就会检测到监听套接字上的可读事件,并将其作为一个就绪事件返回给服务器
Accepter(); //处理新的客户端连接
}
else //普通客户端套接字有数据可读
{
//其他fd上面的普通读取事件就绪
Recver(fd); //处理客户端发送的数据
}
}
}
}
void Start()
{
//将listensock添加到epoll中, 就是listensock和他关心的事件,添加到内核epoll模型中的rb_tree(红黑树)中
_epller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
//要让epoll去等listen套接字 要让套接字增多
//epoll只负责在IO模型中进行等待
//---解释---调用Epoller类的EpollerUpdate方法,将监听套接字添加到epoll实例中,并关注可读事件
struct epoll_event revs[num];//---解释---存储epoll_wait函数返回的就绪事件
for(;;)
{
int n = _epller_ptr->EpollerWait(revs, num); //不断调用epoll_wait函数等待事件发生
if(n > 0)
{
//有事件就绪
Dispatcher(revs,n); //事件派发
}
else if(n == 0)
{
lg(Info, "time out ...");
}
else
{
lg(Error, "epll wait error");
}
}
}
~EpollServer()
{
_listsocket_ptr->Close();
}
private:
std::shared_ptr<Sock> _listsocket_ptr;
std::shared_ptr<Epoller> _epller_ptr;
uint16_t _port;
};
3、epoll工作方式
LT:水平触发Level Triggered
ET:边缘触发Edge Triggered : 数据/连接,从无到有,从有到多,变化的时候,才会通知我们一次。
epoll默认模式:LT模式(有事件到来,上层不取走,就一直通知你)
ET的通知效率高。不仅如此,ET的IO效率也更高。
边缘触发会倒逼程序员,在每次通知的时候把数据全取走,循环读取,直到读取出错。所有的fd必须是非阻塞的。当读取不成功就会出错,而不是一直阻塞等待。(面试题) 这意味着tcp会向对方通告一个更大的窗口,从而从概率上让对方一次给我发送更多的数据!!
其实LT也可以将所有的fd设置成为non_block,然后循环读取。在通知第一次的时候,就全部读取,不就和ET一样了。
向就绪队列里添加一次,还是一直添加(这就是ET和LT的区别)
上面的代码存在问题:read如何保证一次读完完整的报文。 没读完的数据在缓冲区中,如果想着下一次再去读,缓冲区已经被清空或者覆盖了。
下一节代码Reactor解决这个问题