见上文poll
一、Epoll——Linux下独有且Epol将用户关注的文件描述符上的事件直接由内核记录
一组函数:
创建:
Int epoll_create(int size);//创建内核事件表; 内核事件表底层是用红黑树构建的;
Size只是给内核的一个提示,告诉他需要多大的事件表;
操作内核事件表:
Int epoll_ctl(int epfd, int cmd, int fd, struct epoll_event *event);//设置(添加、修改、删除)内核事件表中的文件描述符上的事件;
Fd:为要操作的文件描述符;
Op:指定操作类型(往事件表中注册,修改,删除fd上的注册事件)
Cmd:
EPOLL_CTL_ADD:往事件表中注册fd的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件
Event:指定事件;下面是epoll_event的结构:
Epoll系列系统调用的主要接口:
Int epoll_wait(int epollfd ,struct epoll_event *revents, int maxevents, int timeout);//返回就绪文件描述符的个数
Revents:只返回就绪的文件描述符
Maxevents:数组的长度;
此函数如果检测到事件,就将所有就绪事件从内核事件表中(由epfd中的参数指定)复制到它的第二个参数events指定的数组中,这个数组只输出epoll_wait检测出的就绪事件。所以,搜索就绪文件描述符的时间复杂度为O(1).
二、epoll的简单实现
代码:
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#define SIZE 100
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
listen(sockfd,5);
int epollfd = epoll_create(5);//创建epoll
assert(epollfd != -1);
struct epoll_event event;
event.events = EPOLLIN;//事件
event.data.fd = sockfd;//指定文件描述符
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);//注册sockfd文件描述符到内核表中
while(1)
{
struct epoll_event events[SIZE];
int n = epoll_wait(epollfd,events,SIZE,-1);//就绪的事件 内核填充的
if(n <= 0)
{
printf("epoll_wait error\n");
continue;
}
int i = 0;
for(;i < n;++i)
{
int fd = events[i].data.fd;
if(fd == sockfd)
{
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
event.events = EPOLLIN | EPOLLRDHUP;
event.data.fd = c;
epoll_ctl(epollfd,EPOLL_CTL_ADD,c,&event);
}
else if(events[i].events & EPOLLRDHUP)//用户关注的事件
{
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
printf("%d was over\n",fd);
}
else if(events[i].events & EPOLLIN)
{
char buff[128] = {0};
recv(fd,buff,127,0);
printf("%d:%s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
return 0;
}
结果:
三、epoll对比与select和poll
1、文件描述符的范围
2、文件描述符的个数
3、事件类型会更多
4、用户关注的事件由内核维护,每次调用epoll_wait时,不需要将用户空间的数据拷贝到内核空间;
5、epoll只会返回就绪的文件描述符,效率高于select和poll
6、用户程序检测就绪文件描述符的时间复杂度为O(1);
7、epoll内核实现比select和poll高效;select和poll内核监听采用的是轮询的方式;而epoll采用的是回调的方式,(即就是哪一个就绪,就只会触发就绪的内核事件);
回调:适用于关注的文件描述符很多,但活动的就绪的文件描述符少(即多连接少活动的);
轮询:适用于关注的和就绪的文件描述符差不多的时候;(多连接多活动)
8、epoll支持高效的EL模式,poll和select只能工作在低效的LT模式;
以下是书上给出的三种I/O函数的比较:
这三组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。Select的参数类型fd_set没有将文件描述符和事件绑定,他仅仅是一个文件描述符的集合,因此selec需要提供3个这种类型的参数来分别传入和输出可读、可写以及异常等事件。这一方面使得select不能处理更多类型的事件,另一方面由于内核对于fd_set集合的在线修改,应用程序下次调用select前不得不重置这3个fd_set集合。Poll的参数类型pollfd则好一些,它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁的多。并且内核每次修改的是pollfd结构体的revents成员,而events成员保持不变,因此下次调用poll时应用程序无须重置pollfd类型的事件集参数。由于每次select和poll调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为O(n)。epoll则采用与select和poll’完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。Epoll_Wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到了O(1).
Poll和epoll_wait分别用nfds和maxevents参数指定最多监听多少个文件描述符和事件。这两个数值都能达到系统允许打开的最大文件描述符的数目,即65535.而select允许监听的最大文件描述符数量通常非常的有限制。
小结epoll:
epoll相对于poll和select来说是比较高效的模式。但是不能说epoll是绝对的高效,我们就可以摒弃poll和select这两种方式。在不同的场景下,效率是不同的。当活动连接比较多的时候,epoll_wait的效率未必就比select和poll高,因为此时回调函数被触发得过于频繁。所以epoll_wait适用于连接数量多,但活动数量较少的情况。epoll早内核中维护了一个事件表,并且由独立的系统调用epoll_ctl来控制对事件表的操作。
首先在内核中注册一个内核事件表,当在用户态有新的事件到来时,切换到内核态,将该事件注册到内核事件表中,然后,通过epoll_wait在内核事件表中检测就绪事件,当检测到就绪事件时,就将所有就绪事件从内核事件表中(由epfd中的参数指定)复制到它的第二个参数events指定的数组中,返回到用户态(返回时只返回就绪事件)。