接着上篇内容,我接着给大家介绍一下select的改进----poll;poll的改进----epoll
一、poll函数原型
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件,甚至更多的类型。fds传递的是数组首地址,关注更多的类型。
pollfd结构体的定义如下:
struct pollfd
{
int fd; //文件描述符
short events; //用户关注的事件
short revents; //由内核修改,表示发生了那些事件
}
nfds : 数组的长度 ,元素的个数。 用户关注的文件描述符的个数
timeout: 超时时间 如果为-1 ,永久阻塞,直到有文件描述符到来。
返回值: 0 超时
-1 出错
>0 就绪文件描述符的个数
events关注的事件(红色为常用的三个):
我们可以将poll与select比较简单的比较:
不同:
- 用户关注的事件类型更多 (不在关注依靠read、 write、 except; 直接依靠short events类型的成员)
- 内核修改的和用户关注的分开表示,每次调用不需要重新设置。
- 文件描述符不是在按位表示,直接用int类型
a.用户关注的文件描述符的值可以更大
b.用户关注的文件描述符的个数由用户数组决定,所以个数会更多。
相同: poll返回的时候,也是将用户关注的所有文件描述符返回。poll检测就绪文件描述符的时间复杂度 O(n),poll返回后,用户程序依旧需要循环检测那些文件描述符就绪。
二、poll的代码实现
客户端都是一样,在这里就不重复写啦!不会写的可以看看上篇的代码~
#define _GNU_SOURCE //必须申明,否则挂起事件不支持
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#define SIZE 100
void Init_fds(struct pollfd *fds)
{
int i = 0;
for(; i < SIZE; ++i)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void Insert_fd(struct pollfd *fds,int fd,short event)
{
int i = 0;
for(; i < SIZE; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = event;
break;
}
}
}
void Delete_fd(struct pollfd *fds,int fd)
{
int i = 0;
for(; i < SIZE; ++i)
{
if(fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
break;
}
}
}
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(6888);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
struct pollfd fds[SIZE];
Init_fds(fds);
Insert_fd(fds,sockfd,POLLIN); //关注POLLIN事件
while(1)
{
int n = poll(fds,SIZE,-1);
if(n <= 0)
{
printf("poll error\n");
continue;
}
int i = 0;
for(; i < SIZE; ++i)
{
if(fds[i].fd != -1)
{
int fd = fds[i].fd;
if(fds[i].revents & POLLRDHUP) //先判断挂起状态的事件,如果是就不需要判断POLLIN事件
{
printf("%d will close\n",fd);
close(fd);
Delete_fd(fds,fd);
}
else if(fds[i].revents & POLLIN)
{
if(fd == sockfd) //与客户端连接 连接不一定发生
{
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
Insert_fd(fds,c,POLLIN | POLLRDHUP);//宏用或
}
else //客户端发送数据
{
char buff[128] = {0};
recv(fd,buff,127,0);
printf("%d; %s\n",fd,buff);
send(fd,"ok",2,0);
}
}
}
}
}
}
验证如下:
三、epoll原型函数
epoll: 将用户关注的文件描述符上的事件直接由内核记录(select的r、w,e和poll的数组都是用户之间维护的)
一组函数 :
1.int epoll_create(int size); 创建内核事件表----> 红黑树
2.int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event) 设置(添加 修改 删除)内核事件表中的文件描述符上的事件 //event的事件:是用户添加的事件
epfd是内核事件表的fd;fd参数的是要操作的文件描述符;op参数则指定操作类型,操作类型有如下3种:
EPOLL_CTL_ADD,往事件表中注册fd上的事件;
EPOLL_CTL_MOD,修改fd上的注册事件;
EPOLL_CTL_DEL,删除fd上的注册事件。
3.int eopll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout); //events是内核填充的事件
epoll_wait: 成功返回的是就绪的文件描述符个数,失败返回-1,并设置为error
events:只返回所有就绪的文件描述符。数组
epoll_events 的定义如下:
其中events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件为EPOLLIN.但epoll有关两个额外的事件类型----EPOLLET(高效的处理模式)和EPOLLONESHOT(防止事件重复触发,只能触发一次)
data成员用于存储用户数据,其类型epoll_data_t的定义如下:
其中fd表示用户关注的文件描述符。
四、epoll代码实现
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.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(6888);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
int epollfd = epoll_create(5);
assert(epollfd != -1);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);
while(1)
{
struct epoll_event events[SIZE];
int n = epoll_wait(epollfd,events,SIZE,-1);//内核填充的就绪文件符个数
if(n <= 0)
{
printf("Epoll error\n");
continue;
}
int i = 0;
for(; i < n; ++i)
{
if(events[i].data.fd != -1)
{
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);
}
}
}
}
}
我们不需要写初始化,插入,删除,内核给我们提供了函数,我们直接可以用,相比较select和poll来说,代码实现简单了。结果为:
总体来说就是:
epoll :由内核态事件表保存用户关注的文件描述符以及其事件,减少了用户态数据向内核态的拷贝
epoll_wait 返回的仅仅是就绪的文件描述符,所以检验就绪文件描述符的时间复杂度O(1)
epoll内核采用回溯的方式。
五、select、poll。epoll的对比
- select通过三个结构体分别表示可读、可写、异常事件,poll和epoll用一个short类型的变量表示关注的事件,事件类型更多
- select通过32个元素的long类型的数组按位记录文件描述符,最多1024个文件描述符,范围是0—1023.poll和epoll都是通过一个int的fd表示文件描述符.poll通过用户数组记录所有文件描述符,epoll通过内核事件表记录。一般能达到系统允许打开的最大文件描述符。
- select通过三个结构体传递文件描述符,也是通过其返回就绪和未就绪的文件描述符,所有每次调用select都必须重新设置三个结构体。epoll将用户关注的事件和内核反馈发生的事件分开表示,poll通过数组返回就绪的文件描述符,epoll和poll不需要重置三个结构体。
- select和poll返回的是就绪和未就绪的文件描述符,检测就绪文件描述符的事件复杂度为O(n),epoll直接通过数组仅仅返回所有的就绪文件描述符,检测就绪文件描述符的时间复杂度为O(1)
- select和poll内核采用轮询的方式,epoll采用回调的方式;回调是哪个文件就绪了,才会触发。 轮循是一个一个的去检查。
- select内核通过数组,poll内核链表,epoll内核是红黑树+链表
- select和poll仅仅支持LT模式,epoll支持LT 和ET模式
- select和poll都是单独的函数,epoll是一组函数
- select和poll每次调用都会把数据从用户态拷贝到内核态,epoll会直接从内核态拷贝
从以上分析来看,epoll的效率最高,但是在有一种情况下:注册10000个文件描述符,每次都有9960个就绪,就意味着活动的文件描述符很大,这种情况下poll的效率反而比epoll的效率高。因为几乎文件描述符都是就绪,概率很大,并且采用轮循就比要回调9960次更快,效率更高。
因此epoll使用于连接很多,但是活动连接较少的情况下。