Epoll模型
一、底层核心原理
每一个epoll模型内部都维护了一棵红黑树,当调用epoll_ctl并执行EPOLL_CTL_ADD操作时,会将新增的文件描述符(fd)及关联数据构造成一个新结点,存入这棵红黑树中。此外epoll模型中还维护了一个就绪队列 :当文件描述符上有事件就绪时,底层会触发中断,操作系统会将该文件描述符对应的红黑树结点放入就绪队列中。当执行epoll_wait时,就能直接拿到所有已经就绪的事件对应的fd,由程序员自行处理。
二、为什么用红黑树存储 fd 数据?
因为epoll模型可监测的 fd 数量没有硬上限,当存在大量 fd 时,使用红黑树既可以对 fd 进行高效管理,也能实现快速查找与增删操作。
三、事件结点如何加入就绪队列?
红黑树的每一个结点中,存储的不只有 fd,还包含其他指针字段。当某个结点有事件就绪时,不需要重新拷贝整个结点再添加到就绪队列,只需要通过结点内的就绪队列指针,将结点链入就绪队列即可(头插或尾插方式),全程无需拷贝结点数据。
四、epoll IO 多路转接的整体使用逻辑
epoll 是 IO 多路转接模型的一种实现,相比 select/poll 效率更高。
使用 epoll 的大致流程为:
-
调用
epoll_create创建一个 epoll 模型,获取该模型的句柄(本质也是一个文件描述符) -
调用
epoll_ctl,将需要监听的文件描述符添加到 epoll 模型中进行事件检测 -
在事件循环中调用
epoll_wait接口,等待就绪的文件描述符
五、核心接口详细介绍
1. epoll_create
功能:创建一个 epoll 模型实例,返回该实例的句柄(文件描述符)。
说明:该函数的参数在新版本内核中已被弃用,传入一个大于 0 的数字即可。
函数原型:
c++
#include <sys/epoll.h>
int epoll_create(int size);
使用样例:
c++
// 1.创建epoll模型
_epfd = epoll_create(128);
if(_epfd < 0)
{
std::cout << "epoll create error, " << strerror(errno) << std::endl;
exit(-1);
}
2. epoll_ctl
功能:通过 epoll 句柄,对被监听的文件描述符执行增、删、改操作。成功返回 0,失败返回 - 1。
函数原型:
c++
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
-
epfd:epoll 模型的句柄 -
op:操作类型-
EPOLL_CTL_ADD:将 fd 添加到 epoll 模型中进行监听 -
EPOLL_CTL_MOD:修改 fd 对应的监听事件 -
EPOLL_CTL_DEL:将 fd 从 epoll 模型中移除
-
-
fd:目标文件描述符 -
event:需要监听的事件结构体
其中epoll_event结构体定义如下:
events字段:用于设置需要监听的事件,常用的有EPOLLIN(可读事件)、EPOLLOUT(可写事件)data字段:用户自定义数据,一般将 fd 存入该字段的fd成员中
c++
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
使用样例:
c++
// 2.将listen套接字加入到epoll模型中
// 先构建一个事件结构体
struct epoll_event ev;
ev.events |= EPOLLIN;
ev.data.fd = _socket->GetSockfd();
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _socket->GetSockfd(), &ev); // 成功返回0
if(n != 0)
{
std::cout << "epoll control error, " << strerror(errno) << std::endl;
exit(-1);
}
3. epoll_wait
功能:检查 epoll 模型中是否有文件描述符的事件就绪。如果有就绪事件,会将这些就绪的 epoll 事件放到events缓冲区中;成功时返回就绪事件的个数,超时返回 0,出错返回 - 1。
函数原型:
c++
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数说明:
-
epfd:epoll 模型的句柄 -
events:存放就绪事件的输出缓冲区 -
maxevents:缓冲区最多能容纳的事件个数 -
timeout:超时时间,单位是毫秒
使用样例:
c++
// 定义常量,表示就绪事件缓冲区的最大容量
const static int gnum = 64;
// 定义存放就绪事件的缓冲区
struct epoll_event _events[gnum];
// 调用epoll_wait进行等待,超过1秒就会返回,继续向下执行
int n = epoll_wait(_epfd, _events, gnum, 1000);
if(n == 0)
{
// 没有事件就绪,超时返回
std::cout << "time out..." << std::endl;
}
else if(n > 0)
{
// 有n个事件就绪,开始处理事件
HandlerEvents(n);
}
else
{
if(errno == EINTR)
continue;
std::cout << "epoll wait fail, " << strerror(errno) << std::endl;
exit(-1);
}
代码样例------EpollServerDemo
EpollServer.hpp
c++
#pragma once
#include <iostream>
#include <memory>
#include "TcpSocket.hpp"
#include <unordered_map>
#include <unistd.h>
#include <sys/epoll.h>
class EpollServer
{
private:
const static int N=sizeof(fd_set)*8;
const static int defaultfd=-1;
const static int gnum=64;
public:
EpollServer(uint16_t port)
:_port(port)
,_socket(std::make_unique<TcpSocket>(port))
,_epfd(defaultfd)
{
// 1.创建epoll模型
_epfd=epoll_create(128);
if(_epfd<0)
{
std::cout<<"epoll create error, "<<strerror(errno)<<std::endl;
exit(-1);
}
// 2.将listen套接字加入到epoll模型中
// 先构建一个事件结构体
struct epoll_event ev;
ev.events|=EPOLLIN;
ev.data.fd=_socket->GetSockfd();
int n =epoll_ctl(_epfd, EPOLL_CTL_ADD, _socket->GetSockfd(), &ev); // 成功返回0
if(n!=0)
{
std::cout<<"epoll control error, "<<strerror(errno)<<std::endl;
exit(-1);
}
}
void HandlerEvents(int num)
{
std::cout<<"有Num:"<<num<<"个事件就绪了"<<std::endl;
}
void Loop()
{
while(true)
{
int n=epoll_wait(_epfd, _events, gnum, 1000);
if(n==0)
{
// 事件没有就绪
std::cout<<"time out..."<<std::endl;
}
else if(n>0)
{
// 有n个事件就绪了
HandlerEvents(n);
}
else
{
if(errno==EINTR)
continue;
std::cout<<"epoll wait fail, "<<strerror(errno)<<std::endl;
exit(-1);
}
}
}
private:
int _epfd;
uint16_t _port;
std::unique_ptr<TcpSocket> _socket;
struct epoll_event _events[gnum];
};
TcpSocket.hpp
c++
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
class InetAddr
{
public:
InetAddr()
{}
InetAddr(const std::string&ip, uint16_t port)
:_ip(ip)
,_port(port)
{
memset(&_sockaddr, 0, sizeof(_sockaddr));
_sockaddr.sin_family=AF_INET;
_sockaddr.sin_port=htons(port);
_sockaddr.sin_addr.s_addr=inet_addr(_ip.c_str());
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
// 将addr里面的数据,拷贝给自己
void GetInetAddr(sockaddr_in*addr)
{
memcpy(&_sockaddr, addr, sizeof(*addr));
_port=ntohs(addr->sin_port);
_ip=inet_ntoa(addr->sin_addr);
}
private:
struct sockaddr_in _sockaddr;
std::string _ip;
uint16_t _port;
};
class TcpSocket
{
public:
TcpSocket(uint16_t port)
:_port(port)
{
_listensocket=socket(AF_INET, SOCK_STREAM, 0);
if(_listensocket<0)
{
std::cout<<"监听套接字失败"<<std::endl;
exit(-1);
}
// 设置地址复用
int opt=1;
setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 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;
int n=bind(_listensocket, (struct sockaddr*)&local, sizeof(local));
if(n<0)
{
std::cout<<"绑定失败"<<std::endl;
exit(-1);
}
// 开启监听
n=::listen(_listensocket, _defaultBacklog);
if(n<0)
{
std::cout<<"TCP监听失败"<<std::endl;
exit(-1);
}
}
int Accept(InetAddr*addr)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sockfd=accept(_listensocket, (struct sockaddr*)&peer, &len);
addr->GetInetAddr(&peer);
return sockfd;
}
int GetSockfd()
{
return _listensocket;
}
private:
uint16_t _port;
int _listensocket;
// 类内常量,所有类对象共享一份
const static int _defaultBacklog=16;
};
c++
#include <iostream>
#include "EpollServer.hpp"
int main()
{
std::unique_ptr<EpollServer> server=std::make_unique<EpollServer>(8080);
server->Loop();
return 0;
}
makefile
c++
test_server:Main.cc
g++ -g -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -f test_server