多路复用IO模型之select与并发问题进一步优化

多路复用I/O

多路I/O复用表示支持多个任务同时对某一进程的I/O进程操作,普通的read/write只能实现同一时间操作一个,无法实现网络通信的并发操作。那么多路复用I/O分为三种机制:select/poll/epoll

多进程多线程的socket模型具有明显缺陷

1.占用内存多 2.进程(线程)切换时间多。3.进程(线程)之间同步麻烦

多路复用的解决理念:

在主控线程中将需要监控的文件描述符保存到文件描述符集中,该文件描述符集为一个位图,我们知道文件描述符正常情况下总是累加上去的,也是一个整数,因此这个整数巧好可以表示该文件描述符在位图的位置(例如位图上的3号位置为1,表示文件描述符等于3有事件发生,否则为空闲。),将服务器和客户端的文件描述符加入到该(文件描述符集)位图中进行监控,若有事件发生则才处理。

API分析:

  1. int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval timeout);
    nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
    readfds:监控有读数据到达文件描述符集合,传入传出参数
    writefds:监控写数据到达文件描述符集合,传入传出参数
    exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
    timeout:定时阻塞监控时间,3种情况
    1.NULL,永远等下去
    2.设置 timeval,等待固定时间
    3.设置 timeval里时间均为0,检查描述字后立即返回,轮询
    struct timeval {
    long tv_sec; /
    /
    long tv_usec; /
    微妙 */
    };

  2. void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0

  3. int FD_ISSET(int fd, fd_set *set); 测试文件描述符集合里fd是否置1

  4. void FD_SET(int fd, fd_set *set); 把文件描述符集合里fd位置1

  5. void FD_ZERO(fd_set *set); 把文件描述符集合里所有位清0

函数2-5均为宏定义函数

通过上述API可知道:
select监控的有三种事件的文件描述符集(读、写、异常)。以及可设置阻塞时间,大大地解决死等的弊端,select无请求后,主控函数将执行后续指令。
但是要注意的是

  1. select监听的文件描述符集最大支持1024,大小可用宏定义表示FD_SETSIZE( = 1024),解决1024以下客户端时使用select是很合适的,后面会讲解poll,解决1024的限制。
  2. select采用的是轮询模型,数量级在千级还是很适合,但是如果更大会导致每次监控都要从文件描述符集从0到maxfd+1进行遍历,会大大降低服务器响应效率,不应在select上投入更多精力

扩展:

int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);
参数timeout类型:
	struct timespec {
	long tv_sec; /* 秒 */
	long tv_nsec; /* 纳秒 */
	};

用sigmask替代当前进程的阻塞信号集,调用返回后还原原有阻塞信号集
我们知道调用pause()函数时,会因多任务竞态导致pause()无法被定时信号唤醒,而suspend则集成睡眠与信号屏蔽的功能,保证信号提前抵达,主程序从其他任务切换回来后能够正常执行。该pselect也是同样功能,不过该用法不多。

关于网络并发优化问题:

  1. 大量客户端连接到服务器后,会从0遍历到maxfd,返回就绪态任务数量,再根据数量在通过FD_ISSET重新遍历判断事件发生的文件描述符,如果某些客户端断开连接(文件描述符小于maxfd)造成浪费在遍历已断开连接的客户端上的时间,由于select函数固定,第一次遍历优化不方便(修改内核源码),我们可以对第二次遍历进行优化。
  2. 我们知道文件描述符从0-2是标准输入、输出、错误,这三个不需要遍历。
  3. 如果在处理客户端数据时,某一次read没有对数据读完,那么造成重新进行下一次时select,获取上一次未处理完的文件描述符,从0开始遍历到maxfd,对上一次的进行再一次操作,效率十分低。

对应的代码优化:

  1. 定义一个客户端数组,初始化为-1,表示空闲,等待加载客户端信息,按顺序记录下连接成功的客户端文件描述符到该数组,客户端遍历该数组注册到元素值为-1的位置,若某一客户端断开连接,首先会产生读事件(即客户端会向服务器发送FIN信号),在服务器端select监控到有读事件发生,遍历客户端数组,并用FD_ISSET(fd,&readfds)判断具体是哪一个客户端产生事件,获取到客户端文件描述符后,调用close关闭服务器端打开的客户端文件描述符,在用FD_CLR清除监控该文件描述符,并将该客户端对应的数组位置等于-1,表示空闲,这样在第二次遍历时,我们可以通过遍历客户端数组提高效率,也同时解决了问题2,客户端数组只存有客户端文件描述符。
  2. 对读缓存区循环读,直到返回EAGAIN在处理数据。

