Linux - 多路复用I/O [select]

一、为什么需要?

看上一篇文章

二、基本思想

 

        #先构造一张有关描述符的表,然后调用一个函数,这些文件描述符的一个或者多个已准备好进行IO操作时的函数才返回;

 

        函数返回时告诉进程那个描述符已就绪,可以进行IO操作。

三、select函数 

      

 

大部分Unix/Linux都支持select函数,该函数用于探测多个文件句柄的状态变化。

 

1.头文件

 

#include <sys/time.h>
#include <sys/types.h> 
#include <unistd.h>

 

2.函数体

 

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval 

*timeout);

 

3.参数

 

nfds:最大的文件描述符+1

 

readfds:读事件的表

 

writefds:写事件的表

 

exceptfds:异常事件的表

 

timeout:超时检测  //填NULL表示一直阻塞,直到文件描述符准备好为止

 

4.宏选项

 用于设置文件描述符

       void FD_CLR(int fd, fd_set *set);//把表中的一个文件描述符删除
       int  FD_ISSET(int fd, fd_set *set);//检测文件描述符是否准备好了,准备好了返回1,否则返回0
       void FD_SET(int fd, fd_set *set);//加入到表中
       void FD_ZERO(fd_set *set);//清空表

 

5.返回值

         成功:准备好的文件描述符个数

        失败:-1;

        

6.优缺点分析

 

select基本原理:

 

首先创建一张文件描述符表(fd_set),通过使用特有的函数(select),让内核帮助上层用户循环检测是否有可操作的文件描述符,如果有则告诉应用程序去操作。

 

在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真

 

正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用

 

一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线

 

程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少

 

了资源占用。

 

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还

 

多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后

 

最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个

 

socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多

 

个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

 

这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触

 

发某个特定的响应。我们可以将这种模型归类为“事件驱动模型”。相比其他模型,使用select的事

 

件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多CPU,同时能够为多客户端提供服务。

 

如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

 

但这个模型依旧有着很多问题:

 

当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄;该模型将事件

 

探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,那么就会导

 

致后续的事件迟迟得不到处理,并且会影响新的事件轮询,在很大程度上降低了事件探测的及时性。

 

所以select机制只适用于"短作业" 处理机制. (处理时间短)
实例代码:

 

       案例一: 利用复用IO,简单解决对多个管道进行写操作时阻塞的问题
        
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc, const char *argv[])
{
	int fd1 = open("./f1", O_RDWR);
	int fd2 = open("./f2", O_RDWR);
	int fd3 = open("./f3", O_RDWR);

	//1.创建一张文件描述符表
	fd_set readfds, tmpfds;
	
	FD_ZERO(&readfds); //2.清空表

	FD_SET(fd1, &readfds);
	FD_SET(fd2, &readfds);
	FD_SET(fd3, &readfds);

	int maxfd = fd3;

	tmpfds = readfds;

	char buf[64] = {0};

	int ret, i;

	while(1)
	{
		readfds = tmpfds;

		ret = select(maxfd+1, &readfds, NULL, NULL,NULL);
		if(ret == -1)
		{
			perror("select");
			return -1;
		}

		for(i=fd1; i<maxfd+1; i++)
		{
			if( FD_ISSET(i, &readfds) )
			{
				read(i, buf, sizeof(buf));
			
				printf("%s\n", buf);

				memset(buf, 0, sizeof(buf));
			}
		
		}
	}

	close(fd1);
	close(fd2);
	close(fd3);

	return 0;
}

案例二

        利用复用IO,解决多个客户端对服务器进行收发数据阻塞的问题 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("sockfd");
		return -1;
	}

	printf("sockfd = %d\n", sockfd);

	//端口复用函数:解决端口号被系统占用的情况
	int on = 1;
	int k = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if(k == -1)
	{
		perror("setsockopt");
		return -1;
	}

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0 ,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(8888);
	serveraddr.sin_addr.s_addr = inet_addr("0"); //自动路由,寻找IP地址

	int len = sizeof(serveraddr);

	int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len);
	if(ret == -1)
	{
		perror("bind");
		return -1;
	}

	ret = listen(sockfd, 5);
	if(ret == -1)
	{
		perror("listen");
		return -1;
	}

	//select机制	
	fd_set readfds,tmpfds;//readfds读事件,tmpfds是用来修正表的

	FD_ZERO(&readfds);//清空表

	FD_SET(sockfd, &readfds);//加入表

	int maxfd = sockfd;//把文件描述符3设置为当前最大的文件描述

	tmpfds = readfds;//用于修正读事件表

	char buf[64] = {0};

	int  i;

	while(1)
	{
		readfds = tmpfds;//修正表

		ret = select(maxfd+1, &readfds, NULL, NULL, NULL);
		if(ret == -1)
		{
			perror("select");
			exit(-1);
		}

		for(i=sockfd; i<maxfd+1; i++)
		{
			if(FD_ISSET(i, &readfds))
			{
				if(i== sockfd)//先处理sockfd也就是3号文件描述符
				{
					printf("%d 已经准备好了\n", i);

					//建立连接,用accept函数
					int connfd = accept(i, NULL, NULL);

					printf("%d is link\n", connfd);

					FD_SET(connfd, &tmpfds);

					if(maxfd < connfd)
					{
						maxfd = connfd;
					}
				}

				else{
					
					memset(buf, 0, sizeof(buf));

					//处理connfd
					ret = recv(i, buf, sizeof(buf), 0);
					if(ret == -1)
					{
						close(i);
						perror("recv");
						return -1;
					}
					else if(ret == 0) 
					{
						printf("%d is unlink\n", i);
						FD_CLR(i, &tmpfds);
						close(i);
						break;
					}
					else{

						printf("%d:message=%s\n", i,buf);

					}
				}
			}	

		}

	}

	close(sockfd);

	return 0;
}

四、理解事件表的修正

这里来着重理解一下读事件表的修正:
                可以把这里的读事件表理解为是由一个两行多列的表组成

第一行为文件描述符;第二行为读事件响应标志位,该文件描述符读事件就绪就为1,没就绪就为零,这里举个例子

        

文件描述符0123456
事件响应标志位0000000

注:文件描述符的0 、1 、2 指的是标准输入、输出、错误输出

所以这里的读事件表表示的就是 4号 5号 6号文件描述符代表的客户端已连接上了 3号文件描述符代表的服务器,并且已加入读事件表,事件响应标志位的0代表的是该客户端并未就绪

----------------------------------------------------------------------------------------------------------------------------

                当4号文件描述符所代表客户端向服务器发送消息时,select函数会轮询检测到该文件句柄发生变化(就绪态),就把该客户端的事件响应标志位置为1,如下图所示

文件描述符0123456
事件响应标志位0000100

———————————————————————————————————————————

               当后续操作(服务器收到消息并打印出来)执行完时;需要把其对应的事件响应标志位置为初始状态,也就是0(上述代码中是用tmpfds保存的读事件表的初始状态)

 

保存读事件表的初始状态

———————————————————————————————————————————

 

把读事件表置为初始状态

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值