Linux编程学习笔记--多路复用三种方式(select、poll、epoll)

Linux编程学习笔记–多路复用三种方式(select、poll、epoll)

select多路复用
select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理;

int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);

说明: select监视并等待多个文件描述符的属性发生变化,它监视的属性分3类,分别是readfds(文件描述符可读)、writefds(文件描述符可写)、和exceptfds(文件描述符异常)。调用后select函数会阻塞,直到有描述符就绪,或者超时( timeout 指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fd_set:

FD_ZERO(fd_set* fds) //清空集合
FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中,判断哪个发生事件
FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除 

来找到究竟是哪些文件描述符就绪;

  1. select函数的返回值是就绪描述符的数目,超时时返回0,出错返回-1,发生事件大于0;
  2. 第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1
  3. 中间三个参数readset、writeset和exceptset(异常)指定要让内核测试读、写和异常条件的fd集合,如果不关心的可以设置为NULL;
  4. 最后一个参数是设置select的超时时间,如果设置为NULL则永不超时;

在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,这也意味着select所用到的FD_SET是有限的,也正是这个原因select()默认只能同时处理1024个客户端的连接请求:

/linux/posix_types.h:
#define __FD_SETSIZE 1024

利用select编写一个服务器端部分代码:

    int socket_server_init(char *listen_ip, int listen_port);//定义一个socket调用函数
	if( (listenfd=socket_server_init(NULL, serv_port)) < 0 )     //调用服务端初始化函数,传入ip地址和端口
 	{
 		printf("ERROR: %s server listen on port %d failure\n", argv[0],serv_port);
 		return -2;
 	}
 
	printf("%s server start to listen on port %d\n", argv[0],serv_port);

 	if( daemon_run )   //程序是否到后台运行
 	{
 		daemon(0, 0);   //到后台运行
 	}
 
	for(i=0; i<ARRAY_SIZE(fds_array) ; i++)      //ARRAY_SIZE宏求一个数组里有多少个元素
 	{
 		fds_array[i]=-1;          //将fds_array数组里所有的元素设为-1,文件描述符可能为0,所以不用0
 	}
 
	fds_array[0] = listenfd;    //把listenfd描述符放入数组中第一个成员
 
	for ( ; ; )       //死循环
 	{
 		FD_ZERO(&rdset);      //初始化rdset
 		
		for(i=0; i<ARRAY_SIZE(fds_array) ; i++)   //遍历数组fds_array里的文件描述符
 		{
 			
			if( fds_array[i] < 0 )   //没有新的事件触发
 			
				continue;
 			
			maxfd = fds_array[i]>maxfd ? fds_array[i] : maxfd;   //设置maxfd
 			FD_SET(fds_array[i], &rdset);   //把fds_array[i]对应的文件描述符放入rdset中
 		}
 
 		rv = select(maxfd+1, &rdset, NULL, NULL, NULL);   //调用select函数,阻塞
 		
		if(rv < 0)
 		{
 			printf("select failure: %s\n", strerror(errno));
 			break;
 		}

		else if(rv == 0)    //超时
 		{		
 			printf("select get timeout\n");
	 		continue;
 		}
 		
		//reset中有文件描述符发生了事件
 		
		if ( FD_ISSET(listenfd, &rdset) )   //判断如果是listenfd对应的文件描述符发生了事件,新的客户端发起连接请求
 		{
 			
			if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)   //等待客户端连接
 			{
 				printf("accept new client failure: %s\n", strerror(errno));
 				continue;
 			}
 
			found = 0;
 			
			for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
 			{
 			
				if( fds_array[i] < 0 )    //i这个位置还没有被占用
 				{    
 					fds_array[i] = connfd;  //将与新的客户端通信的accept产生的fd放入数组中该位置
 					printf("accept new client[%d] and add it into array\n", connfd );
					found = 1;    
 					break;
				}
 			}
 
			if( !found )     //说明找不到没有被占用的i,客户端满了
 			{
 				printf("accept new client[%d] but full, so refuse it\n", connfd);
 				close(connfd);
 			}
 		}
 
		else   // 已经连上的客户端发生了事件
 		{
 		
			for(i=0; i<ARRAY_SIZE(fds_array); i++)
 			{
 
				if( fds_array[i]<0 || !FD_ISSET(fds_array[i], &rdset) )   //如果不是已经连上的客户端发生事件
 				
					continue;
 
				memset(buf, 0, sizeof(buf));
				if( (rv=read(fds_array[i], buf, sizeof(buf))) <= 0)    //否则读取客户端信息,如果读取失败
 				{
 					printf("socket[%d] read failure or get disconncet.\n", fds_array[i]);
 					close(fds_array[i]);
 					fds_array[i] = -1;    //释放位置
 				}
 
				else         //读取成功
 				{
 					printf("socket[%d] read get %d bytes data:%s\n", fds_array[i], rv, buf);
 					
	 				for(j=0; j<rv; j++)
 						
						buf[j]=toupper(buf[j]);   //将读到的内容转为大写
 					
					if( write(fds_array[i], buf, rv) < 0 )   //将转化后的内容发给客户端
 					{
 						printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno));
 						close(fds_array[i]);
 						fds_array[i] = -1;
 					}
 				}
 			}
 		}
 	

