一、epoll
epoll是为处理大批量句柄而作了改进的poll,它是在2.5.44内核中被引进的(epoll(4) is a newAPI introducedinLinuxkernel2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
二、epoll的3个相关的系统调用
2.1、 epoll_create

创建一个epoll的句柄:
自从linux2.6.8之后,size参数是被忽略的。用完之后,必须调用closeO关闭。
2.2、epoll_wait

收集在epoll监控的事件中已经发送的事件:
(1)参数events是分配好的epoll_event结构体数组。
(2)epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
(3)maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
(4)参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。
(5)epfd就是epoll_create返回的文件描述符。
(6)如果函数调用成功,返回对应I/O上已准备好的文件描述符数目(已经就绪的fd的个数),如返回0表示已超时,返回小于0表示函数失败。
(7)epoll_event结构体:

2.3、epoll_ctl

epoll的事件注册函数:
它不同于select0是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
(1)第一个参数是epoll_createO的返回值(epoll的句柄)。
(2)第二个参数表示动作,用三个宏来表示。
(3)第三个参数是需要监听的fd。
(4)第四个参数是告诉内核需要监听什么事。
第二个参数op的取值:
(1)EPOLL_CTL_ADD:注册新的fd到epfd中。
(2)EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
(3)EPOLL_CTL_DEL:从epfd中删除一个fd。

events可以是以下几个宏的集合:
(1)EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
(2)EPOLLOUT:表示对应的文件描述符可以写。
(3)EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
(4)EPOLLERR:表示对应的文件描述符发生错误。
(5)EPOLLHUP:表示对应的文件描述符被挂断。
(6)EPOLLET:将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(Level
Triggered)来说的。
(7)EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL红黑树里。
三、epoll的工作原理
3.1原理解释以及图解

1、当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

2、每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
3、这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
4、而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
5、这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.。
6、在epoll中,对于每一个事件,都会建立一个epitem结构体。

7、当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。
8、如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是0(1)。
3.2总结epoll的使用过程:
(1)调用epoll_create创建一个epoll句柄。
(2)调用epoll_ctl,将要监控的文件描述符进行注册。
(3)调用epoll_wait,等待文件描述符就绪。
epoll的优点(和select的缺点对应)
(1)接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
(2)数据拷贝轻量:只在合适的时候调用EPOLL_CTLADD将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
(3)事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会受到影响。
(4)没有数量限制:文件描述符数目无上限
四、epoll代码:
4.1nocopy.hpp文件:
为了防止epoll拷贝,创建一个nocopy类:
cpp
#pragma once
#include <iostream>
class nocopy
{
public:
nocopy()
{
}
nocopy(const nocopy &) = delete;
nocopy &operator=(const nocopy &) = delete;
};
4.2Sock.hpp文件
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"
const int backlog = 10;
extern log lg;
enum
{
SocketError = 2,
BindError,
ListenError
};
class Sock
{
public:
Sock()
: _sockfd(-1)
{
}
~Sock()
{
if (_sockfd >= 0)
{
close(_sockfd);
}
}
// 创建套接字
void Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
lg.logmessage(Fatal, "socket fail, %s, %d", errno, strerror(errno));
exit(SocketError);
}
int opt = -1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
// 创建一个绑定接口
void Bind(const uint16_t port)
{
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;
if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg.logmessage(Fatal, "bind fail, %s, %d", errno, strerror(errno));
exit(BindError);
}
}
void Listen()
{
if (listen(_sockfd, backlog) < 0)
{
lg.logmessage(Fatal, "listen fail, %s, %d", errno, strerror(errno));
exit(ListenError);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (newfd < 0)
{
lg.logmessage(Warning, "accept fail, %s, %d", errno, strerror(errno));
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
// 获取远端主机的信息
*clientport = ntohs(peer.sin_port);
*clientip = ipstr;
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);
int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
{
std::cerr << "connect to" << ip << ":" << port << std::endl;
return false;
}
return true;
}
void Close()
{
close(_sockfd);
}
int Fd()
{
return _sockfd;
}
private:
int _sockfd;
};
4.3 Log.hpp文件
cpp
#pragma once
#include <iostream>
#include <stdarg.h>
#include <time.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>
#define SIZE 1024
// 设置日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define logFile "log.txt"
class log
{
public:
log()
{
PrintMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
PrintMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
default:
return "None";
}
}
void logmessage(int level, const char *format, ...) // 后面的省略号表示可变参数
{
char leftbuffer[SIZE];
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
snprintf(leftbuffer, sizeof(leftbuffer), "[%s],[%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
char rightbuffer[SIZE];
va_list s;
va_start(s, format);
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 3];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year + 1900, ctime->tm_mon, ctime->tm_mday, ctime->tm_hour,ctime->tm_min,ctime->tm_sec);
//printf("%s", logtxt);
PrintLog(level,logtxt);
// 格式:默认部分+自定义部分(可变参数部分)
}
void PrintLog(int level, const std::string& logtxt)
{
switch(PrintMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(logFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd < 0)
{
return ;
}
write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = logFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~log()
{
}
private:
int PrintMethod;
std::string path;
};
log lg;
4.4 epoll.hpp文件
对epoll函数接口的封装:
cpp
#pragma once
#include <sys/epoll.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "nocopy.hpp"
class Epoller : public nocopy
{
static const int size = 128;
public:
Epoller()
{
_epfd = epoll_create(size);
if (_epfd == -1)
{
lg.logmessage(Error, "epoll_create error:%s", strerror(errno));
}
else
{
// 创建成功
lg.logmessage(Info, "epoll_create success:%d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num)
{
int n = epoll_wait(_epfd, revents, num, _timeout);
return n;
}
int EpollerUpdate(int oper, int sockfd, uint32_t event)
{
int n = 0;
if (oper == EPOLL_CTL_DEL)
{
// 如果是删除,最后一个参数可以不要了
n = epoll_ctl(_epfd, oper, sockfd, nullptr);
if (n != 0)
{
lg.logmessage(Error, "delete epoll_ctl error");
}
}
else
{
// 修改或添加
struct epoll_event ev;
ev.events = event;
ev.data.fd = sockfd;
n = epoll_ctl(_epfd, oper, sockfd, &ev);
if (n != 0)
{
lg.logmessage(Error, "epoll_ctl error");
}
}
return n;
}
~Epoller()
{
if (_epfd >= 0)
{
close(_epfd);
}
}
private:
int _epfd;
int _timeout = {3000};
};
4.5 EpollServer.hpp文件
cpp
#pragma once
#include <sys/epoll.h>
#include <iostream>
#include <memory>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"
#include "nocopy.hpp"
const uint16_t defaultport = 8080;
uint16_t EVENT_IN = (EPOLLIN);
uint16_t EVENT_OUT = (EPOLLOUT);
class EpollServer : public nocopy
{
static const int num = 64;
public:
EpollServer(uint16_t port = defaultport)
: _port(port), _listensocket_ptr(new Sock), _epoller_ptr(new Epoller)
{
}
void Init()
{
_listensocket_ptr->Socket();
_listensocket_ptr->Bind(_port);
_listensocket_ptr->Listen();
lg.logmessage(Info, "create listensock success, listensockfd: %d", _listensocket_ptr->Fd());
}
void Dispatcher(struct epoll_event revs[], int num)
{
// 遍历
for (size_t i = 0; i < num; i++)
{
// 获取文件描述符
int fd = revs[i].data.fd;
// 获取事件
uint32_t events = revs[i].events;
if (events & EVENT_IN)
{
// 读事件
if (fd == _listensocket_ptr->Fd())
{
std::string clientip;
uint16_t clientport;
// 获取了一个新链接
int sock = _listensocket_ptr->Accept(&clientip, &clientport);
if (sock > 0)
{
// 不能直接读,因为可能没数据,需要交给epoll替我们关心
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg.logmessage(Info, "get a new link, client message:clientip:%s,clientport:%d", clientip.c_str(), clientport);
}
else
{
continue;
}
}
else
{
// 其他的文件描述符就绪,进行读取操作
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 = buffer;
echo += "Server Echo# ";
write(fd, echo.c_str(), echo.size());
}
else if (n == 0)
{
lg.logmessage(Info, "client quit, close fd is:%d", fd);
// 从epoll中移除了文件描述符(一定是先移除在关闭)
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
else
{
lg.logmessage(Warning, "read error, fd is:%d", fd);
// 从epoll中移除了文件描述符(一定是先移除在关闭)
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
}
}
else if (events & EVENT_OUT)
{
// 写事件
}
else
{
}
}
}
void Start()
{
// 将listen文件描述符加入到epoll中,本质是吧listen文件描述符合他关心的事件添加到红黑树中
int n = _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EVENT_IN); // 只关心读事件
struct epoll_event revs[num];
for (;;)
{
int n = _epoller_ptr->EpollerWait(revs, num);
if (n > 0)
{
// 有事件就绪
lg.logmessage(Info, "event happended, fd is %d", revs[0].data.fd);
// 处理事件
Dispatcher(revs, n);
}
else if (n == 0)
{
lg.logmessage(Info, "wait time out!");
}
else
{
lg.logmessage(Warning, "epoll wait error");
}
}
}
~EpollServer()
{
_listensocket_ptr->Close();
}
private:
std::shared_ptr<Sock> _listensocket_ptr;
std::shared_ptr<Epoller> _epoller_ptr;
uint16_t _port;
};
4.6 main.cc文件
cpp
#include <memory>
#include "EpollServer.hpp"
int main()
{
std::unique_ptr<EpollServer> svr(new EpollServer());
svr->Init();
svr->Start();
return 0;
}
五、epoll工作方式
epoll有2种工作方式:水平触发(LT)和边缘触发(ET))
5.1水平触发LevelTriggered工作模式
epoll默认状态下就是LT工作模式
(1)当epoll检测到socket上事件就绪的时候,口可以不立刻进行处理.口或者只处理一部分。
(2)假如只读了1K数据,缓冲区中还剩1K数据,在第二次调用epoll_wait时,epoll_wait仍然会立刻返回并通知socket读事件就绪。
(3)直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回。
(4)支持阻塞读写和非阻塞读写
5.2边缘触发EdgeTriggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。
(1)当epoll检测到socket上事件就绪时,必须立刻处理。
(2)如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用epoll_wait的时候,epoll_wait 不会再返回了。
(3)也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会,这也就使得我们必须循环读取,直到读取出错,但是由于fd默认是阻塞的,所以在ET模式下,所有的fd都必须是非阻塞的(non_block)。(重点)
(4)ET的性能比LT性能更高(epoll_wait返回的次数少了很多),Nginx默认采用ET模式使用epoll。
(5)只支持非阻塞的读写
注意:select和poll其实也是工作在LT模式下,epoll既可以支持LT,也可以支持ET。
总结:ET相对于LT,ET的通知效率更高,并且ET的IO也更高效,因为每一次都保证要把数据全部取走,那么这也保证了TCP在通知对方的时候,可以告诉对方一个更大的窗口,从而在概率上一次性发送更多数据。
六、epoll的使用场景:
epoll的高性能,是有一定的特定场景的
(1)对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。例如,典型的
一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适
合epoll。
(2)如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用
epoll就并不合适,具体要根据需求和场景特点来决定使用哪种lo模型。