目录
1.epoll就解决了select和poll的三个痛点
1.从用户空间传递到内核空间,select和poll的开销是非常大的;
2.从内核实现上来讲,select和poll内核实现是轮询,事件复杂度是O(n);
3.select和poll内核检测O(n).所以select和poll就不适合描述符特别多的场景:
2.epoll一组函数的介绍
epoll实际上是一组函数,epoll_creat,epoll_ctl,epoll_wait;
epoll三个函数各自的原型如下:
注意,这些方法都是可以查询帮助手册:
man epoll_creat/ctl/wait;
2.1 epoll_create()
用于创建内核事件表,epoll_create()成功返回内核事件表的文件描述符(文件描述符是一个整型,所以返回一个整型),通过这个返回值我们可以控制内核事件表(比如打开它);失败返回-1
#include <sys/epoll.h>
int epoll_create(int size):
size 表示内核事件表的大小;参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。
2.2 epoll_ctl()
用于操作内核事件表,这个函数可以给刚才创建的内核事件表添加,修改,移除描述符;epoll_ctl()成功返回 0,失败返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
第一个参数epfd 就是刚才epoll_creat的返回值,指定要操作的内核事件表的文件描述符
第二个参数op是操作方法,指定操作类型
- EPOLL_CTL_ADD 往内核事件表中注册 fd 上的事件
- EPOLL_CTL_MOD修改 fd 上的注册事件
- EPOLL_CTL_DEL 删除 fd 上的注册事件
第三个参数fd,指定要操作的文件描述符
文件描述符上的事件就在struct epoll_event上来存放;
struct epoll_event
{
_uint32_t events;//events表示epo11 事件,可以按位存放多类型的事件;因为它是32位的,所以可以表示32个事件;
epoll_data_t data;//用户数据,其中data是一个联合体,结构在下面: 相当于data.fd存放描述符;
};
其中, events 成员描述事件类型, epoll 支持的事件类型与 poll 基本相同,表示epoll 事件的宏是在 poll 对应的宏前加上'E’,比如 epoll的数据可读事件是EPOLLIN。但是 epoll 有两个额外的事件类型:EPOLLET 和EPOLLONESHOT
data 成员用于存储用户数据,是一个联合体,其定义如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
//其中 fd 成员使用的最多,它指定事件所从属的目标文件描述符。相当于data.fd存放描述符;
2.3 epoll_wait()
用于在一段超时时间内等待一组文件描述符上的事件,epoll_wait()成功返回就绪的文件描述符的个数,失败返回-1,超时返回 0
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- epfd 参数指定要操作的内核事件表的文件描述符
- events 参数是一个用户数组,这个数组仅仅在 epoll_wait 返回时保存内核检测到的所有就绪事件,而不像select 和 poll 的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。
- maxevents 参数指定用户数组的大小,即指定最多监听多少个事件,它必须大于0
- timeout 参数指定超时时间,单位为亳秒,如果 timeout 为 0,则 epoll_wait 会立即返回,如果 timeout为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。
注意,这个超时时间和poll一样,以毫秒为单位;
3.利用epoll实现tcp服务器端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAXFD 10
int create_socket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
return -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("127.0.0.1");
int res=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(res==-1)
{
return -1;
}
res=listen(sockfd,5);
if(res==-1)
{
return -1;
}
return sockfd;
}
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 add err!\n");
}
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
{
perror("epoll ctl del err!\n");
}
}
int main()
{
int sockfd=create_socket();
assert(sockfd!=-1);
int epfd=epoll_create(MAXFD);
assert(epfd!=-1);
epoll_add(epfd,sockfd);
struct epoll_event evs[MAXFD];
while(1)
{
int n=epoll_wait(epfd,evs,MAXFD,5000);
if(n==-1)
{
perror("epoll wait err!\n");
continue;
}
else if(n==0)
{
printf("time out!\n");
continue;
}
else
{
for(int i=0;i<n;i++)
{
int fd=evs[i].data.fd;
if(evs[i].events&EPOLLIN)
{
if(fd==sockfd)
{
//accept
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c==-1)
{
continue;
}
printf("accept c=%d\n",c);
epoll_add(epfd,c);
}
else
{
//recv
char buff[128]={0};
int res=recv(fd,buff,127,0);
if(res<=0)
{
epoll_del(epfd,fd);
close(fd);
printf("one client over!\n");
}
else
{
printf("buff(c=%d)=%s\n",fd,buff);
send(fd,"ok",2,0);
}
}
}
}
}
}
exit(0);
}
4.epoll的总结
epoll在实现和使用上与 select、poll有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表.
特别强调:epoll的返回值至关重要