socket调用函数:

int socket_server_init(char *listen_ip, int listen_port)   //封装服务器端初始化函数
{
	struct sockaddr_in servaddr;
	int   rv = 0;
	int   on = 1;
	int   listenfd;

   if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)   //创建socket
	{
   	printf("Use socket() to create a TCP socket failure: %s\n", strerror(errno));
   	return -1;
	}

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));   //避免地址占用
	memset(&servaddr, 0, sizeof(servaddr));   //初始化结构体
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(listen_port);

   if( !listen_ip )            //如果输入listen_ip为0,监听所有ip
	{
		servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
	}		

   else      //监听指定的ip
	{			
   	if (inet_pton(AF_INET, listen_ip, &servaddr.sin_addr) <= 0)    //将点分十进制的ip转化为二进制网络字节序ip,如果返回值为0,表示输入的ip地址表达式无效
		{
			printf("inet_pton() set listen IP address failure.\n");
			rv = -2;
			goto CleanUp;
		}
	}

   if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)     //调用bind函数
	{
		printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
		rv = -3;
		goto CleanUp;
	}

   if(listen(listenfd, 13) < 0)          //监听对应的sockfd
	{
		printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
		rv = -4;
		goto CleanUp;
	}
   
   CleanUp:
	if(rv<0)
			
   	close(listenfd);   //关闭socket
	
   else                       //如果为0,表示inet_pton函数中输入的ip地址为无效表达式
		rv = listenfd;
		return rv;
}

用tcp_test_tool模拟客户端与服务器端通信:
在这里插入图片描述
poll多路复用
select()和poll()系统调用在本质上没有多大差别,机制类似 ,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

int poll(struct pollfd *fds, nfds_t nfds, int timeout)  //timeout为毫秒

第一个参数用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符;

struct pollfd
{
 int fd; /* 文件描述符 */
 short events; /* 等待的事件 */   //要监听哪个事件
 short revents; /* 实际发生了的事件 */   //实际监听了哪个事件
} ;

第二个参数 nfds 指定数组中监听的元素个数;
第三个参数 timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件;

该函数成功调用时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
  EBADF   一个或多个结构体中指定的文件描述符无效。
  EFAULTfds   指针指向的地址超出进程的地址空间。
  EINTR     请求的事件之前产生一个信号,调用可以重新发起。
  EINVALnfds  参数超出PLIMIT_NOFILE值。
  ENOMEM   可用内存不足,无法完成请求。

