I/O多路复用之select、epoll的实现和区别 ,ET与LT模式

 概念:IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。


select:select系统调用是用来让我们的程序监视多个文件句柄的状态变化。

 1.函数原型intselect(int nfds,fd_set *readfds,fd_set

*writefds,fd_set *exceptsfds,conststruct timeval *timeout)

   返回值:就绪描述符的数目,超时返回0,出错返回-1。

   参数:nfds是需要监听的最大文件描述符+1(即文件描述的个数),第2,3,4分别对应需要检测的可读/写/异常文件描述的集合,struct timeval结构设置超时范围。

可通过以下四个宏对文件描述符进行设置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 

运用select编写的一个tcp服务器:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define _SIZE_ 128
int gfds[_SIZE_];
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		exit(1);
	}
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("sock");
		exit(2);
	}
	int opt=1;
	setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
	struct sockaddr_in local;
	local.sin_family=AF_INET;
	local.sin_addr.s_addr=inet_addr(argv[1]);
	local.sin_port=htons(atoi(argv[2]));
	socklen_t len=sizeof(local);
	if(bind(sock,(struct sockaddr*)&local,len)<0)
	{
		perror("bind");
		exit(3);
	}
    if(listen(sock,5)<0)
	{
		perror("listen");
		exit(4);
	}

	//select
	int i=0;
	for(;i<_SIZE_;i++)
	{
		gfds[i]=-1;
	}
	gfds[0]=sock;

	while(1)
	{
		int max_fd=-1;
		struct timeval timeout={5,0};
		fd_set rfds;
		FD_ZERO(&rfds);
		int j=0;
		for(;j<_SIZE_;j++)
		{
			if(gfds[j]>=0)
				FD_SET(gfds[j],&rfds);  //设置读集
			if(max_fd<gfds[j])
				max_fd=gfds[j];  //更新最大文件描述符
		}
		switch(select(max_fd+1,&rfds,NULL,NULL,&timeout))
		{
			case 0:
				printf("timeout\n");
				break;
			case -1:
				printf("faild\n");
				break;
		    default:  //成功
				{	
                int k=0;
				for(;k<_SIZE_;k++)
				{
					//没就绪
					if(gfds[k]<0)
					{
						continue;
					}
					if(gfds[k]==sock&&FD_ISSET(gfds[k],&rfds))
					{
						struct sockaddr_in peer;
						socklen_t len=sizeof(peer);
						int new_sock=accept(gfds[k],(struct sockaddr*)&peer,&len);
						int m=0;
						for(;m<_SIZE_;m++)
						{
							if(gfds[m]==-1)
							{
								gfds[m]=new_sock;
							 	break;
							} 
						}
						if(m==_SIZE_)
						{
							close(new_sock);
						}
					}//if
					else if(FD_ISSET(gfds[k],&rfds))
					{
						char buf[128];
						int s=read(gfds[k],buf,sizeof(buf)-1);
						if(s==0)//对端连接关闭了,行当于读到文件结尾
						{
							printf("client is quit\n");
							close(gfds[k]);
							gfds[k]=-1;
						}
						if(s>0)
						{
							buf[s-1]=0;
							printf("%s\n",buf);
						}
					}

				}
				}
		}
	}
}



select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024


epoll:

    1. epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

       2. epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

运用epoll编写的一个tcp服务器:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<stdlib.h>
#include<string.h>
typedef struct epbuf
{
	int fd;
	char buf[1024];
}epbuf_t,*epbuf_p,**epbuf_pp;

static epbuf_p alloc_epbuf(int fd)
{
	epbuf_p ptr=(epbuf_p)malloc(sizeof(epbuf_t));
	if(ptr==NULL)
	{
		perror("malloc");
		exit(5);
	}
	ptr->fd=fd;
	return ptr;
}
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		printf("参数错误\n");
		exit(1);
	}
	int listen_sock=socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock<0)
	{
		perror("socket");
		exit(2);
	}
	struct sockaddr_in local;
	local.sin_family=AF_INET;
	local.sin_addr.s_addr=inet_addr(argv[2]);
	local.sin_port=htons(atoi(argv[1]));
	socklen_t len=sizeof(local);
	if(bind(listen_sock,(struct sockaddr*)&local,len)<0)
	{
		perror("bind");
		exit(3);
	}
	if(listen(listen_sock,5)<0)
	{
		perror("listen");
		exit(4);
	}
	//1.创建一个epoll
	int epfd=epoll_create(200);
	struct epoll_event ev;
	ev.events=EPOLLIN|EPOLLET;
	ev.data.ptr=alloc_epbuf(listen_sock);
    //2.注册
	epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);

	while(1)
	{
		int nums=0;
		struct epoll_event evs[32];
		int max_evs=32;
		int timeout=5000;
		switch(nums=epoll_wait(epfd,evs,max_evs,timeout))
		{
			case 0: //超时
				printf("timeout\n");
				break;
			case -1:
				perror("epoll_wait");
				break;
			default:
		 		{
		 			int i=0;
					for(;i<nums;i++)
					{
						int fd=((epbuf_p)(evs[i].data.ptr))->fd;
						if((evs[i].events&EPOLLIN)&&fd==listen_sock)
						{
							struct sockaddr_in peer;
							socklen_t len=sizeof(peer);
							int new_sock=accept(fd,(struct sockaddr*)&peer,&len);
							if(new_sock<0)
							{
								perror("accept" );
								continue;
							}
							ev.events=EPOLLIN;
							ev.data.ptr=alloc_epbuf(new_sock);
							epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
						}//if
						else if((evs[i].events&EPOLLIN)&&fd!=listen_sock)//读事件就绪
						{
							char* buf=((epbuf_p)(evs[i].data.ptr))->buf;
							ssize_t s=read(fd,buf,1023);
							if(s>0)
							{
								buf[s]=0;
								printf("%s\n",buf);
								//回写
								ev.events=EPOLLOUT;
								epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
							}
							else if(s==0)
							{
							    free(evs[i].data.ptr);
								evs[i].data.ptr=NULL;
								epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
							}
						}//else if
			     		else if((evs[i].events&EPOLLOUT)&&fd!=listen_sock)//写事件就绪
						{
							const char* msg="HTTP/1.0 200  OK \r\n\r\n<html><h1>HELLO WORLD!</h1></html>\n";
							write(fd,msg,strlen(msg));
						    free(evs[i].data.ptr);
							epoll_ctl(epfd,EPOLL_CTL_DEL,fd ,NULL);
		 		 		}
				 	}
				} 
		}
	}
	return 0;
}

      epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

  ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。



select与epoll的区别与联系:

      epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。

     1. epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

     2. epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

     3. epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值