I/O复用函数
select
poll
epoll<Linux的独有的I/O复用>
接下来我们最后一个I/O复用,也是Linux独有的I/O复用并对三种I/O复用进行简单的分析
epoll
epoll不再是一个函数,而是一组函数,分别是:
int epoll_create(int seize);//创建内核事件表,不用用户维护,由内核维护
int epoll_ctl(int epfd,int op,,int fd,struct epoll_event *event);//也就是将事件注册到内核事件表中
int epoll_wait(int epfd,struct epoll_event *events,int eventslen,int timeout);//epoll监听
epoll_create
内核事件表:在系统内核创建一个用于记录用户关注的文件描述符上的事件的一个表,对于select和poll来说事件表都是创建在用户空间的,每次操作需要从用户空间进入到内核空间,从内核空间再拷贝出数据到用户空间,而epoll生成内核事件表,比起其它两种少一次拷贝。
epoll_create返回值 -1——失败
成功返回内核事件表的标识符
epoll_ctl
int epfd//内核事件表标识符,由epoll_create获得之
int op//宏,指定操作类型,在同一个函数上实现了泛式算法,也就是函数对象,op有三个宏,分别为:
EPOLL_CTL_ADD 往事件表中注册fd上的事件
EPOLL_CTL_MOD 更改在fd上注册的事件
EPOLL_CTL_DEL 删除注册在fd上的事件
int fd//要操作的文件描述符,传入一个sockfd或者c 文件描述符都可以
struct epoll_event *event//这个参数指定事件,是struct epoll_event *类型,以下是struct epoll_event *的定义:
struct epoll_event
{
uint32_t events;用户关注的事件
epoll_data_t data;//联合体,用户关注的fd就在这里
}
对于epoll关注的事件来说,支持的事件基本相同,都是在poll的事件前面加上E即可,但是epoll有额外的EPOLLET和EPOLLONESHOT,这两个对epoll的高效性打下了基础,有新的朋友可以去看看epoll的LT和ET模式,下次博客会有讲到这些,这里我们先对三种I/O复用进行学习。
epoll_data_t的定义如下:
typedef union epoll_data
{
void *ptr;
int fd;//用户关注的文件描述符
uint32_t u32;
uint64_t u64;
}
常用的就是fd,标识关注的文件描述符,其他的不常用,同时也因为union的原因,想要保存fd也保存其他的数据就得用上ptr指向结构体了,算是为以后编程预留了可拓展的接口。
epoll_wait
events:是用户指定的数组,len是长度,指定数组元素的个数
这个数组用来保存epoll_wait返回时,内核填充的就绪文件描述符信息,也就是接受全部都是准备就绪的文件描述符,限定了一次处理的个数
wait返回值0 超时,-1失败,>0就绪文件描述符的个数
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define FDMAXNUM 100
void DealClientData(int fd,int epfd,short events)
{
if(events & EPOLLRDHUP)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}
else if(events & EPOLLIN)
{
char buff[128] = {0};
int n = recv(fd,buff,127,0);
if(n <= 0)
{
printf("Love is over");
close(fd);
return;
}
printf("%s\n", buff);
send(fd,"OK",2,0);
}
}
void GetClientLink(int fd,int epfd,struct epoll_event *event,struct sockaddr_in cli)
{
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
assert(c != -1);
event->events = EPOLLIN | EPOLLRDHUP;
event->data.fd = c;
epoll_ctl(epfd,EPOLL_CTL_ADD,c,event);
}
void DealFinshEvent(int sockfd,int epfd,struct epoll_event *events,
int n,struct sockaddr_in cli,struct epoll_event *event)
{
int i = 0;
for(;i < n;++i)
{
int fd = events[i].data.fd;
if(fd == sockfd)
{
GetClientLink(fd,epfd,event,cli);
}
else
{
DealClientData(fd,epfd,events[i].events);
}
}
}
int main(int argc, char const *argv[])
{
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");//New Change
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
int epfd = epoll_create(5);
assert(epfd != -1);
struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
while(1)
{
struct epoll_event events[FDMAXNUM];
int n = epoll_wait(epfd,events,FDMAXNUM,-1);//Wait a event
if(n <= 0)
{
printf("This Client is unlink");
continue;
}
DealFinshEvent(sockfd,epfd,events,n,cli,&event);
}
close(sockfd);
return 0;
}
对于EPOLLIN和EPOLLRDHUP的判断也要进行,和poll很相似,但是epoll明显要比poll高效得多
epoll相比poll高效的地方:
- 用户关心的事件直接保存在内核事件表中,每次epoll_wait不需要从用户空间向内核空间拷贝
- epoll_wait返回时,只需要将就绪的文件描述符拷贝到用户空间的数组上。而select和poll需要将所有的文件描述符(无论就绪或者未就绪)返回。效率高。
- epoll用户探测就绪文件描述符的时间复杂度为O(1),还有些优点继承了poll的优点,相对于select有优化。poll在内核实现中,是fbs[n]是链表。epoll是红黑二叉树
- epoll采用回调方式,返回events数组的底层是链表,poll和select都是事件轮询
三种I/O复用的区别:
- EPOLL的回调方法适合关注描述符多,一次返回就绪的少的情况,因为回调函数也要出栈入栈,函数触发的次数过于频繁
- POLL的轮询方法适合关注描述符多,一次返回就绪的多的情况。
- epoll内核消息队列实现是红黑树,poll内部是链表,select内部是