(原)Socket API 编程模型
(摘)I/O多路复用详解(二)

(摘)I/O多路复用详解(一)

孔令春 posted @ 2009年10月15日 05:00 in 网络安全 with tags Select IO多路复用 , 12853 阅读

        要想完全理解I/O多路复用,需先要了解I/O模型:

一、五种I/O模型

1、阻塞I/O模型

     最流行的I/O模型是阻塞I/O模型,缺省情形下,所有套接口都是阻塞的。我们以数据报套接口为例来讲解此模型(我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接口低潮标记等额外变量开始活动,导致这个概念变得复杂)。

     进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。我们就说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。

2、非阻塞I/O模型

      进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。

3、I/O复用模型

     调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。

4、信号驱动I/O模型

     首先开启套接口信号驱动I/O功能, 并通过系统调用sigaction安装一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据报准备好被读时,就为该进程生成一个SIGIO信号。随即可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也可以通知主循环,让它来读数据报。

5、异步I/O模型

     告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:
           信号驱动I/O:由内核通知我们何时可以启动一个I/O操作,
           异步I/O模型:由内核通知我们I/O操作何时完成。

 

二、I/O复用的典型应用场合:

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

 2、如果一个服务器要处理多个服务或者多个协议(例如既要处理TCP,又要处理UDP),一般就要使用I/O复用。

 

三、支持I/O复用的系统调用

     目前支持I/O复用的系统调用有select、pselect、poll、epoll:

1、select函数

     该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

格式为:

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
          
         返回:就绪描述字的正数目,0-超时,-1-出错

    我们从该函数的最后一个参数开始介绍,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,我们把该参数设置为空指针。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

    中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果我们对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 ?

目前支持的异常条件只有两个:

(1)某个套接口的带外数据的到达。

(2)某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。

    第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此我们把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。

 一个应用select的例子:

/**
  *TCP回射服务器客户端程序
  */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
#include <math.h>
#include <sys/select.h>
#include <sys/time.h>

#define SERVER_PORT 3333  //服务器端口号

void str_cli(FILE *fp, int sockfd)
{
	int maxfdp1, stdineof;
	fd_set rset;
	char buf[BUFSIZ];
	int n;

	stdineof = 0;
	FD_ZERO(&rset);

	while(1)
	{
		if( stdineof == 0 )
			FD_SET(fileno(fp),&rset);
		FD_SET(sockfd, &rset);

		maxfdp1 = ((fileno(fp) > sockfd) ? fileno(fp) : sockfd) + 1;

		select(maxfdp1, &rset, NULL, NULL, NULL);

		if( FD_ISSET(sockfd, &rset) )
		{
			if( (n = read(sockfd, buf, BUFSIZ)) == 0 )
				if( stdineof == 1 )
					return;
				else
					perror("server terminated prematurely");
			write(fileno(stdout), buf, n);
		}

		if( FD_ISSET(fileno(fp), &rset))
		{
			if( (n = read(fileno(fp), buf, BUFSIZ)) == 0 )
			{
				stdineof = 1;
				shutdown(sockfd, SHUT_WR);
				FD_CLR(fileno(fp), &rset);
				continue;
			}
			write(sockfd, buf, n);
		}
	}
}

int main(int argc, char *argv[])
{
	int sockfd[5];
	struct sockaddr_in servaddr;
	struct hostent  *hp;
	char buf[BUFSIZ];

	if( argc != 2 )
	{
		printf("Please input %s <hostname>\n", argv[0]);
		exit(1);
	}
	
	int i;
	for(i = 0; i < 5; ++i)
	{

		//创建socket
		if( (sockfd[i] = socket(AF_INET, SOCK_STREAM,0)) < 0 )
		{
			printf("Create socket error!\n");
			exit(1);
		}

		//设置服务器地址结构
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin_family = AF_INET;
		if( (hp = gethostbyname(argv[1])) != NULL )
		{
			bcopy(hp->h_addr, (struct sockaddr*)&servaddr.sin_addr, hp->h_length);
		}
		else if(inet_aton(argv[1], &servaddr.sin_addr) < 0 )
		{
			printf("Input Server IP error!\n");
			exit(1);
		}
		servaddr.sin_port = htons(SERVER_PORT);

		//连接服务器
		if( connect(sockfd[i],(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0 )
		{
			printf("Connect server failure!\n");
			exit(1);
		}
	}
	str_cli(stdin, sockfd[0]);

	exit(0);
}

 

注:本章内容摘自<Unix 网络编程>第六章。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter