目录
- 初识
- 相关调用
- 工作原理
- 工作方式
- 使用场景
- 惊群问题
- 优缺点
- 项目构建工具
- LT示例
1. 初识
按照man手册的说法:是为处理大批句柄而做了改进的poll
它是在2.5.44内核中被引进(epoll(4) is a new API introduced in Linux Kerner 2.5.44),它几乎具备了之前所说的一切优点,被公认为linux2.6下性能最好的多路io就绪通知的方法
2. 相关调用
有3个相关调用
epoll_create
int epoll_create (int size);
创建一个epoll的句柄
- 自从linux2.6.8之后,size参数是被忽略的
- 用完之后,必须调用close()关闭
epoll_ctl
int epoll_ctl (int epfd, int op, int fd, struct epoll_event* event);
事件注册函数:
- 不同于selelct是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
- 第一个参数是epoll_create()的返回值(epoll的句柄)
- 第二个参数表示动作,用三个宏表示
- 第三个参数是需要监听的fd
- 第四个参数是告诉内核需要监听什么事
第二个参数的取值:
EPOLL_CTL_ADD: 注册新的fd到epfd中
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件
EPOLL_CTL_DEL: 从epfd中删除一个fd
struct epoll_event结构:
events可以是以下几个宏的集合:
EPOLLIN:表示对应的文件买搜狐福可读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET:将EPOLL设为边缘触发(Edge TRiggered)模式,这时相对于水平触发(Level Triggered来说的)
EPOLLONSHOT:只监听一次事件,当监听完事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait
int epoll_wait( (int epfd, struct epoll_event* evnets, int maxevents, int timeout);
收集在epoll监控的时间中已经发送的事件
- 参数evnets是分配好的epoll_event结构体数组
- epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个evnets数组中,不会去帮助我们在用户态分配内存)
- maxevents告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
- 参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)
- 如果函数调用成功,返回对应I/O上已经准备好的文件买搜狐福数目,如果返回0表示已超时,返回小于0表示函数失败
3. 工作原理
数据先到达网卡的,网卡是硬件,通过硬件中断的方式让os知道有数据到达,将数据拿到内存,通过回调函数将数据向上交付。维护一个红黑树,节点包括要关心的fd,还有要关心的事件,再维护一个就绪事件队列,当红黑树中要关心的事件就绪时,将这个链接到队列中。驱动层这个回调函数做4个事情,1.向上交付 2.交付给tcp的接收队列 3.查找红黑树,用文件描述符做键,找到就绪事件 4.构建就绪节点,插入到就绪队列
epoll模型就是创建和维护这些东西,将它看做文件,有对应的struct file结构,指向epoll模型。把它添加到进程对应的文件描述符表里,通过fd就可以找到这个结构
当某一进程调用epoll_create方法时,内核会创建一个eventpoll及饿哦固体,这个里有两个成员与epoll的使用方式密切相关
struct eventpoll{
…
/红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件/
struct rb_root rbr;
/双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件/
struct list_head rdlist;
…
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象添加的事件
这些事件会挂在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来,(红黑树的插入时间效率是lgn,其中n为树的高度)
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,当响应的事件发生时,会调用这个回调方法
这个回调方法在内核中叫ep_poll_callback,它会将发生的事添加到rdlist双链表中
在epoll中,对于每一个时间事件,都会建立一个epitem结构体
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表是否有epitem元素即可,所以是O(1)的,获取就绪时O(N)的
如果rdlist不为空,则把发生的事件复制到用户态,同时将时间数量返回给用户,这个操作的时间复杂度是O(1)
总结一下,epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄
调用epoll_ctl,将要监控的文件描述符进行注册
调用epoll_wait,等待文件描述符就绪
4. 工作方式
epoll有两种方式,水平触发(LT)和边缘触发(ET)
正在玩游戏,到了决赛时刻,你妈饭做好了,喊吃饭的两种方式
- 喊一次没动,继续喊第二次,第三次…(水平触发)
- 喊一次,没动,不管了(边缘触发)
假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这时候socket的另一端被写入了2kb数据
调用epoll_wait,并且会返回,说明它已经准备好读取操作
然后调用read,只读取了1kb的数据
继续调用epoll_wait。。。
水平触发Level Triggered工作模式
epoll默认状态下就是LT工作模式,事件到来,上层不处理,高电平一直有效
- 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分
- 如上面的例子,由于只读了1k数据,缓冲区还剩1k数据,在第二次调用
epoll_wait
时,epoll_wait
仍然会立刻返回并通知socket读事件就绪 - 直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回
边缘触发Edge Triggered工作模式
如果在第一部将socket添加到epoll描述符使用了EPOLLET标志,进入ET工作模式。数据或者连接,从无到有,从有到多,变化的时候,才会通知一次
- 当epoll检测到socket上事件就绪时,必须立刻处理
- 上面的例子,只读取了1k的数据,还剩1k,第二次调用的时候,不会再返回了
- ET模式下,文件描述就绪后,只有一次处理机会
- ET的性能比LT性能更高,(epoll_wait返回的次数少了很多),Nginx默认采用ET模式epoll
- 只支持非阻塞的读写
slect和poll也是工作在LT模式下,epoll两种都支持
对比LT和ET
LT是epoll默认行为,使用ET能减少epoll触发次数,代价就是必须一次相应就把所有数据处理完
相当于一个文件描述符就绪后,不会反复提示就绪,通知效率更高,减少了epoll次数,io效率更高,使得返回给对方的窗口更大。但是LT如果也能做到每次就绪的文件描述都立刻处理,设为nonblock循环读取,不让这个就绪重复提示,其实性能也是一样的
ET的代码复杂度更高了
理解ET模式和非阻塞文件描述符
使用et模式的epoll需要将文件描述符设为非阻塞,不只是接口的要求,而是实践上的要求
假如这样的场景:服务器接受到一个10k的请求,会向客户端返回一个应答数据,如果客户端收不到应答,不会发送第二个10k的请求
如果服务器的代码是阻塞式的read,并且一次只read1k的数据的话(read不能保证一次就把所有数据读出来,参考man手册说明,可能被信号打断),剩下9k数据会待在缓冲区
此时由于epoll是et模式,并不会认为文件描述符就绪,epoll_wait不会再返回,剩下9k数据一直在缓冲区中,客户端收不到应答不会再发送。直到下一次客户端再给服务器写数据,epoll_wait才返回
问题来了
- 服务器只读到了1k个数据,要10k都读完才能给客户端返回响应数据
- 客户端要读到服务器的响应,才会发送下一个请求
- 客户端发送了下一个请求,epoll_wait才会返回,惨能去读缓冲区中剩余的数据
所以,为了解决上面的问题(阻塞read不一定能一次把完整的请求读完),于是使用非阻塞轮询的方式读缓冲区,保证一定吧完整的请求都读出来
如果lt没这个问题,只要缓冲区数据没读完,能让epollwait返回就绪
5. 使用场景
epoll的高性能,在一些特定场景不适宜
对于多连接,且多链接中只有一部分链接比较活跃时,比较适合用epoll
例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网app的入口服务器,这样的服务器很适合epoll,如果只是系统内部,服务器和服务器之间通信,只有少数几个连接,这种情况下用epoll不合适,具体根据需求和场景决定使用哪种io模型
6. 惊群问题
多线程或多进程环境下,多个线程同时监听socket,当新链接请求到来,os不知道派哪个线程处理,干脆将其中几个线程都唤醒,实际上只有一个线程能成功处理accept,其他线程都失败,错误码EAGAIN,这种现象称为惊群效应
多线程下,不建议让多个线程同时在epoll_wait监听socket,让其中一个线程监听,有新的连接到来,由这个线程建立,交给其他线程处理后续读写
多进程下,如lighttpd,nginx等很多采用了master/workers的模式提高并发能力,nginx中甚至采用了负载均衡。niginx的解决,同一时刻,只有一个进程监听,创建全局的mutex,子进程wait前,先获取锁,当连接数到达一定程度,不会再去获取锁
7. 优缺点
和select的缺点对应
- 接口使用方便,虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次虚幻都设置关注的文件描述符,也做到了输入输出参数分开
- 数据拷贝轻量,只在合适的时候调用EPOLL_CTL_ADD,将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道那些文件描述符就绪,这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会受到影响
- 没有数量限制:文件描述符数目无上限
缺点
1.系统依赖性,linux特有的io事件通知机制,跨平台不兼容
2.更复杂,调试困难,涉及多个复杂的回调机制
3.epoll_ctl开销,处理大量短连接会对性能影响
4.边缘触发模式的风险,支持边缘出发et模式,没有及时处理可能会导致数据丢失
注意:
有些地方说,epool使用了内存映射机制,内存映射机制是内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝内存这样的额外性能开销
这种说法是不准确的,我们定义的struct epoll_event是我们在用户空间中分配好的内存,势必还是需要将内核的数据拷贝这个用户空间的内存中的
总结select,poll,epoll之间的优点和缺点(重要)
8. 项目构建工具
CMakeLists.txt
除过makefile构建项目外,还可以使用CmakeLists,自动生成makefile,创建这个文件
指定使用的cmake版本
cmake_minimum_required(VERSION 2.8.12.2)
项目名称和语言
project(名字)
编译选项
使用c++11标准,早期的版本可以适用该语句,较新的版本使用第二个
set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -std=c++11”)
set(CMAKE_CXX_STANDARD 11)
确保使用c++11标准
set(CMAKE_CXX_STANDARD_REQUIRED ON)
添加源文件和程序名
add_executable(程序名 源文件)
编译命令
cmake . //编译生成在当前目录
构建程序
make
清除
make clean
重新编译,删除build的文件夹,就是CMakeFiles文件夹
9. LT示例
先创建cmakefile
CMakeLists.txt
cmake_minimum_required(VERSION 2.8.12.2)
project(EpollServer)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 确保必须使用指定的标准
add_executable(EpollServer EpollServer.cc)
将Epoll模型的几个函数封装成类
类维护模型返回的文件标识
EpollWait函数的返回值,调用epoll_wait会返回就绪的事件数量,可以作为函数的返回值
EpollUpdate函数封装epoll_ctl函数,参数传入需要进行的操作,文件标识,需要添加的事件。调用函数前根据需要进行的操作区分,分为添加事件和删除,添加事件需要创建一个epoll_event结构体,赋值传入。删除只需要将事件设为nullptr
Epoll.hpp
#pragma once
#include <sys/epoll.h>
#include "nocopy.hpp"
#include "log.hpp"
class Epoll : public nocopy
{
static const int size = 128;
Log log;
public:
Epoll()
{
_epfd = epoll_create(size);
if (_epfd == -1)
{
log.logmessage(ERROR, "epoll create error:%s", strerror(errno));
}
else
{
log.logmessage(info, "epoll create success:%d", _epfd);
}
}
int EpollWait(struct epoll_event revents[], int num)
{
int n = epoll_wait(_epfd, revents, num, -1);
return n;
}
int EpollUpdate(int oper, int sock, uint32_t event)
{
int n = 0;
if (oper == EPOLL_CTL_DEL)
{
n = epoll_ctl(_epfd, oper, sock, nullptr);
if (n != 0)
{
log.logmessage(ERROR, "epoll_ctl delete error");
}
}
else
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;
n = epoll_ctl(_epfd, oper, sock, &ev);
if (n != 0)
{
log.logmessage(ERROR, "epoll_ctl add error");
}
}
}
~Epoll()
{
if (_epfd >= 0)
{
close(_epfd);
}
}
private:
int _epfd;
int _timeout{3000};
};
上面的类不想让它拷贝,所以继承一个不能拷贝的类
nocopy.hpp
#pragma once
class nocopy
{
public:
nocopy(){}
nocopy(const nocopy &) = delete;
nocopy& operator=(const nocopy&) = delete;
};
服务器源文件生成一个对象,调用初始化和执行函数
EpollServer.cc
#include <memory>
#include "EpollServer.hpp"
int main()
{
std::unique_ptr<EpollServer> svr(new EpollServer);
svr->Init();
svr->Start();
return 0;
}
epoll服务器类维护套接字类和epoll类的对象
构造的时候初始化,init函数套接字设为监听,start函数将listen套接字添加到epoll模型中,死循环调用epoll_wiat函数,for循环外创建一个epoll_event,作为函数的输出参数。当返回值大于0时说明有事件就绪,调用分配函数
分配函数需要创建的epoll_event变量,还有就绪的事件数量。根据事件数量循环取到事件位图和标识符,判断是读还是写事件,读的话区分是新链接还是其他事件,如果是新链接,获取套接字,并添加到epoll的监听事件里。如果是读事件就正常读写,然后回显给客户端
EpollServer.hpp
#pragma ocne
#include <iostream>
#include <memory>
#include "log.hpp"
#include "Epoll.hpp"
#include "Socket.hpp"
static const uint16_t port = 8000;
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENt_OUT = (EPOLLOUT);
class EpollServer :public nocopy
{
static const int num = 64;
public:
EpollServer()
: _listensocket_ptr(new Sock()), _epoll_ptr(new Epoll())
{
}
void Init()
{
_listensocket_ptr->Socket();
int opt = 1;
setsockopt(_listensocket_ptr->Fd(), SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
_listensocket_ptr->Bind(port);
_listensocket_ptr->Listen();
}
void Accepter()
{
// 获取新链接
std::string ip;
uint16_t port = 0;
int sock = _listensocket_ptr->Accept(&ip, &port);
if (sock > 0)
{
// 不能直接取,需要注册
_epoll_ptr->EpollUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg.logmessage(info, "get a new link,client@%s:%d", ip.c_str(), port);
}
}
void Recver(int fd)
{
char buff[1024];
ssize_t n = read(fd, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n] = 0;
std::cout << "get a message:" << buff << std::endl;
// 回显
std::string echo_str = "server echo$:";
echo_str += buff;
write(fd, echo_str.c_str(), echo_str.size());
}
else if (n == 0)
{
lg.logmessage(info, "client quit, me too, close fd:%d", fd);
// 先取消关注,再关文件
_epoll_ptr->EpollUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
else
{
lg.logmessage(warning, "recv error fd:%d", fd);
_epoll_ptr->EpollUpdate(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
}
void Dispatcher(struct epoll_event rev[], int num)
{
for (int i = 0; i < num; i++)
{
uint32_t events = rev[i].events;
int fd = rev[i].data.fd;
if (events & EVENT_IN)
{
if (fd == _listensocket_ptr->Fd())
{
Accepter();
}
else
{
Recver(fd);
}
}
else if (events & EVENt_OUT)
{
}
else
{
}
}
}
void Start()
{
// 将listensock添加到epoll中 -> listensock和他关心的事件,添加到内核epoll模型中rb_tree
_epoll_ptr->EpollUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
for (;;)
{
int n = _epoll_ptr->EpollWait(revs, num);
if (n > 0)
{
lg.logmessage(info, "event happened,fd:%d", revs[0].data.fd);
Dispatcher(revs, n);
}
else if (n == 0)
{
lg.logmessage(info, "timeout...");
}
else
{
lg.logmessage(ERROR, "epoll wait error");
}
}
}
private:
std::unique_ptr<Sock> _listensocket_ptr;
std::unique_ptr<Epoll> _epoll_ptr;
};
其他
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include "log.hpp"
enum
{
SOCKERR = 1,
BINDERR,
LISERR
};
Log lg;
const int backlog = 5;
class Sock
{
public:
Sock()
{
}
void Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
lg.logmessage(fatal, "socket error");
exit(SOCKERR);
}
int opt = 1;
setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
int bret = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if (bret < 0)
{
lg.logmessage(fatal, "bind error");
exit(BINDERR);
}
}
void Listen()
{
int lret = listen(_sockfd, backlog);
if (lret < 0)
{
lg.logmessage(fatal, "listen error");
exit(LISERR);
}
}
int Accept(string* clientip, uint16_t* clientport)
{
sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(_sockfd, (sockaddr*)&peer, &len);
if (newfd < 0)
{
lg.logmessage(warning, "accept error");
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const string ip, const uint16_t port)
{
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);
peer.sin_port = htons(port);
int cret = connect(_sockfd, (const struct sockaddr*)&peer, sizeof(peer));
if (cret == -1)
{
lg.logmessage(warning, "connect error");
return false;
}
return true;
}
void Close()
{
close(_sockfd);
}
int Fd()
{
return _sockfd;
}
~Sock()
{
}
public:
int _sockfd;
};
#pragma once
#include <stdarg.h>
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <time.h>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
#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 PATH "log.txt"
class Log
{
public:
Log(int style = screen)
{
printstyle = style;
dir = "log/";
}
void enable(int method)
{
printstyle = method;
}
const char *leveltostring(int level)
{
switch (level)
{
case 0:
return "info";
break;
case 1:
return "debug";
break;
case 2:
return "warning";
break;
case 3:
return "error";
break;
case 4:
return "fatal";
break;
default:
return "none";
break;
}
}
void printlog(int level, const string &logtxt)
{
switch (printstyle)
{
case screen:
cout << logtxt;
break;
case onefile:
printonefile(PATH, logtxt);
break;
case classfile:
printclassfile(level, logtxt);
break;
}
}
void logmessage(int level, const char *format, ...)
{
time_t t = time(0);
tm *ctime = localtime(&t);
char leftbuff[1024];
sprintf(leftbuff, "[%s]%d-%d-%d %d:%d:%d:", leveltostring(level), ctime->tm_year + 1900,
ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
char rightbuff[1024];
va_list s;
va_start(s, format);
vsprintf(rightbuff, format, s);
va_end(s);
char logtext[2048];
sprintf(logtext, "%s %s\n", leftbuff, rightbuff);
//printf(logtext);
printlog(level, logtext);
}
void printonefile(const string& logname, const string& logtxt)
{
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 string &logtxt)
{
//log.txt.info
string filename = dir + PATH;
filename += ".";
filename += leveltostring(level);
printonefile(filename, logtxt);
}
~Log(){};
private:
int printstyle;
string dir; //分类日志,放入目录中
};
// int sum(int n, ...)
// {
// int sum = 0;
// va_list s;
// va_start(s, n);
// while (n)
// {
// sum = sum + va_arg(s, int);
// n--;
// }
// return sum;
// }