I/O复用(二)poll和epoll

poll:

  • 函数原型:

int  poll(struct pollfd * fds, int nfds, int timeout);

struct  pollfd

{

     int fd;//指定的是用户关注的文件描述符的值

     short events;//用户关注的事件

     short revents;//由内核修改,表示关注的文件描述符发生了哪些事件

}

nfds:数组的长度,用户关注的文件描述符的格式

timeout:超时时间。-1代表永久阻塞,直到文件就绪才能返回。

select 里面关注的是文件描述符上的事件,poll里面的第一个参数是一个pollfd的类型的,第一个参数是一个数组,每个数组就是一个pollfd的类型

返回值:-1 出错  0 超时  >0 就绪文件描述符的个数

  • 特点:
  1. 关注的事件不再是通过变量传递,而是通过一个short类型传递,所以可以关注的文件描述符的个数会更多
  2. 内核修改的和用户关注的分开表示,每次调用就不需要重新设置
  3. 取值范围更大,文件描述符不再是按位来表示,直接使用int类型来表示

3.1 用户关注的文件描述符的值可以更大,一个文件描述符使用一个int值来表示,

3.2 用户关注的文件描述符的个数由用户数组决定,第一个参数传递的是数组首地址,这个数组是由用户定义的,

4、 poll返回的就是文件描述符个数,poll检测就绪文件描述符的时间复杂度依旧是O(n),poll返回后,用户程序依旧需要循环检测哪些文件描述符就绪。

5、poll的事件类型:

I/O复用(二)poll和epoll_第1张图片 I/O复用(二)poll和epoll_第2张图片

POLLPRI:这里需要说明的就是TCP的带外数据,就是TCP报头里面的那个紧急指针。

  • 实现代码
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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 inster_fds(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 caddr,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));
	assert(res != -1);

	listen(sockfd ,5);

	struct pollfd fds[SIZE];//定义数组
	init_fds(fds);//初始化数组
	inster_fds(fds,sockfd,POLLIN);//先将链接文件描述符插入数组里,只有读事件,所以选择POLLIN事件

	while(1)
	{
		int n = poll(fds,SIZE,-1);//
		if(n <= 0)
		{
			printf("poll error\n");
			continue;
		}
		//n>0,说明有文件描述符就绪,进行处理
		//将整个数组遍历一遍,检查哪个文件描述符就绪
		int i = 0;
		for(; i < SIZE ;++i)
		{
			if(fds[i].fd != -1)//fd != -1 才能进行判断l
			{
				int fd = fds[i].fd;	
				if(fds[i].revents & POLLRDHUP)//如果有关闭TCP链接的事件发生,进行断开链接处理
				{
					close(fd);
					delete_fd(fds,fd);
				}	
				else if(fds[i].revents & POLLIN)//检查是否有读事件就绪
				{
					if(fd == sockfd)//代表有客户端链接事件发生
					{
						int len = sizeof(caddr);
						int c = accept(fd,(struct sockaddr *) &caddr ,&len);
						if(c < 0)//出错处理
						{
							continue;
						}
						inster_fds(fds,c,POLLIN|POLLRDHUP);//将用户的链接套接字添加进数组里
					}
					else//代表客户端发送事件发生,进行接收数据处理
					{
						char buff [128] = {0};
						recv(fd,buff,127,0);
						printf("%d : %s",fd,buff);
						send(fd,"ok",2,0);
					}
				}
			}
		}
	}
}

 

epoll:Linux独有

epoll提供了一组函数将用户关注的文件描述符上的事件直接由内核记录

int epoll_create(int size);à 创建内核事件表,内核时间表底层是由红黑树实现的

int epoll_ctl(int epollfd,int cmd,int fd,struct epoll_event * event); à 设置内核事件表,设置(添加,修改,删除)事件包括

cmd:EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DELETE

fd:要操作的文件描述符

int epoll_wait(int epollfd,struct epoll_event * revents,int maxevents,int timeout);

epoll_wait返回的是就绪文件描述符的个数  

revents 只返回就绪的文件描述符

maxevents 是数组的大小,一次能返回多少个就绪的文件描述符

epoll使用的优点:

用户关注的事件由内核维护,每次调用epoll_wait时,不需要将用户空间的数据拷贝到内核空间。

每次epoll只会返回就绪的文件描述符,每次用户程序检测就绪文件描述符的效率O(1)。 epoll的内核实现比select和poll高效

下面是epoll_event结构体的成员:

I/O复用(二)poll和epoll_第3张图片

select和poll,内核监听的时候采用的是轮询的方式,epoll采用的是回调的方式,

epoll支持高效的ET模式

epoll实现代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define SIZE 100

int main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in caddr,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));
	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 wait eorror!\n");
			continue;
		}
		int i = 0;
		for(;i< n;++i)
		{
			int fd = events[i].data.fd;
			if(fd == sockfd)
			{
				int len = sizeof(caddr);
				int c = accept(fd,(struct sockaddr*)&caddr,&len);
				if(c<0)
				{
					continue;
				}
				event.events = EPOLLIN | EPOLLRDHUP;
				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 is 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 比较:

系统调用

select

poll

epoll

事件集合

用户通过三个参数分别传入感兴趣的可读可写及异常等事件,内核通过对这些参数的在线修改,来反馈其中的就绪事件,这使得用户在每次调用select都要重置这三个参数

统一处理所有的事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中的的就绪事件。

内核通过一个事件表直接管理用户感兴趣的所有事件,因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件。

应用程序索引就绪文件描述符的时间复杂度

O(n)

O(n)

O(1)

最大支持的文件描述符的个数

一般有最大限制值

65535

65535

工作模式

LT

LT

ET和LT

内核实现和工作效率

采用轮询方式来检测就绪的事件

采用轮询的方式来检测就绪事件

采用回调方式来检测就绪事件。

 

 

你可能感兴趣的:(Linux)