epoll_create
头文件#include
函数原型:
int epoll_create(int size);
参数:
size:szie是一个忽略的值,但是必须大于0
返回值是一个文件描述符新epoll实例的文件描述符
调用epoll_create他在底层做了什么?他在操作系统层面上创建一个epoll模型,这个epoll模型用文件描述符访问
epoll_stl
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);参数:
epfd:epoll_cteate创建的epoll模型
op:添加文件描述符fd对应的事件,取值不同,事件就不同,如下是op参数取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
fd:
events:添加对应要检测的事件
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里.
epoll_ctl是将epoll模型中添加对应的事件和文件描述符,它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
epoll_wait
函数原型:
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参数:
epfd:epoll_create创建的epoll模型
events/maxevents输出型参数:由内核告诉用户那些文件描述符的那些事件已经就绪了,events用于存放已经就绪的事件,参数events是分配好的epoll_event结构体数组.epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size
timeout:等待时间
返回值:
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函
数失败.
之前都是让操作系统轮询检测要检测的文件描述符对应的事件是否就绪,但是这样的效率比较低下,因为每次轮询检测都可能只有少数事件就绪了。所以改变了原来地由操作系统轮询检测,变成了由就绪事件来找操作系统,然后由os把它添加到就绪事件集中,这样地机制可用回调机制完成,先让操作系统告诉文件描述符如果对应事件就绪了,就让它自己来找操作系统。
当某一进程调用epoll_create方法时,Linux内核会创建一个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
元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度
是O(1).
总结:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪;
随着节点地增多,效率不断增高。因为节点越多,能更快地凑齐事件就绪队列
当一个server收到一个request时,一次recv时没有读完完整地request,而且buff已经读满了,就要继续等等下一次事件就绪继续读request剩下的内容全部读完,因为这个buff是局部变量,当一次recv结束这个buff就会被释放,原来读取的request内容就会丢失,等到下一次在读取request剩下的内容后这就不是一个完整地request请求了。如果buff是全局变量,这个buff就会变成所有的文件描述符所有共有的,每次recv之后就会被覆盖,所谓地buff不应该局部和全局,应该每一个文件描述符都有一个buff,将读取到地数据放在自己地buff中。所以在epoll中实现了这个功能。
结构体中第一个events是事件,第二个是就是每一个文件描述符对应的buff缓冲区,我们使用的只是一个void*ptr。
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
struct bucket{
char buff[10];
int fd;
int num;//上一次读取的数据长度
bucket(int fd_):fd(fd_),num(0)
{
memset(buff,0,sizeof(buff));//清空
}
bucket(){
memset(buff,0,sizeof(buff));//清空
}
//memset(buff,0,sizeof(buff));//清空
};
class EpollServer{
private:
int lsock;
int post;
int mepoll;
public:
EpollServer(int _post){
post=_post;
}
void Init(){
lsock=socket(AF_INET,SOCK_STREAM,0);
if(lsock<0){
cout<<"socket error"<<endl;
exit(1);
}
struct sockaddr_in in_addr;
in_addr.sin_family=AF_INET;
in_addr.sin_port=htons(post);
in_addr.sin_addr.s_addr=INADDR_ANY;
if(bind(lsock,(struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"bind error"<<endl;
exit(0);
}
if(listen(lsock,5)<0){
cout<<"listen error"<<endl;
exit(4);
}
mepoll=epoll_create(128);
if(mepoll<0){
cout<<"epoll_create error"<<endl;
exit(3);
}
}
void Addevents(int sock,uint32_t opov){
//添加套接字到epoll模型
struct epoll_event event;
//添加写事件
event.events=opov;
//监听套接字不需要data缓冲区
if(sock==lsock){
event.data.ptr=NULL;
}else{
//给sock添加buff缓冲区
//添加文件描述符到bucket
event.data.ptr=new struct bucket(sock);
}
epoll_ctl(mepoll,EPOLL_CTL_ADD,sock,&event);
}
void start(){
Addevents(lsock,EPOLLIN);//服务器启动时先将监听套接字对应的事件添加到要检测的事件集中建立链接时读事件所以添加事件为EPOLLIN
while(1){
struct epoll_event events[1024];//返回的就绪事件集
//不用担心就绪事件集不够用,这一次没有全部装下,剩下的下次会一起处理
//就绪事件会从下标为0开始添加
int timeout=2000;
int num=epoll_wait(mepoll,events,1024,timeout);
//返回值是已经就绪的事件的个数
switch(num){
case 0:
cout<<"等待超时"<<endl;
break;
case -1:
cout<<"等待出错"<<endl;
break;
default:
//执行事件
cout<<"等待成功"<<endl;
service(events,num);
break;
}
}
}
void service(struct epoll_event events[],int num){
for(int i=0;i<num;i++){
uint32_t ev =events[i].events;//获取就绪事件
if(ev&EPOLLIN){
//读事件就绪
//因为链接也是读事件所以也要检测是不是监听套接字
//因为监听套接字的ptr设置为NILL了
if(events[i].data.ptr==NULL){
//链接事件
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
int sock=accept(lsock,(struct sockaddr*)&addr,&len);
if(sock>0){
cout<<"get a new link......."<<endl;
Addevents(sock,EPOLLIN);
}
}else if(events[i].data.ptr!=NULL){
//普通事件
//拿到这个事件对应的bucket
bucket *b=(bucket*)events[i].data.ptr;
//自上一次recv读取的缓冲区添加数据后再recv拷贝数据保证数据完整
int s=recv(b->fd,b->buff+b->num,sizeof(b->buff)-b->num/*总容量减去上一次拷贝到缓冲区的数据长度 */,0);
if(s>0){
b->num+=s;//保存这次读取数据的长度下一次从这开始添加
cout<<"client"<<b->buff<<endl;
//打印的buff中数据会越来越多
if(b->num>=sizeof(b->buff)){
//假如已经拿到完整定长requst
//就要发送response
//修改读事件为写事件
struct epoll_event event;
event.data.ptr=events[i].data.ptr;
event.events=EPOLLOUT;
epoll_ctl(mepoll,EPOLL_CTL_MOD,b->fd,&event);
}
}
else if(s==0){
close(b->fd);
epoll_ctl(mepoll,EPOLL_CTL_DEL,b->fd,NULL);//从epol模型l删除关闭的文件描述符号
cout<<"client have quited"<<endl;
//将对应的bucket缓冲区释放
delete b;
}
}
}else if(ev&EPOLLOUT){
//写事件就绪
bucket *b=(bucket*)events[i].data.ptr;
int s=send(b->fd,b->buff,sizeof(b->buff),0);
close(b->fd);
epoll_ctl(mepoll,EPOLL_CTL_DEL,b->fd,NULL);//从epol模型l删除关闭的文件描述符号
delete b;
}
else{
//其他事件
}
}
}
~EpollServer(){
close(lsock);
close(mepoll);//关闭epoll模型
}
};
void openov(string op){
cout<<op<<endl;
}
int main(int argc,char *argv[]){
if(argc!=2){
openov(argv[0]);
exit(0);
}
EpollServer *es=new EpollServer(atoi(argv[1]));
es->Init();
es->start();
return 0;
}