UNIX网络编程(六)---I/O复用:select和poll函数

一、概述

我们前面看到TCP客户同时处理两个输入:标准输入和TCP套接字。我们遇到的问题是客户阻塞于(标准输入)fgets调用期间,服务器进程会被kill。服务器TCP虽然正确地发送了一个FIN,但是客户进程正阻塞于标准输入的过程,它将看不到这个EOF,直到从套接字读时为止(可能已经过了很久我们才输入)。

这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定一个或者多个I/O条件就绪,它就通知进程,这个能力成为IO复用,是有select和poll这两个函数支持的。

         I/O复用典型使用在下列网络应用场合:

        1)当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。上一节

         2)一个客户同时处理多个套接字是可能的,不过比较少见。在16.5节结合一个web客户的上下文给出这种场合使用select的例子

          3)如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。本节

          4)如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。8.15节有这么一个例子

         5)如果一个服务器要处理多个服务或者多个协议(在13.5节讲述的inetd守护进程),就要用I/O复用

   I/O复用并非只限于网络编程,许多重要的应用程序也需要使用这项技术。
 

 

二、I/O模型

五种I/O模型:

阻塞式/非阻塞式 I/O、I/O复用、信号驱动I/O、异步I/O

一个输入操作通常包括两个不同的阶段:

1)等待数据准备好。

2)从内核向进程复制数据。

对于一个套接字输入:第一步就是涉及等待数据从网络中到达,当所等待的分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制应用到进程缓冲区。

 

阻塞式I/O模型:(最流行)

我们以UDP为例子:

可以看到:进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回时,应用进程开始处理数据报。

 

 

2、非阻塞式I/O模型

进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。16章中详细~

 

 

前三次调用recvfrom时没有数据返回,因此内核立即返回一个EWOLDBLOCK错误。第四次调用recvfrom时已经有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回,我们继续接着处理数据。

轮询往往耗费大量CPU时间。不过这种模型偶尔也会遇到,通常是专门提供某一种功能的系统中才用。

 

3、I/O复用模型

有了I/O复用,我们就能调用select或者poll上,阻塞在这两个系统上调用中的某一个上,而不是阻塞在真正的I/O上。

 我们阻塞与select调用,等待数据报套接字变为可读。当select返回的套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用缓冲区。

比较上面的两种阻塞/非阻塞式,I/O复用并不显得有什么优势,事实上,select的优势在于可以等待多个描述符就绪。

 

4、信号驱动时I/O模型

可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。

 

 我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理。也可以立即通知循环,让它读取数据报。

         无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
 

 

5、异步I/O模型

告知内核启动某个动作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成 后通知我们。

这种模型与信号驱动模型的主要区别:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O是由内核通知我们I/O操作何时完成

 

 

I/O模型对比

同步I/O模型:导致请求进程阻塞,直到I/O操作完成。

异步I/O模型:不导致请求进程阻塞。

 

 

 

前4中模型都是同步进程:真正的I/O操作将阻塞进程。

 

 

 

 

三、select函数

 

         select函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或者多个事件发生或经过某个指定的时间后才唤醒进程。也就是说,我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。我们所关心的描述字不受限于套接口,任何描述符都可用select来监听。

 

最后一个参数内核等待指定描述符的任何一个就绪花的时间。

三种可能:1、永远等待 (设置为空指针) 2、等待一段固定   3、根本不等待(设置为0),轮询。

但是这里的时间是要被操作系统给弄得很粗糙,向上舍入10ms的倍数。

timeout参数是const的不可被修改。

 

select使用描述符集,通常是一个数组,其中每个整数的每一位对应一个描述符。比如:32位整数,该数组的第一个元素对应于一个描述符0~31,第二个元素对应于描述符32~63,依次类推,所有这些实现都与程序无关,他们隐藏在名为fd_set的数据类型和以下四个宏中:

 

 

描述符就绪条件:

 

 

 

四、str_cli函数

 

使用select重写了上一节的str_cli函数,本版本阻塞与select调用:

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1;
	fd_set		rset;
	char		sendline[MAXLINE], recvline[MAXLINE];

	FD_ZERO(&rset);//init
	for ( ; ; ) {
		FD_SET(fileno(fp), &rset);
		FD_SET(sockfd, &rset);
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		Select(maxfdp1, &rset, NULL, NULL, NULL);//写,异常,timeout均为空(一直阻塞)

		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */
			if (Readline(sockfd, recvline, MAXLINE) == 0)
				err_quit("str_cli: server terminated prematurely");
			Fputs(recvline, stdout);
		}

		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
			if (Fgets(sendline, MAXLINE, fp) == NULL)
				return;		/* all done */
			Writen(sockfd, sendline, strlen(sendline));
		}
	}
}

依然存在问题:

在批量输入的情况下,我们写完不能立即关闭连接,因为管道中存在其他的请求和应答。问题在于我们对标准输入的EOF的处理:str_cli函数就此返回到main函数,main函数随后终止,标准输入的EOF并不意味着我们同时也完成了从套接字的读入;可能仍有请求在去往服务器的路上,或者仍有应答返回客户的路上。

