epoll是为处理大量句柄而做了改进的poll
句柄是一个用来标识对象或者项目的
标识符
,可以用来描述窗体、文件等
int epoll_create(int size);
创建一个epoll的句柄。
size大小可以随意传入,是一个被废弃的参数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事。第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;返回值
成功返回0
struct epoll_event
{
uint32_t events;
epoll_data_t date;
};
union epoll_data_t
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
参数 | 作用 |
EPOLLIN | 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭); |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的 |
EPOLLONESHOT | 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要 再次把这个socket加入到EPOLL队列里。 |
int epoll_wait(int epfd, struct epoll_event * events,
int maxevents, int timeout);
获取在epoll监控中已经发生的事件
events
是分配好的epoll_event结构体数组.epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
timeout超时时间 (毫秒, 0会立即返回, -1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败
细节一:如果底层有很多就绪的sock,revs承装不下,怎么办?
没事,一次拿不完。下次接着拿。
细节二:关于epoll_wait的返回值问题
返回就绪的个数。有几个fd上的事件就绪,就返回几,epoll返回的时候,会将所有就绪的event按照顺序放入到revs数组中,一共有返回值个。
先回答两个问题:操作系统怎么知道网卡里面有数据了?操作系统怎么知道键盘有用户输入了呢?
硬件中断!在操作系统中存在一个中断向量表,其中对应着不同硬件中断对应的处理方法。当用户在键盘输入时,电信号会转变为一个具体的数字放在寄存器中,OS就会通过这个数字在中断向量表里查询对应的处理方法,从而将网卡中的数据读到操作系统中。
在epoll_creat时OS首先在内核创建一个epoll模型,该模型有一个红黑树的结构,一些回调方法,和一个就绪队列。 当用户需要epoll去关心某些文件时,OS会在红黑树中添加节点,节点里面至少包括了文件描述符和关心的事件类型等信息。然后OS会自动检测这些事件资源是否就绪,如果就绪,会通过设置的一些回调方法自动处理,并会创建一个新的就绪节点。随后将这个节点挂到就绪队列。
再谈epoll的三个调用接口
epoll_create 就是构建红黑树、就绪队列、建立回调机制这三个过程。即创建一个epoll模型。
epoll_ctl 就是修改底层红黑树(增删改)
epoll_wait 就是从就绪队列拿就绪的资源。
epoll_create返回值为fd的原因
首先在进程中,会有task_struct结构体,里面有文件描述符表,这些函数的返回值就是建立epoll模型的文件描述符。OS可以通过这个文件描述符找到对应的文件,该文件会中有一个指针,就指向epoll模型。
1.红黑树是k,v结构的,key值刚好可以由文件描述符充当,所以用户后续拿走就绪事件时,也会按照一定的顺序。
2.用户只需要设置关系,获取结果即可,不用关心任何对fd和event的管理细节。
3.在底层中使用红黑树代替了数组。并且不再通过遍历一遍遍的看资源是否就绪,而是资源就绪时,OS会采用回调函数直接帮我们处理。
4.底层只要有fd就绪,OS会自动构建节点,然后连入到就绪队列中。上层只要不断的从就绪队列中将数据拿走,就完成了获取就绪事件的任务。发现,内核或者用户都会操作这个epoll模型,也就是说epoll模型是临界资源,本质上就是一个生产者消费者模型。在epoll中已经保证了线程安全。
5.如果就绪队列里没有就绪时间。可以自由的设置阻塞/非阻塞/自定义等待时间。
注:本博文只实现了对于读入的处理方法
#pragma once
#include
#include
#include
class Epoll
{
public:
static const int gsize=256;
static int CreateEpoll()
{
int epfd=epoll_create(gsize);
if(epfd>0) return epfd;
else
{
exit(5);
}
}
//对哪个epoll模型 什么操作 哪个文件 事件是什么EPPLLIN?EPOLLOUT?
static bool CtlEpoll(int epfd,int operator,int sock,uint32_t events)
{
struct epoll_event ev;
ev.events=events;
ev.data.fd=sock;
int n=epoll_ctl(epfd,operator,sock,&ev);
return n==0;
}
static int WaitEpoll(int epfd,struct epoll_event revs[],int num,int timeout)
{
return epoll_wait(epfd,revs,num,timeout);//返回就绪的个数
}
};
我们把方法暴露给外面 可以通过传入不同的参数指针 对结果进行不同的处理。
#include "EpollServer.hpp"
#include
using namespace std;
using namespace ns_epoll;
void change(std::string request)
{
//完成业务逻辑
std::cout << "change : " << request << std::endl;
}
int main()
{
unique_ptr epoll_server(new EpollServer(change));
epoll_server->Start();
return 0;
}
注意当我们listensock的初始化进行完成后,不能直接accept(),因为不知道什么时候底层连接就绪,所以一定要直接把listensock添加到epoll里面,让它帮我们等待。
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__
#include
#include
#include
#include "Epoll.hpp"
#include "Sock.hpp"
#include "log.hpp"
#include
namespace ns_epoll
{
static const int default_num=8080;//默认的端口号
const static int gnum=64;//创建的可以存放就绪事件数组的最大长度
using std::cout;
using std::cin;
using std::string;
class EpollServer
{
public:
using func_t = std::function;
//将对于数据的使用暴露给外面 传进来什么 就怎么操作
EpollServer(func_t HandlerRequset,const uint16_t& port=default_num)
:_port(port)
,_HandlerRequset(HandlerRequset)
,_revs_num(gnum)
{
//0.由于在私有成员中添加了一个获得就绪时间的数组指针
//需要开辟空间 以方便以后使用
_revs=new struct epoll_event[_revs_num];
//1. 创建_listensock
_listensock=Sock::Socket();
Sock::Bind(_listensock,_port);
Sock::Listen(_listensock);
//2. 创建epoll模型
_epfd=Epoll::CreateEpoll();
logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);
//3.将_listensock加入epoll
if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN)) exit(6);
logMessage(DEBUG, "add listensock to epoll success.");
}
~EpollServer()
{
if(_listensock>=0) close(_listensock);
if(_epfd>=0) close(_epfd);
if(_revs) delete[] _revs;
}
private:
int _listensock;
int _epfd;
uint16_t _port;
struct epoll_event *_revs;//用于一次性获取多个就绪事件的数组
int _revs_num;//获得就绪数组的大小
func_t _HandlerRequset;//使用的方法是什么
};
}
#endif
Start中对于LooOnce的封装其实没什么用,就是把单次循环和死循环拆开了。
当程序刚开始,epoll中只有_listensock是关心的。如果它就绪,说明连接完成。可以调用accept()去获取这个就绪事件了。当然随着程序运行,肯定会获得越来越多的套接字,所以我们设定方法去调用不同的处理方法。
void Start()
{
int timeout = -1;
//阻塞式等待
while(true)
{
LoopOnce(timeout);
}
}
//循环一次
void LoopOnce(int timeout)
{
int n = Epoll::WaitEpoll(_epfd, _revs, _revs_num, timeout);
//if(n == _revs_num) //扩容
switch (n)
{
case 0:
logMessage(DEBUG, "timeout..."); // 3, 4
break;
case -1:
logMessage(WARNING, "epoll wait error: %s", strerror(errno));
break;
default:
// 等待成功
logMessage(DEBUG, "get a event");
HandlerEvents(n);
break;
}
}
void HandlerEvents(int n)
{
assert(n>0);
//走进来就一定有资源就绪了 看看是哪种事件
//_revs里面存放着所有的就绪事件 我们挨着处理就可以
for(int i=0;i
注意
这里Recver()按道理来说是需要制定协议的。这里为了简单测试,就当字符串处理,从而尽可能的减少没有协议带来的影响。
void Acceptr(int listensock)
{
std::string clientip;
uint16_t clientport;
int sock=Sock::Accept(listensock,&clientip, &clientport);
if(sock<0)
{
logMessage(WARNING, "accept error!");
return;
}
//不知道什么时候才会发送消息,接着让epoll帮我们等
if( ! Epoll::CtlEpoll(_epfd,EPOLL_CTL_ADD,sock,EPOLLIN) ) return ;
logMessage(DEBUG, "add new sock : %d to epoll success", sock);
}
void Recver(int sock)
{
char buffer[1024];
ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
_HandlerRequset(buffer);//用外部传入的方法进行处理数据
}
else if(n==0)
{
// 1. 先在epoll中去掉对sock的关心
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
// 2. 再close文件
close(sock);
logMessage(NORMAL, "client %d quit, me too...", sock);
}
else
{
// 1. 先在epoll中去掉对sock的关心
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
// 2. 在close文件
close(sock);
logMessage(NORMAL, "client recv %d error, close error sock", sock);
}