目录
[1. epoll 是什么?核心定位?解决什么问题?](#1. epoll 是什么?核心定位?解决什么问题?)
[2. epoll的接口](#2. epoll的接口)
[2.1 epoll_create()](#2.1 epoll_create())
[2.2 epoll_ctl()](#2.2 epoll_ctl())
[2.3 epoll_wait()](#2.3 epoll_wait())
[3. epoll的原理](#3. epoll的原理)
[3.1 自己的理解](#3.1 自己的理解)
[3.2 理性 ----- 源代码 + 重谈 socket 对象](#3.2 理性 ----- 源代码 + 重谈 socket 对象)
[4. 代码](#4. 代码)
[5. epoll 工作模式](#5. epoll 工作模式)
[5.1 LT:水平触发工作模式(Level Triggered)](#5.1 LT:水平触发工作模式(Level Triggered))
[5.2 ET:边缘触发工作模式(Edge Triggered)](#5.2 ET:边缘触发工作模式(Edge Triggered))
[5.3 LT 和 ET 的设计??](#5.3 LT 和 ET 的设计??)
[5.4 LT vs ET](#5.4 LT vs ET)
1. epoll 是什么?核心定位?解决什么问题?
- epoll 是一个多路复用,多路转接的一种具体方案
- epoll 核心定位:IO = 等 + 拷贝,一次等待多个fd -> 任意一个或者多个fd就绪,通知用户 -> 通过等待多个fd(手段),通知用户哪些fd就绪了(目的);select / poll / epoll 的核心定位就是:就绪事件通知机制!!!
- epoll **是为处理大批量句柄(文件描述符)而作了改进的poll。**它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的⼀切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法
2. epoll的接口
2.1 epoll_create()
int epoll_create(int size);

- 参数:size:被忽略的,设置上大于 0 的值就行了,对应的就是底层红黑树节点的个数的上限,现在是不需要这个上限的,所以忽略

- 返回值:成功,返回文件描述符;失败,返回-1;返回值通常代表的是 epoll 模型
2.2 epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl:向指定的文件描述符**(** int epfd ) ,增加、删除 or 修改(int op ),对指定文件描述符上(int fd ),指定事件(struct epoll_event *event )的关心,是一个 **输入型数组!!**是用户告诉内核!!
- int op 常见有3种:

- struct epoll_event 的结果如下

events 可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这⾥应该表⽰有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发⽣错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加⼊到EPOLL红⿊树⾥.返回值:

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

参数:
1. int epfd:就是 epoll_create 创建出来的返回值
2. int timeout:同poll一样
- timeout = 1000,表示1000以内,阻塞等待,没有就绪,直到超时,超时之后,自动进行timeout
- timeout = -1,代表的是阻塞等待,除非有文件描述符就绪,否则不返回
- timeout = 0,代表的是非阻塞式等待
3. struct epoll_event *events, int maxevents:是一个输出型数组,是内核告诉用户

返回值:和select、poll一样
- > 0:有多少个 fd 就绪
- == 0:超时,没有就绪,也没有报错
- < 0:select 自己调用出错
3. epoll的原理
3.1 自己的理解

3.2 理性 ----- 源代码 + 重谈 socket 对象
创建 epoll 模型,实际上是创建一个 eventpoll 的数据结构对象。

为什么 epoll_create 的返回值是一个文件描述符???

返回文件描述符也意味着要创建 struct file 对象!!!
所以调用 epoll_ctl 拿着文件描述符 -> 找到文件对象 -> 找到 private_data -> 找到eventpoll 就可以读取红黑树、就绪队列,所以这就是为什么在epoll_ctl()函数中的第一个参数是 fd 的根本原因。
回调机制在哪里???什么时候会被触发???

实际上在内核中创建套接字时,创建 struct file ,创建 struct socket ,创建 tcp_socket 或是 udp_socket ,所以获取连接的时候,accept 内核中得到文件描述符,其实创建tcp套接字,创建 socket,创建struct file,分配文件描述符,返回出去。在底层的任意一层,收到了sock结构体,就有 (*sk_data_ready)(struct sock *sk,int bytes);的回调,就将节点激活了。 struct sock是在最内层的,不管是创建udp还是tcp,sock直接就会有,所以接受和发送缓冲区对于tcp、udp都是存在的。
回调机制是在 struct sock 结构体中,有函数指针,创建对象时,直接初始化成 epoll 模型里面的一个方法,节点一激活,放到就绪队列中。
4. 代码
events 可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这⾥应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发⽣错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL红黑树里.

cpp
// EpollEchoServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"
const static int gsize = 64;
class EpollServer
{
public:
EpollServer(uint16_t port)
:_listensock(std::make_unique<TcpSocket>()),_epfd(-1)
{
_listensock->BuildListenSocketMethod(port); // 3
_epfd = epoll_create(123); // 4
if(_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create failed";
return;
}
LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
<< " epoll fd is: " << _epfd;
// 首先要做的是把唯一的一个listensock添加到epoll模型中!!
// 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->SockFd(); // ?
int n = epoll_ctl(_epfd,EPOLL_CTL_ADD,_listensock->SockFd(),&ev);
(void)n;
}
void Start()
{
int timeout = 1000; //同poll
while(true)
{
// 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
// 内核告诉用户,哪些fd上面的哪些事件就绪了
int n = epoll_wait(_epfd,revs,gsize,timeout);
switch(n)
{
case 0:
LOG(LogLevel::DEBUG) << "time out......";
break;
case -1:
LOG(LogLevel::FATAL) << "epoll_wait filed";
break;
default:
// Dispatcher(); // 存在就绪事件,就得派发
break;
}
}
}
~EpollServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
int _epfd;
// 就绪事件放在外面
struct epoll_event revs[gsize]; //revs:return events;
};

非阻塞轮询:
cpp
int timeout = 0;

阻塞等待:
cpp
int timeout = -1;

cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"
const static int gsize = 64;
class EpollServer
{
public:
EpollServer(uint16_t port)
: _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
{
_listensock->BuildListenSocketMethod(port); // 3
_epfd = epoll_create(123); // 4
if (_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create failed";
return;
}
LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
<< " epoll fd is: " << _epfd;
// 首先要做的是把唯一的一个listensock添加到epoll模型中!!
// 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->SockFd(); // ?
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
(void)n;
}
void Dispatcher()
{
LOG(LogLevel::INFO) << "有事件就绪了......";
// 获取新连接
InetAddr clientaddr;
int sockfd = _listensock->Accept(&clientaddr);
if (sockfd > 0)
{
LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
<< " 客户端地址是:" << clientaddr.ToString();
// sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
// 当前套接字上的数据是否就绪,根本就不知道
// 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
(void)n;
LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
}
}
void Start()
{
int timeout = -1; // 同poll
while (true)
{
// 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
// 内核告诉用户,哪些fd上面的哪些事件就绪了
int n = epoll_wait(_epfd, revs, gsize, timeout);
switch (n)
{
case 0:
LOG(LogLevel::DEBUG) << "time out......";
break;
case -1:
LOG(LogLevel::FATAL) << "epoll_wait filed";
break;
default:
Dispatcher(); // 存在就绪事件,就得派发
break;
}
}
}
~EpollServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
int _epfd;
// 就绪事件放在外面
struct epoll_event revs[gsize]; // revs:return events;
};


上层如果没有处理的话,先是阻塞式的等待,只有有一个连接到了,就会一直处于死循环
cpp
void Dispatcher()
{
LOG(LogLevel::INFO) << "有事件就绪了......";
// 获取新连接
// InetAddr clientaddr;
// int sockfd = _listensock->Accept(&clientaddr);
// if (sockfd > 0)
// {
// LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
// << " 客户端地址是:" << clientaddr.ToString();
// // sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
// // 当前套接字上的数据是否就绪,根本就不知道
// // 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
// struct epoll_event ev;
// ev.events = EPOLLIN;
// ev.data.fd = sockfd;
// int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
// (void)n;
// LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
// }
}


为什么会出现这样的情况?
epoll_wait 底层只要有就绪事件,如果没有将就绪事件没有取走 or 即便是取走了,但是没有真正的处理了,底层就会一直触发,进而一直看到的就是死循环。
触发的死循环:
cpp
void Start()
{
int timeout = -1; // 同poll
while (true)
{
// 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
// 内核告诉用户,哪些fd上面的哪些事件就绪了
int n = epoll_wait(_epfd, revs, gsize, timeout);
switch (n)
{
case 0:
LOG(LogLevel::DEBUG) << "time out......";
break;
case -1:
LOG(LogLevel::FATAL) << "epoll_wait filed";
break;
default:
Dispatcher(); // 存在就绪事件,就得派发
break;
}
}
}
cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"
const static int gsize = 64;
class EpollServer
{
public:
EpollServer(uint16_t port)
: _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
{
_listensock->BuildListenSocketMethod(port); // 3
_epfd = epoll_create(123); // 4
if (_epfd < 0)
{
LOG(LogLevel::FATAL) << "epoll_create failed";
return;
}
LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
<< " epoll fd is: " << _epfd;
// 首先要做的是把唯一的一个listensock添加到epoll模型中!!
// 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listensock->SockFd(); // 方便后续处理!!
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
(void)n;
}
void Accepter()
{
// 获取新连接
InetAddr clientaddr;
int sockfd = _listensock->Accept(&clientaddr);
if (sockfd > 0)
{
LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
<< " 客户端地址是:" << clientaddr.ToString();
// sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
// 当前套接字上的数据是否就绪,根本就不知道
// 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
(void)n;
LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
}
}
void Recver(int sockfd)
{
char buffer[1024]; // bug ! --- 读取的时候,不能保证能将报文读取完,也不能保证读取的就是一个完整的报文
// 必须结合协议,序列化、反序列化
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
// 成功
buffer[n] = 0;
std::cout << "Client Say@ " << buffer;
std::string echo_string = "echo server# ";
echo_string += buffer;
// 写?没错,但是不完善
send(sockfd, echo_string.c_str(), echo_string.size(), 0);
}
else if (n == 0)
{
// 对端将连接关掉
LOG(LogLevel::INFO) << "client quit, me too, fd is : " << sockfd;
// 坑:epoll_ctl EPOLL_CTL_DEL:不能对非法fd进行操作
int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
(void)n;
close(sockfd);
}
else
{
// 失败
LOG(LogLevel::INFO) << "recv error, fd is : " << sockfd;
int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
(void)n;
close(sockfd);
}
}
void Dispatcher(int num)
{
LOG(LogLevel::INFO) << "有事件就绪了......";
for (int i = 0; i < num; i++)
{
int fd = _revs[i].data.fd;
uint32_t events = _revs[i].events;
if (events & EPOLLIN)
{
// 1. listensockfd
if (fd == _listensock->SockFd())
{
Accepter();
}
else
{
// 2.普通的sockfd
Recver(fd);
}
}
// if(events & EPOLLOUT)
// {
// // TODO
// }
}
}
void Start()
{
int timeout = -1; // 同poll
while (true)
{
// 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
// 内核告诉用户,哪些fd上面的哪些事件就绪了
// 2. epoll_wait 就绪的fd,就绪的事件可是多种多样的!
int n = epoll_wait(_epfd, _revs, gsize, timeout);
switch (n)
{
case 0:
LOG(LogLevel::DEBUG) << "time out......";
break;
case -1:
LOG(LogLevel::FATAL) << "epoll_wait filed";
break;
default:
Dispatcher(n); // 存在就绪事件,就得派发
break;
}
}
}
~EpollServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
int _epfd;
// 就绪事件放在外面
struct epoll_event _revs[gsize]; // revs:return events;
};


5. epoll 工作模式
LT 和 ET 是就绪事件的通知机制。
5.1 LT:水平触发工作模式(Level Triggered)
epoll默认状态下就是LT工作模式
- 当epoll检测到socket上事件就绪的时候, 可以不⽴刻进⾏处理. 或者只处理⼀部分
- 如上⾯的例⼦, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时,epoll_wait 仍然会⽴刻返回并通知socket读事件就绪.
- 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会⽴刻返回.
- 支持阻塞读写和非阻塞读写
5.2 ET:边缘触发工作模式(Edge Triggered)
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进⼊ET⼯作模式

- 当epoll检测到socket上事件就绪时, 必须⽴刻处理.
- 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第⼆次调⽤ epoll_wait 的时候, epoll_wait 不会再返回了.
- 也就是说, ET模式下, ⽂件描述符上的事件就绪后, 只有⼀次处理机会
- ET的性能比LT性能更⾼( epoll_wait 返回的次数少了很多). Nginx默认采⽤ET模式使⽤epoll.
- 只支持非阻塞的读写
例子,区分上面的两种工作模式:
小帅,在某购物平台上买了5件商品,到了菜鸟驿站,有两个快递员,张三,李四,这天下午,张三将其中3个快递,其余的快递没看到,张三送到小帅的宿舍楼下,小帅在宿舍里面打游戏,张三打电话让小帅下来取快递,但是小帅过来好久都没下去,张三又打电话,让他下来,就这样一直打电话,小帅就下去了,但是其中有一个是大件,先将其余两个取了上去,小帅忘了下面还有了一个快递,就呆在宿舍里面了。此时李四来送小帅剩余的2个快递了,看见张三还在小帅的宿舍楼下等,就将剩余的两个快递给张三,让张三一起给小帅,张三就又给小帅打电话说,你又有两个快递了,快下来取,小帅才去取快递。张三车里只要一直都有小帅的快递,就会一直给小帅打电话。这种就被称为 LT策略,水平触发工作模式
第二天,又有小帅的5个快递了,但是今天的快递员是李四,李四将所有人的快递包括小帅的3个快递拉到小帅的宿舍楼下,打电话给小帅说,小帅快下来取快递,我只打一次电话,你不下来我就走了,小帅听了立马就下去取快递了,看见有一个大件的,就先将大的拿了上去,给李四说,我等下下来取剩下的两个,李四鸟都不鸟他,上去之后,又被舍友拉着打游戏去了,李四将所有人的快递派发完了,此时张三将小帅的剩下的两个快递给李四了,李四再一次打电话,说,小帅你的快递又有两个快下来取,不来的话我就要走了,小帅就下去了,将剩下的所有快递取完了。李四的策略是:通知你,你就要下来,只通知你一次 --- ET模式。从无到有,从有到多,才会通知你一次!!
其实,在通知的过程就是 epoll_wait 的过程,在通知上层哪些事件就绪了!!!
问题1:谁派发快递的效率高一些???李四,为什么??
通知效率高,本质是无效通知少!!!
问题2:你更喜欢谁派发快递的方式???---- 张三
在示波器中,一直处于峰值的横线被称为LT模式,从无到有 或 从右到无变化的称为 ET模式(了解)

5.3 LT 和 ET 的设计??
LT :一个文件描述符节点被激活,通知上层第一次,上层一直再取但是一直没有取完,所以不能将这个文件描述符从就绪队列中移走。
ET :第一次底层有数据,5号文件描述符被激活了,epoll_wait 读取就绪事件,5号节点一旦被epoll_wait 节点拿走,5号节点就会在就绪队列中立即被移除掉,就只在红黑树中存在,从激活状态恢复到非激活状态,当数据新增发生变化,再次被激活。
LT 和 ET 本质就是衡量的是就绪事件,就绪节点在就绪队列中的存活时长的问题。
读写并不会影响红黑树节点的任何状态,只有 epoll_ctl () 对应的add、del、mod 才会修改红黑树,读写只会影响就绪队列。
5.4 LT vs ET
两种模式相比,谁的效率更高??? ---- ET的通知效率高!!
IO方面呢?(通知只是IO方面的一个环节) --- 面试(为什么在ET模式下,将文件描述符设置为非阻塞模式)
ET -> 从无到有,从有到多 -> 才会通知我一次 -> 要求程序员必须一次将本轮数据全部读取完毕 -> 如何保证你把TCP缓冲区本次的数据全部读取完毕!!---- 循环读 - > 最后一次读取完毕,我们往往还要读 -> 因为fd阻塞,recv就会被卡住!!!此时服务器就会被挂起!!!
所以ET模式下,必须把对应的文件描述符设置为非阻塞!!!是为了读取完本轮数据!!!
为什么ET模式下IO会更高呢??
ET 本质是倒逼程序员,按照它的要求把数据读取完毕!上层就会将数据尽快取走,此时ACK应答,就会给发送方通知一个更大的win窗口!!!影响对方的滑动窗口大小!!变得更大,IO效率就更高了!
历史问题:PSH,要求对方内核把数据尽快交给上层!!!交给上层是由用户recv才会把数据读上去,内核能做的就是让对应的fd事件就绪!!!不是说一有数据就调用系统调用,OS会在TCP缓冲区中设置一个高低水位线的概念,低于低水位线,TCP不通知上层,epoll也不通知,超过了才会通知,这样让上层调用一次系统调用拷贝更多的数据,从而减少系统调用次数。而PSH的本质就是:无视高低水位线,只要有数据,立马通知上层。
ET 做的工作 LT 也能做,循环读,将fd设置为非阻塞,但是 LT 本质: 不能倒逼程序员!!!
所以之前的select、poll,获取了一个连接,到来了一个新的事件,但不进行处理,为什么回答while死循环:


因为 epoll 的工作模式默认是 LT !底层事件没有做处理,底层就一直通知,一直返回。select 和 poll 没有工作模式,就变相的是 LT 的工作模式。
