Epoll模型

Epoll模型

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 的大致流程为:

  1. 调用epoll_create创建一个 epoll 模型,获取该模型的句柄(本质也是一个文件描述符)

  2. 调用epoll_ctl,将需要监听的文件描述符添加到 epoll 模型中进行事件检测

  3. 在事件循环中调用epoll_wait接口,等待就绪的文件描述符

五、核心接口详细介绍
1.epoll_create

功能:创建一个 epoll 模型实例,返回该实例的句柄(文件描述符)。

说明:该函数的参数在新版本内核中已被弃用,传入一个大于 0 的数字即可。

函数原型:

#include <sys/epoll.h> int epoll_create(int size);

使用样例:

// 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。

函数原型:

#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成员中
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;

使用样例:

// 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。

函数原型:

#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

参数说明:

  • epfd:epoll 模型的句柄

  • events:存放就绪事件的输出缓冲区

  • maxevents:缓冲区最多能容纳的事件个数

  • timeout:超时时间,单位是毫秒

使用样例:

// 定义常量,表示就绪事件缓冲区的最大容量 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

#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

#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; };

Main.cc

#include <iostream> #include "EpollServer.hpp" int main() { std::unique_ptr<EpollServer> server=std::make_unique<EpollServer>(8080); server->Loop(); return 0; }

makefile

test_server:Main.cc g++ -g -o $@ $^ -std=c++14 .PHONY:clean clean: rm -f test_server