I/O复用----select

本文章是基于《Linux高性能服务器编程》这本书的知识进行学习和分析的,大家有兴趣可以看看这本书的第九章。

一、I/O复用的介绍   

I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。 比如:

  • 客户端程序要同时处理多个socket,例如非阻塞的connect技术。
  • 客户端程序要同时处理用户输入和网络连接。例如聊天室程序。
  • 服务器要同时处理TCP和UDP请求,比如:回射服务器。
  • 服务器要同时监听多个端口,或者处理多种服务,比如xinetd服务器。
  • 服务器要同时监听socket和连接socket,这是I/O复用使用最多的场合。

       需要说明的是,I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的,并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的,如果要实现并发,只能使用多进程或多线程、进程池或线程池等手段。

       I/O复用:一个进程或者一个线程,能够同时对多个文件描述符(socket)提供服务

       服务器上的线程或者进程,如何将多个文件描述符统一监听,当任意一个文件描述符上有事件发生,其都能及时处理。它的三种手段: select  poll  epoll(linux独有)。今天我们就基于单线程单进程同时处理多个文件描述符来展开讲解select。


二、select的函数原型

     select系统调用的原型如下:

     1.nfds: 最大文件描述符值+1(提高效率); 比如:2 3 4 5   就传5+1  6从表示第6的位数位置开始检测

     2.fd_set  记录文件描述符   监听的文件描述符

         readfds: 可读事件的文件描述符集合       不但需要它往内核传递关注的文件描述符,也需要返回

         writefds: 可写事件的文件描述符集合     就绪的文件描述符,将就绪的和未就绪的都返回

         exceptfds:异常事件的文件描述符集合          

      3 .timeout :设置超时时间  

            结构体如下:

          

            有三种可能:

            1)   timeout=NULL(阻塞:直到有一个fd位被置为1,函数才返回)

            2)   timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)

            3)   timeout所指向的结构,时间设为0(非阻塞:函数检查完每个fd后立即返回)

     4.  select的返回值 :-1    出错

                                       ==0 超时

                                         >0  就绪的文件描述符个数

        也许大家对nfds为什么是最大文件描述符值加1,是因为我们是通过位fd进行标志文件描述符的, 32位 有1024个位来描述文件描述符 从0到1023 下标和个数差一  。如下图:

        我们可以用将最大文件描述符的值加1传递给select函数,这样在探测就绪的文件描述符时,就可以节省时间,只探测最大描述符+1之后的位置,既保证了正确性又提高了效率。

 看完select的函数原型后,我们需要考虑以下两个问题:

  1. 如何将文件描述符分别设置到readfds  writefds  exceptfds ?
  2. select返回后,如何知道那些文件描述符是就绪的?

      带着问题我们来看看fd_set这个结构体到底是什么吧~

可看出fd_set 其实是一个int 型的数组:

typedef struct
{

   int  fds_bits[32];

}fd_set;

       由以上定义可见,fd_set结构体仅包含了一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符、fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。由于位操作过于繁琐,我们应该使用下面的一系列宏来访问fd_set结构体中的位:

 有上图可看出,fd_set 的4个宏函数可以解决上面两个问题

1.前三个函数可以解决“如何设置到readfds  writefds  exceptfds ”问题:

 FD_ZERO可以在每一轮开始的时候清除fdset的所有位,类似于初始化;

 FD_SET可以设置位fd;

 FD_CLR是可以清除位fd。

2.最后一个函数可以解决“select返回后,如何知道那些文件描述符是就绪的?”的问题。

 FD_ISSET函数就是用来检测fdset的位fd是否被设置

      select是I/O复用中最麻烦又相比效率不高的一个函数,但是它是I/O复用的入门和理解,让我们明白什么是I/O复用。学完select原型和分析后,我们要明白以下几点。

  1. select 函数原型    
  2. fd_set  结构体的定义 ----> int  fds[32];                                                                                                                                   按位表示关注的文件描述符(不用考虑是否超过32位,宏函数已经写好,有兴趣的可以深入看看) 如何设置 前三个宏函数
  3. 如何返回就绪文件描述符 ?内核仅仅将fd_set结构体变量中将就绪文件描述符的位 修改  1----> 0   a |= 1  <<  n   (将第几位设置为1)
  4. 应用程序如何探测就绪的文件描述符? 循环 fd_isset()  就绪和非就绪的都会返回,所以得都探测查看一下
  5. 每次调用select之前重新设置读、写、异常事件

    三、select代码实现

         此代码是在linux环境下编写的,首先我先给出伪代码:

int main()

{

  1. TCP服务器设置  socket bind listen
  2. 将sockfd添加到fds中
  3. 启动while循环

             3.1将fds中的文件描述符设置到readfds上

             3.2启动select 完成监听

             3.3循环探测那些文件描述符就绪

                         3.3.1 Sockfd     ---》 有客户端完成了三次握手  accept  insert_fd 

                         3.3.2 连接 fd  (就是c)  ---》客户端有数据到达 recv的值

                                                                 c <= 0  close fd ,delete_fd

                                                                   c >0  处理数据

}

 接着我将实现一个简单的只关注读事件的select代码,如下:

