高性能服务器编程-----I/O多路复用(select)

我们从多进程/多线程编程到进程池与线程池编程,效率在一步一步提高,但是对于线程池/进程池还是有个弊端,就是一旦分配一个进程/线程与某一个客户端进行交互时,这个进程/线程不论客户端有没有事件请求,都必须等待直到客户端与其断开这个进程/线程才能与其他客户端进行交互,实际上这是非常不好的,因为我们根本不知道客户端什么时候断开链接,那么在客户端没有事件请求的时候,我们怎么样将这些时间利用起来处理有事件发生的客户端连接呢?如何将多个文件描述符同一监听,当任意一个文件描述符上有事件发生就能够及时处理呢?

什么是I/O多路复用?

多路是个什么概念?

多路指的是多条独立的i/o流,i/o流可以这么理解:读是一条流(称之为读流,比如输入流),写是一条流(称之为写流,比如输出流),异常也是一条流(称之为异常流),每条流用一个文件描述符来表示,同一个文件描述符可以同时表示读流和写流。

复用的是什么东西?

复用的是线程,复用线程来跟踪每io的状态,然后用一个线程就可以处理所有的io

总的来说I/O多路复用就是一个线程追踪多条io流(读,写,异常),但cpu不使用轮询,而是由设备本身告知程序哪条流可用了,这样一来就解放了cpu,也充分利用io资源(一个线程同时监听多个文件描述符,相当于一条路可以很多人同时走)

我们为什么不用循环来处理io流呢?

有人会说我不用这个I/O多路复用也能在一个线程就处理完所有的io流,用个循环挨个处理一次不就解决了嘛?那为什么还要提出这个技术呢?原因我们想的方法(轮询)效率太低了,资源利用率也不高。如果某个io被设置成了阻塞io,那么其他的io都将被卡死,也就浪费掉了其他的io资源。另一方面,假设所有io被设置成非阻塞,那么cpu一天到晚也不用干别的事了,就在这不停的问,现在可以进行io操作了吗,直到有一个设备准备好环境才能进行io,也就是在设备准备io环境的这一段时间,cpu是没必要瞎问的,问了也没结果。

为什么不选择多线程/多进程来处理呢,这样就不会阻塞了呀?

硬件发展起来了,有了多核的概念,也就有了多线程。这个时候可以这样做,来一条io我开一个线程,这样的话再也不用轮询了。然而,管理线程是很耗费系统资源的,另外线程之间的交互是十分麻烦的(开发人员很头疼)。这样一来程序的复杂性蹭蹭蹭地往上涨,io效率是可能提高了,但是软件的开发效率以及维护效率却可能减低了。

I/O复用接口---select

linux下这一技术有三个实现,select,poll,epoll(只支持linux)

select在一段指定时间内,监听用户感兴趣的文件描述符上的可读,可写,异常等事件,即将多个文件描述符统一监听。

select  API

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

//成功返回就绪文件描述符总数,若超时时间没有事件就绪,返回0,出错返回-1
  • nfds:指定被监听的文件描述符的总数,被设置为上面三种事件中最大的文件描述符+1(比它大的就不需要监听了,文件描述符从0开始计数)
  • readfds,读流集合,也就是程序员希望从这些描述符中读内容
  • writefds,写流集合,也就是程序员希望向这些描述符中写内容
  • exceptfds,异常流集合,也就是中间过程发送了异常
  • timeout,设置select的超时时间

fd_set结构体

可以看出fd_set实际上就是一个long int型的32个元素的整形数组,该数组的每一位标记一个文件描述符,也即最大能监听的文件描述符的个数是1024个,最大文件描述符是1023.

系统提供了一组宏来访问fd_set结构体中的位:

#include<sys/select.h>
FD_SET(int fd, fd_set* _fdset),把fd加入_fdset集合中
FD_CLR(int fd, fd_set* _fdset),把fd从_fdset集合中清除
FD_ISSET(int fd, fd_set* _fdset),判定fd是否在_fdset集合中
FD_ZERO(fd_set* _fdset),清除_fdset有描述符

