I/O多路转接之poll链接:链接: link.
epoll它几乎具备了之前所说的一切优点,摒弃了一切的缺点被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
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);
epoll的事件注册函数.
第二个参数的取值:
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符
- EPOLLOUT : 表示对应的文件描述符
- EPOLLPRI : 表示对应的文件描述符
- EPOLLERR : 表示对应的文件描述符
- EPOLLHUP : 表示对应的文件描述符
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
- 再次把这个socket加入到EPOLL队列里.
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
创建一颗红黑树,红黑树的本质是一颗近似平衡的二叉搜索树,当有需要关心什么文件描述符所对应的什么事件的时候,就好比喻创建了一个红黑树结点,里面放着fd和event,然后把他插入到红黑树当中,当事件就绪时,就可以执行原先在创建时所写的回调函数,然后把回调函数返回的event放到最开始所创建的就绪队列中。
总结一下, epoll的使用过程就是三部曲:
比较官方的解释步骤,但是不太通俗易懂,如果想要捋顺核心步骤,看上面几句话即可。
- 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件. 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
- 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中. 在epoll中,对于每一个事件,都会建立一个epitem结构体.
- 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
- 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
select和poll需要轮询的检测那些文件描述符就绪了,在这个过程中既检查了已经就绪的,还检查了没有就绪的,然而检查了没有就绪的这个过程无非是一个浪费时间的过程。epoll就不一样了,他每次都是直接从就绪队列中拿已经就绪的文件描述符,这个过程根本不可能拿到没有就绪的文件描述符,所以对于epoll来说找到已经就绪的文件描述符的时间复杂度是O(1),然而select和poll的时间复杂度则为O(N).(其实还可以用一个小故事来解释:老师说:把作业都放到座位上,我要挨着检查谁没有写作业,那么这个过程就会即检查到已经写作业的也会检查到没有写作业的同学(也就是select和poll的方式)。换种说法:现在已经把作业写完的同学,就拿过来让我检查(这句话就相当于与建立了回调函数,也就是提供了一种方法,当达到什么条件的时候,就可以去做某件事),然后就可以回家,那么此时来找老师检查的同学就都是已经把作业写完的同学了,就不会检查到没有写作业的,这就是epoll的方式)
就想要实现一个client端给server端发送一个定长的字符串,当达到这个定长字符串长度之后,server端给client端返回刚才给server发送的内容,然后关闭连接。(其实这个过程就是http中的request和response,当server端不接收到完整的一个request的时候,是不会作出response的)
main.cc
#include"EpollServer.hpp"
void Usage(std::string proc)
{
cout <<"Usage :\n\t" << proc << " port"<<endl;
}
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
EpollServer *es = new EpollServer();
es->InitServer();
es->Start();
return 0;
}
Sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BACKLOG 5
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
cerr << "socket error!" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock,int port)
{
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr << "bind error!"<< endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock,BACKLOG) < 0)
{
cerr << "listen error!"<< endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock,(struct sockaddr*)&peer,&len);
if(fd < 0)
{
cerr << "accept error!"<<endl;
}
return fd;
}
static void SetsockOpt(int sock)
{
//因为对于server来说,主动断开连接的时候会进入time_wait状态,所以要端口复用
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
}
};
EpollServer.hpp
#include"Sock.hpp"
#include
#define SIZE 64
//可以理解为定制了一个协议
class bucket
{
public:
char buffer[20];//这里所设定的是一个定长数组,可以把它换成我们所需要的request,如果不把这个完整的request接收完,那么服务器将不会给他response
int pos;//这里定义这个变量,就是为了解决网络通信中的request可能一次性读不完,记录一下,直到读完这个resquest才算结束
int fd;
bucket(int sock):fd(sock),pos(0)
{
memset(buffer,0,sizeof(buffer));
}
~bucket()
{}
}; //目的就是要让每一个文件描述符都有一个对应的缓冲区,因为有可能一次读取上来的数据并不是完整的
class EpollServer
{
private:
int lsock;
int port;
int epfd; //需要一个epoll模型
public:
EpollServer(int _p = 8081):port(_p)
{}
void InitServer()
{
lsock = Sock::Socket();
Sock::Bind(lsock,port);
Sock::Listen(lsock);
epfd = epoll_create(256); //这里面的参数并不重要,随便写
if(epfd < 0){
cerr << "epoll_create error!"<< endl;
exit(5);
}
cout << "listen socket : " <<lsock << endl;
cout << "epfd : " <<epfd << endl;
}
void AddEvent2Epoll(int sock,uint32_t event)
{
//对于第二个参数可以理解为动作增 删 修改三种,最后一个参数接收的是struct epoll_event* event
struct epoll_event ev;
ev.events = EPOLLIN;
//但是这里还没有完,因为对于struct epoll_event结构体来说还有一个date变量
if(sock == lsock){
//对于监听套接字是可以不设置缓冲区的,因为他只要是负责接收链接的,并不负责数据交互
ev.data.ptr = nullptr;
}
else{
ev.data.ptr = new bucket(sock);//要让每个文件描述符都有一个对应的缓冲区
}
epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev);
}
void DelEventFromEpoll(int sock)
{
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
}
void HandlerEvents(struct epoll_event revs[],int num)
{
//这里就相当于从就绪队列中取出来
for(int i = 0; i < num; ++i)
{
uint32_t ev = revs[i].events;//已经就绪的事件的内容
if(ev & EPOLLIN)
{
//读事件就绪还分为两种
if(revs[i].data.ptr != nullptr){
//说明是一个普通的sock
bucket *bp = (bucket*)revs[i].data.ptr;
//在添加sock进epoll模型的时候,给对应的sock开了一个属于他的缓冲区
//一直读到bucket里面的buffer满的时候,再开始response
ssize_t s = recv(bp->fd,bp->buffer+bp->pos,sizeof(bp->buffer)-bp->pos,0); //recv是4个参数,此时最后一个参数已经不起作用了
if(s > 0){
//如果是request那就一直读,读到空行就结束
bp->pos += s;
cout << "client# " << bp->buffer<<endl;
if(bp->pos >= sizeof(bp->buffer)){
//可以理解为此时request已经接收完了,此时服务器应该开始response
//但是此时并不知道这个文件描述符的写是件是否就绪了,所以还需要把这个sock从原来的关心读时间修改为写事件
struct epoll_event temp;
temp.events = EPOLLOUT;
temp.data.ptr = bp;
epoll_ctl(epfd,EPOLL_CTL_MOD,bp->fd,&temp);
}
}
else if(s == 0){
//close(bp->fd);
//还需要把这个结点从红黑树里面拿出来
//epoll_ctl(epfd,EPOLL_CTL_DEL,bp->fd,nullptr);//因为这是一个红黑树,所以通过Key值就可以找到对应的value,所以这里不再需要写清楚这个value
//原来还给他创建了一个bucket,也要删除掉
DelEventFromEpoll(bp->fd);
delete bp;
}
else{
cerr << "recv error"<< endl;
epoll_ctl(epfd,EPOLL_CTL_DEL,bp->fd,nullptr);
delete bp;
}
}
else{
//listen sock
int sock = Sock::Accept(lsock);
if(sock > 0){
//把新的文件描述符添加到epoll模型当中
AddEvent2Epoll(sock,EPOLLIN);//相当与创建结点,添加到红黑树当中
}
}
}
else if(ev & EPOLLOUT){
//此时说明你原来给我发的20个字节我已经都读到了
//因为返回的内容都还在这个sock所对应的buffer中存放着
bucket *bp = (bucket*)revs[i].data.ptr;
send(bp->fd,bp->buffer,sizeof(bp->buffer),0);
//server端进行一个echo,然后就算结束了一次完整通信,就可以关闭文件描述符了
DelEventFromEpoll(bp->fd);
delete bp;
}
else{
//other events
}
}
}
void Start()
{
//此时和select/poll都一样,最开始的时候只关心lsock的读时间是否就绪(是否有新的链接到来)
//对epoll_ctl在进行一层封装最大的好处就是方便我们后续使用
AddEvent2Epoll(lsock,EPOLLIN);
//int timeout = 1000;
struct epoll_event revs[SIZE];//把所有已经就绪的文件描述符给我们放到这个缓冲区里面
for(;;){
//走到这里的时候其实OS已经知道了应该帮你关心那些文件描述符上面的那些事件是否发生
//但是这里可能会有一个疑惑?就是缓冲区定义了SIZE大小也就是64,但是假设现在已经就绪了500个文件描述符呢?
//事实上并不影响,拿下来多少个就处理多少个,没拿下的处理的,下一次就会去拿过来处理
//这里的SIZE就相当于原来所学的,你期望处理多少个,但是num作为返回值表示实际要处理多少个文件描述符
//SIZE个那我怎么知道是哪几个文件描述符就绪呢?难道是要遍历整个SIZE个吗? 并不是,他会把已经就绪的文件描述符从0号数组下标依次放入,所以只需要遍历num个就好了
int num = epoll_wait(epfd,revs,SIZE,-1);//因为第二个和第三个都是输出型参数
switch(num){
case 0:
cout << "time out!"<< endl;
break;
case -1:
cerr << "epoll_wait error!"<<endl;
break;
default:
HandlerEvents(revs,num);
break;
}
}
}
~EpollServer()
{
close(lsock);
close(epfd);//因为epfd模型规定要关闭,并且他们本身也都是文件描述符
}
};
你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:
假如有这样一个例子:
简单说:如果底层的数据就绪,那么就一直向上层通知有数据准备好了,快来取,如果没有取完,那么也会不停的发送通知。
epoll默认状态下就是LT工作模式.
- 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
- 如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
- 支持阻塞读写和非阻塞读写
简单说:底层数据来了,回想上层通知一次,数据准备就绪,快来取,如果没有取或者一次性没有取完,那么数据就会被丢弃,且再不会向上层发送通知。
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些(简单说为什么ET比LT高效:因为ET的通知方式没有做重复的动作). 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的
另一方面, ET 的代码复杂程度更高了
epoll惊群
epoll的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.
喜欢问关于select、poll、epoll的问题
- select的缺点、poll的缺点、各自的缺点以及不同
- epoll接口的基本使用、为什么会那么高效、和前两种有什么差别
- 他们的使用场景:很多的长链接
参考blog:
epoll的惊群详解链接: link.