1.epoll函数为什么优于select函数
select函数的缺点:
- 调用select函数后,要针对所有文件描述符进行循环处理。
- 每次调用select函数,都需要向该函数传递监视对象信息。
对于缺点2,是提高性能的最大障碍。因为,套接字是操作系统来管理的,所以每次调用select函数,都会将要监视的对象信息传递给操作系统,这会对程序造成很大的负担。而且无法通过代码来解决,所以,缺点2是提高性能的最大障碍。
所以,有没有这么一种函数,仅向操作系统传递一次监控对象,当监视范围或内容发生变化时,只通知发生变化的事项呢?
答:epoll函数就具有问题里所说的功能。
适合select函数的使用情形:
- 系统需要具有兼容性。epoll函数是基于Linux系统的,而select函数几乎所有系统都有。
- 服务器端接入者少。
综上,epoll函数的优点:
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
- 调用对应于select函数的epoll_wait函数时,无需每次都传递监视对象信息,造成性能负担。
2.epoll函数
2.1 epoll_create函数
作用:创建保存epoll文件描述符的空间
#include <sys/epoll.h>
int epoll_create(int size); //size:epoll实例的大小
成功返回epoll文件描述符
失败返回-1
调用epoll_create函数时创建的文件描述符保存空间称为“epoll例程”。
size参数的传递,只是向操作系统提供建议,实际上操作系统会根据情况调整epoll例程的大小。更实际上的是,Linux2.6.8版本后的内核将完全忽略size参数。
注意:epoll_create函数创建的资源与套接字相同,都由操作系统来管理。所以返回的epoll文件描述符主要用于区分epoll例程的,需要终止时,也要和其他文件描述符相同,要调用close函数。
2.2 epoll_ctl函数
作用:向空间注册或注销文件描述符
#include<sys/epoll.h>
int epoll_ctl(
int epfd, //用于注册监视对象的epoll例程的文件描述符
int op, //用于指定监视对象的添加、删除、更改操作
int fd, //需要监视的文件描述符
struct epoll_event* event //监视对象的事件类型
);
成功返回0,失败返回-1
参数epfd:指定epoll例程空间
参数op:
值 | 含义 |
EPOLL_CTL_ADD | 将文件描述符注册到epoll例程 |
EPOLL_CTL_DEL | 将文件描述符从epoll例程中删除,第四个参数填NULL |
EPOLL_CTL_MOD | 更改注册的文件描述符的关注事件发生情况 |
参数fd:需要监视的文件描述符
参数event:
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void* ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t
events常量(可以通过位或“|”运算符来传递多个参数) | 含义 |
EPOLLIN | 需要读取数据的情况 |
EPOLLOUT | 输出缓冲为空,可以立即发送数据的情况 |
EPOLLPRI | 收到OOB数据的情况 |
EPOLLRDHUP | 断开连接或半关闭的情况,边缘触发下很有用 |
EPOLLERR | 发生错误的情况 |
EPOLLET | 以边缘触发的方式得到事件通知 |
EPOLLONESHOT | 发生一次事件后,相应文件描述符不再收到事件通知。 因此,需要调用epoll_ctl函数,运用第二个参数的EPOLL_CTL_MOD来再次设置事件 |
一般,epoll_event结构体里,只需要填写,events常量,和data联合体里的fd(要监视的文件描述符)即可。例如:
struct epoll_event event;
event.events=EPOLLIN;
event.data.fd=sockfd;
...
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
2.3 epoll_wait函数
作用:与select函数类似,等待文件描述符发生变化
#include<sys/epoll.h>
int epoll_wait(
int epfd, //epoll例程指向的文件描述符
struct epoll_event* events, //保存发生事件的文件描述符集合的结构体地址
int maxevents, //第二个参数中可以保存的最大事件数
int timeout //以1/1000秒为单位的等待时间,传递-1,则会阻塞直到发生事件
);
成功返回发生事件的文件描述符数量
失败返回-1
参数epfd: 指定的epoll例程空间
参数events:
需要分配动态空间(malloc),一般来说分配动态空间时epoll_event结构体的最大可存放数量,就是参数maxevents的值。
参数max_events:events指向的空间里,最大可保存的epoll_event结构体的数量
参数timeout:超时时间。
例如:
#define EPOLL_SIZE 50
struct epoll_event* ep_events;
ep_events=(struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
...
int event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
3.条件触发和边缘触发
条件触发和边缘触发发生在epoll_wait函数时。
3.1 条件触发
含义:当输入缓冲中存有数据时,就会一直触发该事件。例如:当客户端传来20个字节的数据到服务器端,假设服务器端每次只读取4个字节,那么服务器端在客户端中20个字节的数据没有读取完之前,会一直触发EPOLLIN事件,即epoll_wait函数会一直将此客户端的文件描述符给填入到epoll_event结构体里的fd参数里。epoll函数和select函数默认是条件触发。
其实现思路和select函数是差不多的。
条件触发服务器端:
...//头文件
#define EPOLL_SIZE 50
#define READ_SIZE 5
int main()
{
......//这里是正常的socket、bind、listen函数
int epollfd=epoll_create(EPOLL_SIZE);
epoll_event serverevent;
serverevent.events=EPOLLIN;
serverevent.data.fd=serverfd;
if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,serverfd,&serverevent))
{
std::cout<<"server epoll_ctl fail!"<<std::endl;
return 0;
}
int count;
epoll_event* occurevent;
occurevent=(epoll_event*)malloc(sizeof(epoll_event)*EPOLL_SIZE);
while(1)
{
count=epoll_wait(epollfd,occurevent,EPOLL_SIZE,-1);
std::cout<<"触发EPOLLIN事件!"<<std::endl;
for(int i=0;i<count;++i)
{
if(occurevent[i].data.fd==serverfd)
{
sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(clientAddr));
socklen_t clientAddrLen=sizeof(clientAddr);
int clientfd=accept(serverfd,(sockaddr*)&clientAddr,&clientAddrLen);
if(clientfd==-1)
{
std::cout<<"accept fail!"<<std::endl;
continue;
}
epoll_event tempevent;
tempevent.events=EPOLLIN;
tempevent.data.fd=clientfd;
if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,clientfd,&tempevent))
{
std::cout<<"epoll_ctl fail!"<<std::endl;
continue;
}
}
else
{
int clientfd=occurevent[i].data.fd;
char buff[1024];
int readLen=read(clientfd,buff,READ_SIZE);
if(readLen>0)
{
std::cout<<"客户端发来的消息:"<<buff<<std::endl;
write(clientfd,buff,readLen);
}
else if(readLen==0)
{
epoll_ctl(epollfd,EPOLL_CTL_DEL,clientfd,NULL);
close(clientfd);
}
}
}
}
close(serverfd);
close(epollfd);
return 0;
}
执行结果:
客户端:
服务器端:
可以看出服务器端一共触发了5次,第5次应该读的是‘/0’字符。
3.2 边缘触发
含义:当输入缓冲中接收到数据时,有且仅会触发一次事件。即使缓冲中的数据没有读取完,也不会再触发了,只有当缓冲中数据读取完全,下一次有数据传输到缓冲中时,才会再次触发。epoll函数要设置成条件触发需要在调用epoll_wait函数时传入EPOLLET参数。
边缘触发服务器端需要注意以下两点:
1.通过errno变量验证错误原因(因为边缘触发的特性,所以每次触发事件,都需要将缓冲中的数据给读完。)
当缓冲中的数据读完时,read函数会返回-1,表示产生了一个错误,这时仅凭这些内容无法得到产生错误的原因,所以,Linux提供了一个全局变量:
#include<error.h>
int errno;
这个变量存储者错误的常量代码,即当缓冲中的数据读完时,read函数会返回-1,同时,errno变量会被赋值为EAGAIN常量。所以此时你可以用这个常量判断缓冲中的数据是否读完。
int res=read(...);
if(res>0)
{
...
}
else if(res==-1&&errno==EAGAIN) //表明缓冲中数据已经读完了
{
...
}
else
{
...
}
2.要更改套接字特性,完成非阻塞I/O。
在边缘触发方式下,以阻塞方式工作的read&write函数有可能会引起服务器端长时间的停顿。所以要完成非阻塞I/O。
那么怎么完成非阻塞I/O?
答:使用fcntl函数。
此函数在 [C++ 网络协议] 多种I/O函数里有说过,当时是修改recv函数的第四个参数,发送MSG_OOB带外数据时,作为接收方为了避免当有多个进程时不能判断是哪个进程要执行信号处理函数的问题,而将当前套接字的处理进程设置为主进程,代码如:
fcntl(clientfd,F_SETOWN,getpid());
#include<fcntl.h>
int fcntl(
int filedes, //属性更改目标的文件描述符
int cmd, //函数调用目的
...
);
成功返回cmd参数相关值
失败返回-1
cmd参数 | 含义 |
F_GETFL | 获取filedes参数所指的文件描述符属性(int型,代表其属性) |
F_SETFL | 设置其文件描述符属性 |
非阻塞I/O的属性是O_NONBLOCK。
使用示例:
int flag=fcntl(fd,F_GETFL,0); //先获取当前文件描述符的属性
fcntl(fd,F_SETFL,flag|O_NONBLOCK); //将文件描述符的属性和非阻塞IO属性位或
边缘触发服务器端:
......
#define EPOLL_SIZE 50
#define READ_SIZE 5
int main()
{
......
int epollfd=epoll_create(EPOLL_SIZE);
epoll_event serverevent;
serverevent.events=EPOLLIN;
serverevent.data.fd=serverfd;
if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,serverfd,&serverevent))
{
std::cout<<"server epoll_ctl fail!"<<std::endl;
return 0;
}
int count;
epoll_event* occurevent;
occurevent=(epoll_event*)malloc(sizeof(epoll_event)*EPOLL_SIZE);
while(1)
{
count=epoll_wait(epollfd,occurevent,EPOLL_SIZE,-1);
std::cout<<"触发事件!"<<std::endl;
for(int i=0;i<count;++i)
{
if(occurevent[i].data.fd==serverfd)
{
sockaddr_in clientAddr;
memset(&clientAddr,0,sizeof(clientAddr));
socklen_t clientAddrLen=sizeof(clientAddr);
int clientfd=accept(serverfd,(sockaddr*)&clientAddr,&clientAddrLen);
if(clientfd==-1)
{
std::cout<<"accept fail!"<<std::endl;
continue;
}
//与条件触发不同之处(1)******************************************************
int flag=fcntl(clientfd,F_GETFL,0);
fcntl(clientfd,F_SETFL,flag|O_NONBLOCK);
//与条件触发不同之处(1)******************************************************
epoll_event tempevent;
//与条件触发不同之处(2)******************************************************
tempevent.events=EPOLLIN|EPOLLET;
//与条件触发不同之处(2)******************************************************
tempevent.data.fd=clientfd;
if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,clientfd,&tempevent))
{
std::cout<<"epoll_ctl fail!"<<std::endl;
continue;
}
}
else
{
int clientfd=occurevent[i].data.fd;
//与条件触发不同之处(3)******************************************************
while(1)
{
char buff[1024];
int readLen=read(clientfd,buff,READ_SIZE);
if(readLen==-1 && errno==EAGAIN) //说明已经读完了数据
{
std::cout<<"客户端发送的消息已读完!"<<std::endl;
break;
}
else if(readLen>0)
{
std::cout<<"客户端发来的消息:"<<buff<<std::endl;
write(clientfd,buff,readLen);
}
else if(readLen==0)
{
epoll_ctl(epollfd,EPOLL_CTL_DEL,clientfd,NULL);
close(clientfd);
break;
}
}
//与条件触发不同之处(3)******************************************************
}
}
}
close(serverfd);
close(epollfd);
return 0;
}
执行结果:
客户端:
服务器端:
可以看到边缘触发与条件触发执行后的区别。
3.3 条件触发和边缘触发的比较
从服务器端实现模型的角度来分析:边缘触发可以分离接收数据和处理数据的时间点。
例如:
此服务器运行流程如下:
- 服务器端分别从A,B,C接收数据
- 服务器端按照A,B,C的顺序重新组合收到的数据
- 组合的数据将发送给任意主机
要完成该过程,要按如下流程运行程序:
- 客户端按照A,B,C的顺序连接服务器端,并依序向服务器端发送数据
- 需要接收数据的客户端应在客户端A,B,C之前连接到服务器端并等候
但实际上,会出现不同的状况,如:
- 客户端C,B正向服务器端发送数据,但A还未连接到服务器端
- 客户端A,B,C乱序发送数据
- 服务器端已收到数据,但要接收数据的目标客户端还未连接到服务端
所以,这时如果使用边缘触发,那么就可以让服务器来决定读取和处理的时间点,而如果是条件触发,那么如果服务器打算延时处理,那么服务器就会不停的收到事件触发,导致服务器端不堪承受。