利用poll编写一个服务器端部分代码:(其他与select类似):

	for(i=0; i<ARRAY_SIZE(fds_array) ; i++)
 	{
		fds_array[i].fd=-1;    //把fds_array数组的每个fd域设为-1,表示未被占用    
	}
 
	fds_array[0].fd = listenfd;   //把fds_array第一个数组的fd域设为listenfd
 	fds_array[0].events = POLLIN; //只关心listenfd的读事件
 	max = 0;
 
	for ( ; ; )
 	{
 		rv = poll(fds_array, max+1, -1);  //调用poll函数,-1表示永不超时,阻塞
 		
		if(rv < 0)
 		{
 			printf("poll failure: %s\n", strerror(errno));
 			break;
 		}
 
		else if(rv == 0)
 		{
 			printf("poll get timeout\n");
 			continue;
 		}
 
		if (fds_array[0].revents & POLLIN)      //如果listenfd有事件发生(有客户端连接请求)
 		{
			if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)  //调用accept函数接受客户端连接请求
 			{
 				printf("accept new client failure: %s\n", strerror(errno));
 				continue;
	 		}
 			
			found = 0;
 
			for(i=1; i<ARRAY_SIZE(fds_array) ; i++)
 			{
 				if( fds_array[i].fd < 0 )    //找没有被占用的fd
	 			{
 					printf("accept new client[%d] and add it into array\n", connfd );
 					fds_array[i].fd = connfd;
 					fds_array[i].events = POLLIN;  //关心connfd的读事件
 					found = 1;
 					break;
 				}
 			}
 
			if( !found )  //如果连接客户端已满
 			{
 				printf("accept new client[%d] but full, so refuse it\n", connfd);
 				close(connfd);
 				continue;
 			}
 
 			max = i>max ? i : max;
 			if (--rv <= 0)
 			continue;
 		}
 
		else   //如果是已经连接的客户端发生事件
 		{
 			for(i=1; i<ARRAY_SIZE(fds_array); i++)
 			{
 				if( fds_array[i].fd < 0 )
 				
					continue;
 
				memset(buf, 0, sizeof(buf));
				if( (rv=read(fds_array[i].fd, buf, sizeof(buf))) <= 0)
 				{
 					printf("socket[%d] read failure or get disconncet.\n", fds_array[i].fd);
					close(fds_array[i].fd);
 					fds_array[i].fd = -1;
 				}
 				
				else
 				{
					printf("socket[%d] read get %d bytes data:%s\n", fds_array[i].fd, rv, buf);
 					
					for(j=0; j<rv; j++)
 					
						buf[j]=toupper(buf[j]);
 
					if( write(fds_array[i].fd, buf, rv) < 0 )
 					{
 						printf("socket[%d] write failure: %s\n", fds_array[i].fd, strerror(errno));
 						close(fds_array[i].fd);
 						fds_array[i].fd = -1;
 					}
 				}
 			}
 		}

用tcp_test_tool模拟客户端与服务器端通信:
![在这里插入图片描述](https://img-blog.csdnimg.cn/7ac9985a1b7e4209a412f868a75def0b.png在这里插入图片描述

epoll多路复用

select的缺点:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

epoll的设计和实现与select不同,epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:

  1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  2. 调用epoll_ctl向epoll对象中添加需要连接的套接字
  3. 调用epoll_wait收集发生的事件的连接

创建epoll:epoll_create()

int epoll_create(int size);

系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。作为函数返回值,epoll_create()返回了代表新创建的epoll实例的文件描述符。这个文件描述符在其他几个epoll系统调用中用来表示epoll实例。当这个文件描述符不再需要时,应该通过close()来关闭。当所有与epoll实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返还给系统。

参数size指定了我们想要通过epoll实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。从Linux2.6.8版以来,size参数被忽略不用。

修改epoll的兴趣列表:epoll_ctl()

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);

系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。若成功返回0,若出错返回-1。

第一个参数epfd是epoll_create()的返回值;

第二个参数op用来指定需要执行的操作,它可以是如下几种值:
EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误;
EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误;
EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。如果我们试图移除一个不在epfd的兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除;

第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;

第四个参数ev是指向结构体epoll_event的指针,结构体的定义如下:

typedef union epoll_data
{
 void *ptr;     /* Pointer to user-defind data */
 int fd;      /* File descriptor */
 uint32_t u32;    /* 32-bit integer */
 uint64_t u64;      /* 64-bit integer */
} epoll_data_t;
struct epoll_event
{
 uint32_t events;     /* epoll events(bit mask) */
 epoll_data_t data;     /* User data */
};

参数ev为文件描述符fd所做的设置(epoll_event)如下:
events字段是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合;
data字段是一个联合体,当描述符fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息;

事件等待:epoll_wait()

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。调用成功后epoll_wait()返回数组evlist中的元素个数,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0,出错时返回-1并在errno中设定错误码以表示错误原因。

第一个参数epfd是epoll_create()的返回值;

第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;