struct timeval结构体

timeout有三种取值:

  • NULL,select一直阻塞,直到readfds、writefds、exceptfds集合中至少一个文件描述符就绪select才返回
  • 0,select不阻塞
  • timeout_value,select在timeout_value这个时间段内阻塞,有事件就绪就返回或者超时返回

如果非得与“多路”这个词关联起来,那就是readfds+writefds+exceptfds的数量和就是路数。

代码实现

内核是如何告诉通知我们有事件发生了呢?看下图

实现前几点注意问题

<1>select返回的时候返回全部的文件描述符,我需要循环来确定那个文件描述符上的事件发生了

<2>select返回会将我们fd_set结构的变量更改,所以我每次select之前需要重置fd_set的值,所以用户必须自己保存所有文件描述符的值已为下次重置

示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define LEN 1024
void InitFds(int *fds)   //初始化fds
{
	memset(fds,-1,sizeof(int)*LEN);  //设置为-1
}
void InsertFds(int *fds,int fd)
{
	int i=0;
	for(;i<LEN;i++)
	{
		if(fds[i]==-1)
		{
			fds[i]=fd;
			break;
		}
	}
}
void DeleteFds(int *fds,int fd)
{
	int i=0;
	for(;i<LEN;i++)
	{
		if(fds[i]==fd)
		{
			fds[i]=-1;
		}
	}
}
int main()
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd!=-1);

	struct sockaddr_in ser,cli;
	ser.sin_family=AF_INET;
	ser.sin_port=htons(7000);
	ser.sin_addr.s_addr=inet_addr("127.0.0.1");

	int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(-1!=res);
	listen(sockfd,5);

	fd_set readfds;   //用于检测可读事件
	int fds[LEN];    //用于保存监听的描述符以用来设置readfds
	InitFds(fds);   //将fds初始化
	InsertFds(fds,sockfd);  //将监听sockfd插入fds中
	while(1)
	{
		int maxfd=-1;  //记录最大的监听文件描述符
		FD_ZERO(&readfds); //因为select返回就会将readfds修改,每次都需要重新设置监听
		int i=0;
		for(;i<LEN;i++)  //将fds中所要监听的文件描述父加入readfds中
		{
			if(fds[i]!=-1)
			{
				if(fds[i]>maxfd)
				{
					maxfd=fds[i];  
				}
				FD_SET(fds[i],&readfds);
			}
		}
		int n=select(maxfd+1,&readfds,NULL,NULL,NULL); //启动监听,会阻塞的,有事件就绪才会返回
		if(n<=0)  //不存在超时,因为timeout设置NULL,出错-1
		{
			printf("Select error\n");
			continue;
		}
		else
		{
			for(i=0;i<LEN;i++) //循环判断那个链接描述符上有事件发生
			{
				if(fds[i]!=-1&&FD_ISSET(fds[i],&readfds))
				{
					if(fds[i]==sockfd) //链接发生
					{
						int len=sizeof(cli);
						int c=accept(sockfd,(struct sockaddr*)&cli,&len);
						if(c<0)
						{
							printf("%d:CLient link error\n",c);
							break;
						}
						InsertFds(fds,c);  //将链接c放入fds中循环上去监听
					}
					else  //链接描述符可读事件
					{
						char buff[128]={0};
						int n=recv(fds[i],buff,127,0);
						if(n<=0) //客户端断开链接
						{
							close(fds[i]); //释放链接
							DeleteFds(fds,fds[i]);  //将fds[i]在fds中删除
							continue;
						}
						printf("%d:%s\n",fds[i],buff);
						send(fds[i],"ok",2,0);  //如果这里处理的很慢,效率也不高,但是业务处理往往比较快,对于业务处理我们往往采用多线程,集群分布等,这里简单处理
					}
				}
			}
		}
	}
}

猜你喜欢

转载自blog.csdn.net/Eunice_fan1207/article/details/84674204