//ser代码
int maxfd = -1;
int socketfds[1024];  //这里也可以将1024用define宏表示

void Initfds()
{
	int i = 0;
	for(; i < 1024;++i)
	{
		socketfds[i] = -1;//0也算一个文件描述符,所以设置为-1
	}
}

int  Addfd(int fd)
{
	int i = 0;
	for(; i < 1024; ++i)
	{
		if(socketfds[i] == -1)
		{
			socketfds[i] = fd;
			return 1;
		}
	}
	return 0;
}

int Deletefd(int fd)
{
	int i = 0;
	for(;i < 1024; ++i)
	{
		if(socketfds[i] == fd)
		{
			socketfds[i] = -1;
			return 1;
		}
	}
	return 0;
}

int CreateSocket(int port,char *ip)
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//协议簇  TCP协议
	assert(sockfd != -1);

	struct sockaddr_in ser,cli;
	memset(&ser,0,sizeof(ser));

	ser.sin_family = AF_INET;
	ser.sin_addr.s_addr = inet_addr(ip); //IP地址
	ser.sin_port = htons(port);

	int res = bind(sockfd,(struct sockaddr *)&ser,sizeof(ser));
	assert(res != -1);//绑定失败 1.IP地址不对 2.端口号被占用或者没有权限使用
	listen(sockfd,5); //size = 5 内核维护的已经完成链接客户端的文件描述符个数(6)实际会加一

	Addfd(sockfd);
	return sockfd; 
}

int main()
{
	Initfds();
	int sockfd = CreateSocket(6888,"127.0.0.1");

	fd_set readfds;//只关注读事件

	while(1)
	{
		FD_ZERO(&readfds);
		int i = 0;
		for(; i < 1024; ++i)
		{
			if(socketfds[i] != -1)
			{
				FD_SET(socketfds[i],&readfds);
				if(socketfds[i] > maxfd)
				{
					maxfd = socketfds[i];
				}
			}
		}

		int n = select(maxfd + 1,&readfds,NULL,NULL,NULL);//最大描述符加一
		if(n <= 0)
		{
			printf("error\n");
			exit(0);
		}

		for(i = 0;i < 1024;++i)
		{
			int fd = socketfds[i];
			if(fd != -1 && FD_ISSET(fd,&readfds))
			{
				if(fd == sockfd) //客户链接
				{
					struct sockaddr_in cli;
					int len = sizeof(cli);

					int c = accept(fd,(struct sockaddr*)&cli,&len);
					if(c == -1)
					{
						printf("link is error\n");
						continue;
					}

					Addfd(c);
				}
				else  //有事件(数据)发生
				{
					char buff[128] = {0};
					int n = recv(fd,buff,127,0);
					if(n <= 0) //异常 客户端有异常
					{
						close(fd);
						Deletefd(fd);
						continue;
					}

					printf("%d:  recv: %s\n",fd,buff);
					send(fd,"ok",2,0);
				}
			}
		}

	}
}
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//cli的代码
int main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in ser,cli;//服务器的IP地址 端口号
	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 = connect(sockfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(res != -1);

	while(1)
	{
		printf("please input:");
		char buff[128] = {0};
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3) == 0)
		{
			close(sockfd);
			break;
		}
		send(sockfd,buff,strlen(buff)-1,0);

		char recvbuff[128] = {0};
		int n = recv(sockfd,recvbuff,127,0);
		if(n <= 0)
		{
			close(sockfd);
			break;
		}
		printf("client recv data: %s\n",recvbuff);
	}
}

结果如下:

四、select的特点

     select: 关注可读、可写 、异常事件   

      1.记录每种事件的结构  (在数组按位来记录关注的文件描述符上的事件)

      2.每次做多可以监听1024个文件描述符,并且其最大值1023。因为底层是一个int型32位数组

      3.select函数返回时,通过传递的结构体变量将结果带回 (就绪的文件和未就绪的文件描述符),并且内核会修改用户变量

                a.每次都必须循环探测那些文件描述符就绪  时间复杂度为O(n)

                b.每次调用select之前都必须重新设置三个结构体变量

       4.select函数第一个参数  最大的文件描述符值+1   提高底层效率

        用户传递的文件描述符和内核反馈的文件描述符都是通过select参数,所以每次调用select之前必须重新设置结构体,内核会将所有的文件描述符返回所以用户探测就绪文件描述符的时间复杂度O(n)。用户态和内核态交互,内核修改后再传递给用户态。如下图:

        如果要处理业务,可以使用多进程,多线程  ,高效的处理事件模式reactor模式,另一个是proactor模式。大家感兴趣的话可以了解一下,在书中的第八章~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值