第三个参数maxevents指定所evlist数组里包含的元素个数;

第四个参数timeout用来确定epoll_wait()的阻塞行为,有如下几种:

  1. 等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
  2. 等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
  3. 大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。

数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。
data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。(注意,data字段是唯一可获知同这个事件相关的文件描述符的途径。因此,当我们调用epoll_ctl()将文件描述符添加到感兴趣列表中时,应该要么将ev.date.fd设为文件描述符号,要么将ev.date.ptr设为指向包含文件描述符号的结构体)

利用epoll编写一个服务器端部分代码:

if( (listenfd=socket_server_init(NULL, serv_port)) < 0 )  //调用socket函数,与select中类似
 	{
 		printf("ERROR: %s server listen on port %d failure\n", argv[0],serv_port);
 		return -2;
 	}
 
	printf("%s server start to listen on port %d\n", argv[0],serv_port);
 
	if( daemon_run )
 	{
 		daemon(0, 0);
	}
 	
	if( (epollfd=epoll_create(MAX_EVENTS)) < 0 )  //调用epoll_create函数创建epollfd
 	{
 		printf("epoll_create() failure: %s\n", strerror(errno));
 		return -3;
 	}

 	event.events = EPOLLIN;    //关心读事件
 	event.data.fd = listenfd;  //关心listenfd,加入到结构体中
 
	if( epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0)   //将listenfd添加event结构体中
 	{
 		printf("epoll add listen socket failure: %s\n", strerror(errno));
 		return -4;
 	}
 
for ( ; ; )  //死循环,阻塞
 	{
 
 		events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1);  //等待文件描述符发生事件,event_array保存一次返回同时发生的事件的个数,MAX_EVENTS为数组大小,永不超时
 
		if(events < 0)
	 	{
 			printf("epoll failure: %s\n", strerror(errno));
 			break;
	 	}
 
		else if(events == 0)
 		{
	 		printf("epoll get timeout\n");
 			continue;
	 	}
 		//有事件发生,返回值为有多少个事件发生

		for(i=0; i<events; i++)   //遍历每个事件
	 	{
 			if ( (event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP) )    //如果事件出错或者断开连接
 			{
 				printf("epoll_wait get error on fd[%d]: %s\n", event_array[i].data.fd, strerror(errno));
 				epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);   //移除事件
 				close(event_array[i].data.fd);  //释放fd
 			}
 
			if( event_array[i].data.fd == listenfd )   //有新的客户端发起连接请求
 			{
 
				if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0)  //调用accept函数接受请求
 				{
 					printf("accept new client failure: %s\n", strerror(errno));
 					continue;
 				}
 	
				event.data.fd = connfd;   //把新的客户端文件描述符加入到结构体中
 				//event.events = EPOLLIN|EPOLLET;
 				event.events = EPOLLIN;
 
				if( epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) < 0 )   //将新的客户端文件描述符加入结构体中
 				{
 					printf("epoll add client socket failure: %s\n", strerror(errno));
 					close(event_array[i].data.fd);
 					continue;
 				}
 				
				printf("epoll add new client socket[%d] ok.\n", connfd);
 			}
 
			else   //如果是已经连接的客户端发生事件
 			{
				memset(buf, 0, sizeof(buf));

 				if( (rv=read(event_array[i].data.fd, buf, sizeof(buf))) <= 0)
 				{
 					printf("socket[%d] read failure or get disconncet and will be removed.\n",event_array[i].data.fd);
 					epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);   //删除相应的文件描述符
 					close(event_array[i].data.fd);
 					continue;
 				}
 
				else
 				{
 					printf("socket[%d] read get %d bytes data:%s\n", event_array[i].data.fd, rv, buf);
					 /* convert letter from lowercase to uppercase */
 			
					for(j=0; j<rv; j++)
 					buf[j]=toupper(buf[j]);
 
					if( write(event_array[i].data.fd, buf, rv) < 0 )
 					{
 						printf("socket[%d] write failure: %s\n", event_array[i].data.fd, strerror(errno));
 						epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
 						close(event_array[i].data.fd);
 					}
 				}
 			}
 		} 
 	}

用tcp_test_tool模拟客户端与服务器端通信:
在这里插入图片描述

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值