epoll和select以及poll不一样,它是linux独有的一种方法,在linux2.6.11内核版本之后才能够使用。
epoll的实现原理和poll、select不一样,首先他最大的不同就是它是按照一组函数来指执行的,其次,它把用户关心的文件描述符上的事件放在内核的一个事件表中,这个事件表的底层实现是红黑树,所以无需每次将文件描述符或事件集传入函数中,但epoll需要一个特别的文件描述符来唯一标识这个事件表。
文件描述符的使用时通过epoll_create函数来创建的,其定义如下:
#include<sys/epoll.h>
int epoll _create (int size);
size参数现在并不起作用,只是给内核一个提示,告诉它时间表需要多大。该函数返回的文件描述符将用做其他所有的epoll系统调用的第一个参数,以指定要访问的内核事件表。
该函数执行成功返回事件表的标识符,失败返回-1。
通过epoll_ctl函数来对事件表进行操作:
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, steuct epoll_event* event);
epfd 为要操作的事件表标识,fd为操作的文件描述符,op为操作的类型
操作的类型主要有3种:
参数event 指定的事件,它是epoll_event结构体指针的类型,其定义如下;
struct epoll_event
{
_uint32_t events;
epoll_data_t data;
};
events成员描述的事件,epoll支持的事件和poll基本相同,只是在poll事件的宏之前加上“E”,但是epoll也有特别的事件类型:EPOLLET和EPOLLONESHOT.它们对于epoll的高效运行非常的关键。
其次epoll_data_t 类型的定义如下:
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
由此可见epoll_data_t是一个联合体,其中使用最多的就是fd,表示指定事件所从属的目标文件描述符,ptr成员指向的是与fd相关的用户数。但由于epoll_data_t是一个联合体,所以不能够同时使用fd和ptr,为了将文件描述符和用户数据关联起来,我们可以将fd放弃,将fd保存在ptr指向的数据中。从而实现关联文件描述符和用户数据。
epoll_ctl执行成功会返回0,失败会返回-1。
epoll系统调用主要的接口就是epoll_wait函数,它会用一段时间来等待文件描述符是否就绪,原型如下:
#include<sys/epoll.h>
int epoll_wait(int epfd, struct spoll_event* events, int maxevents, int timeout);
该函数执行成功返回就绪的文件描述符,失败返回-1。
timeout参数和poll接口是一样的,maxevent表示最多监听的文件事件,其值必须大于0。
该函数的功能是将所有就绪的事件从内核事件表中复制到它的第二个参数event指向的数组中买这个数组只用于输出epoll_wait 检测到的就绪事件,而不想select和poll一样既用于传入用户注册事件,又用于输出内核检测到的就绪事件,提高了程序索引就绪文件描述符的效率,之前需要O(n),使用epoll_wait后只需要O(1)。
epoll内核采用回调函数进行管理文件描述符,当文件描述符就绪之后,就会调用回调函数,将就绪的文件描述符和就绪事件插入到要返回的链表中。执行顺序为先轮询,在回调。
epoll的实现有两种,分别是ET(边沿触发)和LT(电平触发)。LT是内核默认的工作方式这种方式下的epoll相当于一个效率比较高的poll。当往epoll内核事件表中注册一个文件描述符上的EOLLET事件,epoll将以ET模式来操作该文件描述符,ET模式是epoll高效工作模式。 两种模式的区别: 对于在LT模式下的文件描述符,当epoll_wait函数监测到就绪事件之后,会通知应用程序,此时应用程序可以先不进行处理这个事件,当下次再调用epoll_wait函数的时候,该函数会再次通知应用程序这个事件,直到这个事件被处理。 对于在ET模式下的文件描述符,当epoll_wait函数监听到事件并通知应用程序之后,应用程序必须要立即处理该事件,因为之后,epoll_wait函数不会再通过该事件了。 由此可见:ET模式很大程度降低了同一个epoll事件被重复触发的次数。因此效率比LT高。
测试代码:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<errno.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<sys/types.h>
#include<pthread.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符设置为非阻塞
int setnonblocking (int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
//向事件表中添加事件
void addfd(int epollfd, int fd, int enable_et)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if(enable_et)//通过enable_et表示来控制是否设置为ET模式
{
event.events|= EPOLLET;
}
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
//LT工作模式
void lt(struct epoll_event* events, int number, int epollfd, int listenfd)
{
char buff[BUFFER_SIZE];
for(int i=0; i<number; ++i)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)
{
struct sockaddr_in ser;
int len = sizeof(ser);
int c = accept(listenfd, (struct sockaddr*)&ser, &len);
addfd(epollfd, c, 0);
}
else if(events[i].events & EPOLLIN)
{//当数据缓冲区里面还有数据的时候,会重复触发
printf("event trigger once \n");
memset( buff, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buff, BUFFER_SIZE-1, 0);
if(ret <= 0)
{
close(sockfd);
continue;
}
printf("get %d bytes of content : %s \n", ret, buff);
send(sockfd, "OK", 2, 0);
}
else
{
printf("something else happened \n");
}
}
}
//ET工作模式
void et(struct epoll_event* events, int number, int epollfd, int listenfd)
{
char buff[BUFFER_SIZE];
for(int i=0; i<number; ++i)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)
{
struct sockaddr_in ser;
int len = sizeof(ser);
int c = accept(listenfd, (struct sockaddr*)&ser, &len);
addfd(epollfd, c, 1);
}
else if(events[i].events & EPOLLIN)//判断是否为EPOLLIN事件
{
//即使数据缓冲区里还有数据,这段代码也不会重复触发,所以使用循环来输出缓冲区中的数据。
printf("event trigger once\n");
while(1)
{
memset(buff, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buff, BUFFER_SIZE-1, 0);
if(ret < 0)
{
/*对于非阻塞的IO,下面条件成立表示数据已经全部读取完毕,此后epoll调用就能在次触发sockfd上的EPOLLIN事件,以驱动下一次读操作。errno为全局变量,当recv函数出错之后(出错返回-1),内核会将错误类型记录在errno中
*/
if( (errno == EAGAIN) || (errno == EWOULDBLOCK) )
{
printf("read later\n");
break;
}
close(sockfd);
break;
}
else if(ret == 0)
{
close(sockfd);
}
else
{
printf("get %d bytes of content : %s \n", ret, buff);
send(sockfd, "OK", 2, 0);
}
}
}
else
{
printf("something else happened \n");
}
}
}
int main()
{
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
//配置网络号和端口号
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
int ret = bind(listenfd, (struct sock_addr*)&ser, sizeof(ser));
assert(ret != -1);
listen(listenfd, 5);
struct epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd( epollfd ,listenfd, 1);
while(1)
{
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (ret < 0)
{
printf(" epoll failure\n");
break;
}
// lt (events, ret, epollfd, listenfd);
et (events, ret, epollfd, listenfd);
}
close(listenfd);
return 0;
}
客户端代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符
assert(sockfd!=-1);
struct sockaddr_in ser_addr;
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(6000);
ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=connect(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));//创建一个连接
while(1)
{
printf("please input\n");
char buff[128]={0};
fgets(buff,128,stdin);//从标准输入设备获取
if(strncmp(buff, "bye",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);//发送消息
char data[128]={0};
int n=recv(sockfd,data,sizeof(data),0);//接受消息
printf("%s\n", data);
}
close(res);
exit(0);
}
测试结果:
当使用LT模式的时候:
客户端的输入和输出:
服务器端的结果:
当使用ET模式的测试结果:
客户端输入和输出:
服务器端的结果:
通过比较这两种模式的运行结果,可以看出,LT模式的事件触发的次数远多于ET模式。
对于为什么要将文件描述符设置为非阻塞的,这里解释一下:
因为每个使用ET模式的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作(调用recv或send函数时)将会因为没有后续的事件而一直处于阻塞状态,而不会返回。
EPOLLONESHOT事件
即使在ET模式下,一个socket上的某个事件还是有可能会被多次触发,比如在多线程的环境里面,一个线程在获取某个socket上的数据后开始处理树立,而这时这个socket又有数据可读,这时就会触发一个新的进程去读取这个数据。这时就会导致一个socket被多个线程所获取,这显然不是我们手所期望的。我们希望同一个socket在任意时刻只能被一个线程所处理。这一点可是使用EPOLLONESHOT来实现。
注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发器注册的一个可读、可写或异常的事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理一个socket的时候其他线程就不能再获取这个socket。但每个线程处理完这个socket之后,必须重置一下这个socket上EPOLLONESHOT事件。以确保之后的线程还是可以使用这个socket。
参考《Linux高性能服务器编程》