epoll是Linux系统特有的I/O复用函数。它和select和poll有很大的区别,首先它使用的是一组函数来完成任务而不是一个函数;其次,epoll是将用户关心的文件描述符的事件放在内核中的一个事件表中,从而无须像select和poll每次重复传入文件描述符或者事件集,但是epoll需要一个额外的文件描述符用来唯一标识内核中的事件表。epoll针对的是同一时刻成千上万的文件描述符需要监听的情况,毕竟像select和poll那种轮询的方式来监听文件描述符的话,效率是极其低下的。
epoll有两种模式,一种是默认工作模式LT模式,另外一种是高效工作模式ET模式。
LT模式:某一个文件描述符上一旦有数据就绪,就提醒应用程序,一次没有处理完或者未处理的话,会继续提醒,直到处理完。
ET模式:某一个文件描述符上一旦有数据,就提醒应用程序一次,数据没有处理或者没有处理完全,下一轮epoll也不会再提醒,除非是第二波的数据到了,再提醒;因此epoll在ET模式下,数据就绪后只提醒应用程序一次。
先来说一下LT模式,不论是LT模式还是ET模式都会使用以下三个函数实现操作:
int epoll_create(int size);//创建内核事件表;size的作用只是告诉内核事件表的大小,其返回值是用来唯一标识该事件表的文件描述符,即下面函数的第一个参数epfd。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);//添加文件描述符&移除文件描述符;第二个参数是指定操作类型,一般有三种操作:
EPOLL_CTL_ADD: 往事件表中注册fd上的事件
EPOLL_CTL_DEL :删除fd上注册的事件
EPOLL_CTL_MOD:修改fd上的注册事件
event指定事件类型,其本身是一个结构体:
struct epoll_event
{
unsigned int event;
epoll_data_t data;
};
int epoll_wait(int epfd,struct epoll_event*events,int max,int timeout);//检查就绪文件描述符,返回给应用程序,只返回就绪的文件描述符。该函数返回成功时,返回就绪文件描述符的个数,失败则返回-1,max指定监听的最大个数,timeout是超时设定,单位是毫秒。
下面利用这些函数写一个简单的服务器,感受一些效果。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#define MAX 10
int sock_create();
void epoll_add(int epfd,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
perror("epoll_ctl error");
}
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
{
perror("epoll_ctl error");
}
}
int main()
{
int sockfd = sock_create();
int epfd = epoll_create(MAX);
assert(epfd != -1);
epoll_add(epfd,sockfd);
struct epoll_event fds[MAX];
while(1)
{
int n = epoll_wait(epfd,fds,MAX,5000);
if(n == -1)
{
perror("epoll_wait error");
continue;
}
if(n == 0)
{
printf("time out\n");
continue;
}
else
{
int i = 0;
for(;i<n;i++)
{
if(fds[i].events & EPOLLIN)
{
if(fds[i].data.fd == sockfd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(fds[i].data.fd,(struct sockaddr*)&caddr,&len);
assert(c != -1);
epoll_add(epfd,c);
continue;
}
else
{
char buff[128]={0};
if(recv(fds[i].data.fd,buff,1,0)>0)
{
printf("read:%s\n",buff);
memset(&buff,0,128);
send(fds[i].data.fd,"ok",2,0);
sleep(2);
continue;
}
else
{
printf("%d client over!\n",fds[i].data.fd);
int fd_ = fds[i].data.fd;
epoll_del(epfd,fds[i].data.fd);
close(fd_);
continue;
}
}
}
}
}
}
}
int sock_create()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.31.17");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
listen(sockfd,5);
return sockfd;
}
另外再写一个通用的客户端向该服务器发送数据,客户端由用户从键盘接收数据,发送到服务器,服务器接收数据并打印这些数据,并发送确认数据"ok",表示已接收到来自客户端的数据。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.31.17");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
printf("input:\n");
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff),0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff = %s\n",buff);
}
close(sockfd);
}
这里只说明一次未处理完来自客户端的数据,服务器的处理方式,例如服务器一次只接收一个字符。
测试用例结果显示,服务端对于来自客户端的数据,分六次读取完毕,并且向客户端发送了一个确认信息"ok",这证明了前面我说的epoll的LT模式的特点是对的,并且如果客户端在服务端未读完数据时就关闭连接,服务端会依次读完数据并打印客户端结束连接的提醒。
隔了几秒后:
以上就是epoll LT模式,接下来说ET模式:
ET模式是epoll的高效工作模式,前面说了ET模式的特点,但是也有个问题:那么一次未处理完的数据,下次读到的还是有效的吗?若是,谁知道下次是什么时候,难道我就为了处理完第一次的数据我就得多发n次消息,才能处理?
开玩笑,若是这么理解的话,epollET模式的作用还有个喵啊!在这里我们能想到的是,我为何不一次就把它处理完,省的麻烦,若是这样的话,那跟使用多线程、多进程处理这类问题还有个毛区别,万一没有数据,阻塞了咋办,IO复用函数实现的服务器只有一个主线程啊,咩。所有引入了一个新的概念“非阻塞的套接字”,这样就保证一次读完数据而不会阻塞服务端。那下面验证一下呗。下面是ET模式的源码:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#define MAX 10
int sock_create();
void setnonblock(int fd)//设置非阻塞文件描述符
{
int oldfl = fcntl(fd,F_GETFL);
int newfl = oldfl | O_NONBLOCK;
if(fcntl(fd,F_SETFL,newfl) == -1)
{
perror("fcntl error");
}
}
void epoll_add(int epfd,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
perror("epoll_ctl error");
}
setnonblock(fd);//将fd设置为非阻塞文件描述符
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
{
perror("epoll_ctl error");
}
}
int main()
{
int sockfd = sock_create();
int epfd = epoll_create(MAX);
assert(epfd != -1);
epoll_add(epfd,sockfd);
struct epoll_event fds[MAX];
while(1)
{
int n = epoll_wait(epfd,fds,MAX,5000);
if(n == -1)
{
perror("epoll_wait error");
continue;
}
if(n == 0)
{
printf("time out\n");
continue;
}
else
{
int i = 0;
for(;i<n;i++)
{
if(fds[i].events & EPOLLIN)
{
if(fds[i].data.fd == sockfd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(fds[i].data.fd,(struct sockaddr*)&caddr,&len);
assert(c != -1);
epoll_add(epfd,c);
continue;
}
else
{
while(1)//这里跟LT模式有了区别了
{
char buff[128]={0};
int num = recv(fds[i].data.fd,buff,1,0);
if(num == -1)
{
send(fds[i].data.fd,"ok",2,0);
break;
}
else if(num == 0)
{
int fd = fds[i].data.fd;
epoll_del(epfd,fds[i].data.fd);
close(fd);
}
else
{
printf("read(%d):%s\n",fds[i].data.fd,buff);
}
}
}
}
}
}
}
}
int sock_create()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("192.168.31.17");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
listen(sockfd,5);
return sockfd;
}
看起来跟前面的LT没什么区别,但是效率已经高了很多了,如果是默认的文件描述符的话,你最多也只能读到一个'h'就阻塞了,总的来说,epoll的ET模式是综合了多线程多进程读数据的方式,又包含select和poll那样处理文件描述符的方式,及两家之长,在Linux环境下,已经是一种很高效的方法了。
好了,以上就是我的理解,也许太基础,但会随着项目的深入,我对epoll的理解会更深一个层次。
杀青!!!