缓冲机制:fgets读取输入,数据被转入到缓冲区中。然而fgets只返回第一行,其余输入行仍在stdio缓冲区中。select中不知道stdio使用了缓冲区,它只是从read的系统调用角度指出是否有数据可读,而不是从fgets之类的角度来考虑。所以不要混用select和stdio。

 

 

 

五、shutdown函数

       终止网络连接的方法通常是调用close函数,不过它有两个限制:

1)close把描述符引用计数-1,仅在该计数变为0时才关闭该套接字。使用shutdown可也不管引用计数就激发TCP的正常终止序列。

2)close终止读和写两个方向的数据传送。既然TCP是全双工的,我们就可以仅关闭一端的读和写。

 

 

 

所以有了str_cli的改进版本:

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, stdineof = 0;//0:正常 1:已经关闭
	fd_set		rset;
	char		sendline[MAXLINE], recvline[MAXLINE];

	FD_ZERO(&rset);
	for ( ; ; ) {
		if (stdineof == 0)
			FD_SET(fileno(fp), &rset);
		FD_SET(sockfd, &rset);
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		Select(maxfdp1, &rset, NULL, NULL, NULL);

		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */
			if (Readline(sockfd, recvline, MAXLINE) == 0) {
				if (stdineof == 1)
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");
			}

			Fputs(recvline, stdout);
		}

		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
			if (Fgets(sendline, MAXLINE, fp) == NULL) {
				stdineof = 1;
				Shutdown(sockfd, SHUT_WR);	/* send FIN */
				FD_CLR(fileno(fp), &rset);
				continue;
			}

			Writen(sockfd, sendline, strlen(sendline));
		}
	}
}

 

 

六、TCP回射服务器程序

前面为每个客户派生一个子进程,这里可以使用select来处理任何个客户的单进程程序。

用于跟踪客户的数据结构:

/* include fig01 */
#include	"unp.h"

int
main(int argc, char **argv)
{
	int					i, maxi, maxfd, listenfd, connfd, sockfd;
	int					nready, client[FD_SETSIZE];
	ssize_t				n;
	fd_set				rset, allset;
	char				buf[MAXLINE];
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	maxfd = listenfd;			/* initialize */
	maxi = -1;					/* index into client[] array */
	for (i = 0; i < FD_SETSIZE; i++)
		client[i] = -1;			/* -1 indicates available entry */
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);
/* end fig01 */

/* include fig02 */
	for ( ; ; ) {
		rset = allset;		/* structure assignment */
		nready = Select(maxfd+1, &rset, NULL, NULL, NULL);

		if (FD_ISSET(listenfd, &rset)) {	/* new client connection */
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef	NOTDEF
			printf("new client: %s, port %d\n",
					Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
					ntohs(cliaddr.sin_port));
#endif

			for (i = 0; i < FD_SETSIZE; i++)
				if (client[i] < 0) {
					client[i] = connfd;	/* save descriptor */
					break;
				}
			if (i == FD_SETSIZE)
				err_quit("too many clients");

			FD_SET(connfd, &allset);	/* add new descriptor to set */
			if (connfd > maxfd)
				maxfd = connfd;			/* for select */
			if (i > maxi)
				maxi = i;				/* max index in client[] array */

			if (--nready <= 0)
				continue;				/* no more readable descriptors */
		}

		for (i = 0; i <= maxi; i++) {	/* check all clients for data */
			if ( (sockfd = client[i]) < 0)
				continue;
			if (FD_ISSET(sockfd, &rset)) {
				if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
						/*4connection closed by client */
					Close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				} else
					Writen(sockfd, buf, n);

				if (--nready <= 0)
					break;				/* no more readable descriptors */
			}
		}
	}
}
/* end fig02 */

 

 

 

七、poll函数

poll函数和select函数类似

 

/* include fig02 */
	for ( ; ; ) {
		nready = Poll(client, maxi+1, INFTIM);

		if (client[0].revents & POLLRDNORM) {	/* new client connection */
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef	NOTDEF
			printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif

			for (i = 1; i < OPEN_MAX; i++)
				if (client[i].fd < 0) {
					client[i].fd = connfd;	/* save descriptor */
					break;
				}
			if (i == OPEN_MAX)
				err_quit("too many clients");

			client[i].events = POLLRDNORM;
			if (i > maxi)
				maxi = i;				/* max index in client[] array */

			if (--nready <= 0)
				continue;				/* no more readable descriptors */
		}

		for (i = 1; i <= maxi; i++) {	/* check all clients for data */
			if ( (sockfd = client[i].fd) < 0)
				continue;
			if (client[i].revents & (POLLRDNORM | POLLERR)) {
				if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
					if (errno == ECONNRESET) {
							/*4connection reset by client */
#ifdef	NOTDEF
						printf("client[%d] aborted connection\n", i);
#endif
						Close(sockfd);
						client[i].fd = -1;
					} else
						err_sys("read error");
				} else if (n == 0) {
						/*4connection closed by client */
#ifdef	NOTDEF
					printf("client[%d] closed connection\n", i);
#endif
					Close(sockfd);
					client[i].fd = -1;
				} else
					Writen(sockfd, buf, n);

				if (--nready <= 0)
					break;				/* no more readable descriptors */
			}
		}
	}
}
/* end fig02 */

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值