epoll是改进的poll,但底层毫无关联
epoll的核心定位:基于对多个fd等待的就绪时间通知机制
1.快速认识epoll的接口

接口也分离了,epoll_ctl只做内核告诉用户,epoll_wait只做用户告诉内核
2.epoll的原理
a.示意图


为什么 epoll_creat的返回值是文件描述符?epoll_ctl、epoll_wait 的第一个参数是该文件描述符?
如何和文件系统关联上?
怎么能通过文件描述符fd 找到epoll模型?
b.内核源码

回调机制怎么做的?

回调机制什么时候被激活的?

epoll_ctl:1.在红黑树中 插入节点 2.向底层回调机制,注册回调方法,每注册一个 红黑树节点,还要注册一个回调节点
为什么select和poll慢?

3.编写epoll server demo

当前我们的代码和select、poll差不多
至于为什么epoll_event中还要有epoll_data_t,是因为要把 fd和 对应的事件关联起来,维护的是用户 的数据;而之前设置的 fd,是内核维护的数据,是红黑树节点的 key值!

epoll server也是单进程,处理了多个IO请求!
a.epoll的优点:
- 拆了三个函数,接口方便
- 数据拷贝轻量化,通过EPOLL_CTL_ADD,将要关心的 fd拷贝到内核,之后我们直接通过wait将数据拷贝到用户就可以;而select 和 poll,每次调用,都需要将关心的事件 拷贝给内核,有事件就绪还要再拷贝给用户。
- 事件回调,避免再内核中遍历所有fd,而是直接将就绪的 fd,加入到就绪队列中,检测是否有事件就绪O(1)。
- 可以同时监测的文件描述符没有上限
b.epoll的缺点:认为没有
demo
cpp
#include <iostream>
#include <unistd.h>
#include "Socket.hpp"
#include "Log.hpp"
#include <sys/epoll.h>
using namespace SocketModule;
using namespace LogModule;
class EpollServer
{
const static int size = 64;
const static int defaultfd = -1;
public:
EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
{
// 1.创建listen套接字
_listensock->BulidListenSocket(port); // 3
// 2.创建epoll模型
_epfd = epoll_create(256);
if (_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create error";
exit(EPOLL_CREATE_ERR);
}
LOG(LogLevel::INFO) << "epoll_create_success: " << _epfd; // 4
// 3.让 epoll 关心 listensockfd
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->Fd(); // todo
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev); // 新增了节点
if (n < 0)
{
LOG(LogLevel::FATAL) << "add listensockfd error";
exit(EPOLL_CTL_ERR);
}
}
void Start()
{
_isrunning = true;
int timeout = -1;
while (_isrunning)
{
// 不能直接accept
struct epoll_event ev;
// 获取就绪队列中的事件
int n = epoll_wait(_epfd, _revs, size, timeout);
switch (n)
{
case 0:
LOG(LogLevel::DEBUG) << "timeout ";
break;
case -1:
LOG(LogLevel::ERROR) << "epoll error";
break;
default: // 就绪事件放在数组里了
Dispatcher(n); // 有n个就绪了
break;
}
}
_isrunning = false;
}
// 有新连接到来,或者 客户端发数据了!
// 事件派发器!
void Dispatcher(int rnum) // LT模式,没一次性全拿走,就一直通知,水平触发模式,默认行为
{
LOG(LogLevel::DEBUG) << "event ready";
// n个就绪事件,都在那个数组里!依次拷贝的,从下标0 开始!
for (int i = 0; i < rnum; i++)
{
int sockfd = _revs[i].data.fd; // 哪个fd就绪
uint32_t revent = _revs[i].events; // 这个fd上 哪个事件就绪
if (revent & EPOLLIN) // 读事件就绪
{
// 可能是 listensockfd就绪,可能是 普通fd就绪
// 1.listen 就绪
if (sockfd == _listensock->Fd())
{
Accepter(); // 获取新连接
}
else // 2.普通fd 就绪
{
Recver(sockfd);
}
}
if (revent & EPOLLOUT) // 写事件就绪
{
}
}
}
// 连接管理器!
void Accepter()
{
InetAddr client;
// 至少有一个连接到来,accept一次,绝对不会阻塞!
int fd = _listensock->Accept(&client); // 这回accept不会阻塞,select已经解决了等 的问题
if (fd >= 0)
{
LOG(LogLevel::INFO) << "get a new link success,sockfd:"
<< fd << ",client is" << client.StringAddr();
// 新的fd,让epoll关心!
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
if (n < 0)
{
LOG(LogLevel::WARNING) << "add 普通fd error";
}
else
{
LOG(LogLevel::INFO) << "add 普通fd success,fd: " << fd;
}
}
}
// IO处理器
void Recver(int sockfd)
{
char buffer[1024];
// 普通 fd就绪,本次读取,不会被阻塞!
ssize_t n = recv(sockfd, &buffer, sizeof(buffer) - 1, 0); // bug
if (n > 0)
{
std::cout << "client say@ " << buffer << std::endl;
}
else if (n == 0) // 客户端不写了
{
LOG(LogLevel::INFO) << "client quit";
// epoll_ctl只能移除合法的 fd!,先移除,再关闭!
int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
if (m == 0)
{
LOG(LogLevel::INFO) << "epoll_ctl remove success";
}
close(sockfd);
}
else // 读取出错
{
LOG(LogLevel::ERROR) << "recv error";
int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
if (ret == 0)
{
LOG(LogLevel::INFO) << "epoll_ctl remove success";
}
close(sockfd);
}
}
void Stop()
{
_isrunning = false;
}
~EpollServer()
{
_listensock->Close();
if (_epfd > 0)
close(_epfd);
}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _epfd;
struct epoll_event _revs[size]; // 拿取就绪事件的缓冲区数组,最大捞多少个事件
};
完。