附上详细注释的代码(只包含服务器端):

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "wrap.h"

#define SERV_PORT 8000
#define MAXLINE 128

int main(void)
{
	int serv_fd,connfd,sockfd,maxfd,maxi,i;
	int nready, client[FD_SETSIZE];	//FD_SETSIZE=1024,select模型最大监控数
	ssize_t n;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	struct sockaddr_in servaddr,cliaddr;
	struct socklen_t cliaddr_len;
	fd_set rset, allset;
	//1.创建socket
	serv_fd = Socket(AF_INET,SOCK_STREAM,0);
	//2.绑定socket对应网络进程
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	Bind(serv_fd, &servaddr, sizeof(servaddr));
	//3.创建监听队列
	listen(serv_fd,20);
	//4.1.select监听的最大文件描述符,告诉内核检测前多少个文件描述符的状态
	maxfd = serv_fd;
	maxi = -1;
	//4.2.初始化记录客户端socket号的缓冲区,空闲则为-1,非-1表示已有客户占有
	for(i = 0; i< FD_SETSIZE; i++)
		client[i] = -1;
	//4.3.初始化select读/写/出错的文件描述符集合
	FD_ZERO(&allset);
	//4.4.将服务器的网络进程加入到该监听队列
	FD_SET(serv_fd,&allset);

	for(;;){
		//rset作为操作对象,备份,每次循环都要将监控的文件描述符集的状态重新赋值
		rset = allset;
		//4.5.select开始阻塞监听读事件发生,rset的变化由内核实现
		nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
		//5.1.循环select错误判断,成功返回描述符集内事件发生的个数,没有则为0
		if(nready < 0)
			perr_exit("select error");
		//5.2.测试描述符集内服务器是否发生事件
		if(FD_ISSET(serv_fd, &rset)){
			cliaddr_len = sizeof(cliaddr);
			//5.2.1.调用accept与客户端进行连接
			connfd = Accept(serv_fd, (struct sockaddr *)&cliaddr,\
					 &cliaddr_len);
			printf("received form %s at PORT %d\n",\
		inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,str,sizeof(str)),\
				ntohs(cliaddr.sin_port));
			//5.2.2.1.将该客户端的信息保存在数组中
			for(i = 0; i<FD_SETSIZE; i++)
				if(client[i]<0){
					client[i] = connfd;
					break;
				}
			//5.2.2.2.若自定义保持客户端信息的数组已满,则报错退出
			if(i == FD_SETSIZE){
				fputs("too many clients\n", stderr);
				exit(1);
			}
			//5.2.3.将客户端文件描述符加入监控的文件描述符集中
			FD_SET(connfd, &allset);
			//5.2.4.对要监控的最大文件描述符进行修改
			if(connfd > maxfd)
				maxfd = connfd;
			//5.2.5.更改client数组下标
			if(i > maxi)
					maxi = i;
			/*5.2.6.如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未
					处理完的就绪文件描述符*/
			if(--nready == 0)
				continue;
		}
		//循环判断客户端是否有事件发生
		//6.获取已连接的客户端信息
		for(i = 0; i<=maxi; i++){
			if((sockfd = client[i])<0)
				continue;
			//6.1.判断当前客户端是否有事件发生
			if(FD_ISSET(sockfd, &rset)){
				//6.1.1.阻塞接受客户端数据
				n = Read(sockfd, buf, MAXLINE);
				//6.1.2.1.若为0,表示客户端异常断开连接
				if(n == 0){
					Close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				//6.1.2.2.否则,处理数据并回传给客户端
				}else{
					int j;
					for(j = 0; j < n; j++)
						buf[j] = toupper(buf[j]);
					Write(sockfd, buf, n);
				}
				//6.1.3. 跳出循环
				if(--nready == 0)
					break;
			}
		}
	}
	//7.关闭服务器
	close(serv_fd);
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值