I/O复用----poll与epoll

接着上篇内容,我接着给大家介绍一下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比较简单的比较:

不同:

 

  1. 用户关注的事件类型更多  (不在关注依靠read、 write、 except; 直接依靠short events类型的成员)
  2. 内核修改的和用户关注的分开表示,每次调用不需要重新设置。
  3. 文件描述符不是在按位表示,直接用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的对比

  1. select通过三个结构体分别表示可读、可写、异常事件,poll和epoll用一个short类型的变量表示关注的事件,事件类型更多
  2. select通过32个元素的long类型的数组按位记录文件描述符,最多1024个文件描述符,范围是0—1023.poll和epoll都是通过一个int的fd表示文件描述符.poll通过用户数组记录所有文件描述符,epoll通过内核事件表记录。一般能达到系统允许打开的最大文件描述符。
  3. select通过三个结构体传递文件描述符,也是通过其返回就绪和未就绪的文件描述符,所有每次调用select都必须重新设置三个结构体。epoll将用户关注的事件和内核反馈发生的事件分开表示,poll通过数组返回就绪的文件描述符,epoll和poll不需要重置三个结构体。
  4. select和poll返回的是就绪和未就绪的文件描述符,检测就绪文件描述符的事件复杂度为O(n),epoll直接通过数组仅仅返回所有的就绪文件描述符,检测就绪文件描述符的时间复杂度为O(1)
  5. select和poll内核采用轮询的方式,epoll采用回调的方式;回调是哪个文件就绪了,才会触发。 轮循是一个一个的去检查。
  6. select内核通过数组,poll内核链表,epoll内核是红黑树+链表
  7. select和poll仅仅支持LT模式,epoll支持LT 和ET模式
  8. select和poll都是单独的函数,epoll是一组函数
  9. select和poll每次调用都会把数据从用户态拷贝到内核态,epoll会直接从内核态拷贝

       从以上分析来看,epoll的效率最高,但是在有一种情况下:注册10000个文件描述符,每次都有9960个就绪,就意味着活动的文件描述符很大,这种情况下poll的效率反而比epoll的效率高。因为几乎文件描述符都是就绪,概率很大,并且采用轮循就比要回调9960次更快,效率更高。

       因此epoll使用于连接很多,但是活动连接较少的情况下。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值