Linux/UNIX网络编程笔记 - I/O复用

什么是I/O复用?

What weneed is the capability to tell the kernel that we want to be notified ifone or more I/O conditions are ready (i.e., input is ready to be read, or thedescriptor is capable of taking more output). This capability is calledI/O multiplexing and is provided by the select and poll functions.——来自《Unix网络编程》第三卷

在很多情况下,使用select或是poll,可以把事件的响应交给底层操作系统来管理,当有I/O事件发生时,操作系统会通知我们。

何时使用I/O复用:

1When a client is handling multipledescriptors (normally interactive input and a network socket), I/O multiplexingshould be used. This is the scenario we described previously.

2It is possible, but rare, for a clientto handle multiple sockets at the same time. We will show an example of thisusing select in Section 16.5 in the context of a Web client.

3If a TCP server handles both a listeningsocket and its connected sockets, I/O multiplexing is normally used.

4If a server handles both TCP and UDP,I/O multiplexing is normally used.

5If a server handles multiple servicesand perhaps multiple protocols, I/O multiplexing is normally used.

——来自《Unix网络编程》第三卷

I/O模型

对于read而言,一般都会涉及到两个过程:

1. Waitingfor the data to be ready 
2. Copying the data from the kernel to the process

接下来的讨论,会根据这两阶段的操作进行描述。

I/O一共有5大模型:

1、阻塞I/O

      应用进程产生一个system call ,如果内核没有数据准备好,则会一直wait,处于阻塞,当内核数据准备好之后,将会把数据从内核再拷贝到应用进程,这一copy过程也处于阻塞状态。

2、非阻塞I/O

      之所以称作为非阻塞I/O,就意味着当应用进程产生一个system call的时候,不管内核的数据是否准备好,都会立即返回。而后,再一次发起call,这是一个轮询的过程。当内核数据准备好之后,便可以正常进行响应。这一过程是非阻塞的。而当数据从内核copy到应用进程的过程,仍然是阻塞,应为要保证数据完整与一致。

3、I/O复用

     使用I/O复用,一个或多个 system call 阻塞于select 或是 poll,而不是阻塞与真正的调用。当内核有数据准备好的时候,会通知select或是poll,接下来,会发起真正的system call,也就是图片中的recvfrom。之后,便会正常copy数据到应用进程。值得注意的是,I/O复用产生了两次system call,一次selectpoll),一次recvfrom。因此,如果进程只是处理单一描述字(descriptor)的话,使用I/O复用不但不会有好的效果,而且还会有额外的系统开销,所以,I/O复用一般都用于处理多个描述字(descriptors)的情况下。

4、信号驱动I/O

    我们可以使用信号驱动I/O,当有描述字准备好后,内核会产生信号来通知应用进程。信号驱动模型不同于上述三种,对于应用进程而言,它在等待接受数据过程中,处于被通知状态。这一过程,相当于一个异步操作。但是,对于内核copy数据到应用进程这一过程,应用进程仍然处于阻塞的状态。

5、异步I/O

   信号驱动I/O模型中,在等待内核数据准备阶段中,是一个异步的过程,而数据copy阶段则是阻塞的,也就是同步的。但是对于异步I/O模型而言,这两个阶段都是异步的。也就说,当引用进程产生一个aio_read后,它会继续执行其他操作,整个过程不会产生任何阻塞。

“We callaio_read (the POSIX asynchronous I/O functions begin with aio_ or lio_) andpass the kernel the descriptor, buffer pointer, buffer size (the same threearguments for read), file offset (similar to lseek), and how to notify us whenthe entire operation is complete.” ——来自《Unix网络编程》第三卷

5I/O模型的比较总结:

 

 

select函数

该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或是多个事件发生或经历一段指定的时间后才唤醒它。我们调用select告知内核对哪些描述字(就读、写或异常条件)感兴趣以及等待多长时间。我们感兴趣的描述字不局限于套接口,任何描述字都可以使用select来测试。

select函数原型:

#includeselect.h>
#include
int select (int maxfd , fd_set *readset ,fd_set *writeset,fd_set *exceptionset , const struct timeval * timeout);
返回:就绪描述字的正数目,0——超时,-1——出错

select函数的参数介绍:maxfd表示待测试的描述字个数,其值应该为最大的描述字+1,中间的readset,writeset,exceptionset指定我们要让内核测试读、写、异常条件的描述字,最后一个参数告知内核等待所指定描述字中的任何一个就绪可花多长时间。

timeval结构:

struct timeval {
long tv_sec; //seconds
long tv_usec ; //microseconds
}

timeval参数有三种可能值:1NULL:代表永远等待下去,相当于完全阻塞。2、一个固定的值,代表等待一段固定的时间。3timeval的属性值为0,表示根本不等待,检查描述字之后立即返回,也就是说事非阻塞的。

fd_set结构:

fd_set结构表示一个描述字集。它典型的应该以一个整数数组来表示,其中每个整数中的每一位对应一个描述字。关于fd_set有以下四个宏:

void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */

select函数修改由指针readsetwritesetexceptionset所指向的描述字集,因而这三个参数都是值-结果参数。也就是说,在select函数执行过程中,会修改其中的值。调用该函数时,我们指定关心的描述字的值,该函数返回时,结果指示哪些描述字已就绪。该函数返回后,我们使用FD_ISSET来测试fd_set数据类型中的描述字。描述字集中任何与未就绪的描述字对应的位返回时均清为0.为此,每次重新调用select函数中,我们都得再次把所有描述字集合中的所关心的位置为1。这也是在稍候的通信例子里,我们设置ressetallset两个集合的原因所在。

select函数返回某个套接口就绪的条件:

select函数的通信例子:一个简单的TCP回射服务器程序

SelectServer.c: 使用select机制的服务器端程序

 

#include 
#include 
#include 
#include 
#include 
#include 
 
const static int MAXLINE = 1024;
const static int SERV_PORT = 10001;
 
int main1()
{
   int i , maxi , maxfd, listenfd , connfd , sockfd ;
   /*nready 描述字的数量*/
   int nready ,client[FD_SETSIZE];
   int n ;
   /*创建描述字集合,由于select函数会把未有事件发生的描述字清零,所以我们设置两个集合*/
   fd_set rset , allset;
   char buf[MAXLINE];
   socklen_t clilen;
   struct sockaddr_in cliaddr , servaddr;
   /*创建socket*/
   listenfd = socket(AF_INET , SOCK_STREAM , 0);
   /*定义sockaddr_in*/
   memset(&servaddr , 0 ,sizeof(servaddr));
   servaddr.sin_family = AF_INET;
   servaddr.sin_port = htons(SERV_PORT);
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 
   bind(listenfd, (struct sockaddr *) & servaddr , sizeof(servaddr));
   listen(listenfd , 100);
   /*listenfd 是第一个描述字*/
   /*最大的描述字,用于select函数的第一个参数*/
   maxfd = listenfd;
   /*client的数量,用于轮询*/
   maxi = -1;
   /*init*/
   for(i=0 ;i maxfd) maxfd =connfd;
            if(i>maxi) maxi = i;
            if(nready<=1) continue;
            else nready --;
       }
 
       for(i=0 ; i<=maxi ; i++)
       {
            if (client[i]<0) continue;
            sockfd = client[i];
            if(FD_ISSET(sockfd,&rset))
            {
                n = read(sockfd , buf ,MAXLINE);
                if (n==0)
                {
                    /*当对方关闭的时候,server关闭描述字,并将set的sockfd清空*/
                    close(sockfd);
                    FD_CLR(sockfd,&allset);
                    client[i] = -1;
                }
                else
                {
                    buf[n]='\0';
                    printf("Socket %d said: %s\n",sockfd,buf);
                    write(sockfd,buf,n);//Write back to client
                }
                nready --;
                if(nready<=0) break;
            }
       }
 
   }
   return 0;
}

Client.c: 简单的客户端程序

#include
#include
#include
#include 
#include
 
#define MAXLINE 1024
int main()
{
    int sockfd ,n;
    char buf [MAXLINE];
    sockfd = socket(AF_INET,SOCK_STREAM ,0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0,sizeof(servaddr));   
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(10001);
    inet_pton( AF_INET ,"127.0.0.1" ,&servaddr.sin_addr ) ;
 
    connect(sockfd,(struct sockaddr*)&servaddr , sizeof(servaddr));
    while(1)
    {
        printf("type some words...\n");
        scanf("%s",buf);
        write(sockfd,buf,sizeof(buf));
        n = read(sockfd,buf,MAXLINE);
        printf("%d bytes received \n",n);
        buf[n] = '\0';
        printf("%s\n",buf);
    }
    close(sockfd);
    return 0;
}


poll函数

   在上文unix下网络编程之I/O复用(二)中已经介绍了select函数的相关使用,本文将介绍另一个常用的I/O复用函数pollpoll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

poll函数原型:

#include

int poll (struct pollfd * fdarray , unsigned long nfds , int timeout);

//返回:就需描述字的个数,0——超时,-1——出错

第一个参数是指向一个结构数组第一个元素的指针,每个数组元素都是一个pollfd结构。如下:

struct pollfd {

int fd; //descriptor to check

short events; //events of interest on fd

`   short revents; //events tha occurred on fd

}

要测试的条件由events成员指定,函数在相应的revents成语中返回该描述字的状态。(每个描述字都有两个变量,一个为调用值,另一个为返回结果,从而避免使用值-结果参数,这与select函数是不同的)。下图列出了用于指定events标志以及测试revents标志的一些常值。

上图需要注意的是,POLLERR,POLLHUP,POLLNVAL是处理错误的描述字,因此它们也就不可以出现在input事件中,即eventspoll识别三类数据:普通(normal),优先级带(priority band)和高优先级(high priority)。

TCPUPD而言,以下条件引起poll返回特定的revents

1 All regular TCP data and all UDP data is considered normal. 
2
TCP's out-of-band data (Chapter 24) is considered priority band. 
3
When the read half of a TCP connection is closed (e.g., a FIN is received),this is also considered normal data and a subsequent read operation will return0. 
4
The presence of an error for a TCP connection can be considered either normaldata or an error (POLLERR). In either case, a subsequent read will return –1with errno set to the appropriate value. This handles conditions such as thereceipt of an RST or a timeout. 
5
The availability of a new connection on a listening socket can be consideredeither normal data or priority data. Most implementations consider this normaldata. 
6
The completion of a nonblocking connect is considered to make a socketwritable.

                                                                                   ——unix网络编程》第三版

参数nfds,指示结构数组中元素的个数。

参数timeout

select中的timeout不同,poll函数的timeout参数是一int值,表示poll函数返回前等待多长时间,它是毫秒级别的。它有三种情况的取值:1INFTIM(一个负数值),表示永远等待,即一直阻塞。20,表示立即返回,非阻塞。3>0,表示正待指定数目的毫秒数。

poll函数的返回值:

poll发生错误时,poll函数的返回值-1,若定时器时间到之前没有任何描述字就绪,则返回0,否则返回就绪描述字的个数,即其revents成员值非0的描述字个数。

如果我们不再关心某个特定描述字,那么可以把与他对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将它的revents成员的值置为0

poll函数的通信列子:一个简单的TCP回射服务器程序

pollServer.c:使用select机制的服务器程序

#include 
#include 
#include 
#include 
#include 
#include 
/*环境为ubuntu10.04自带c环境,无法自动引入下列宏,所以自己写在前面了*/
#define INFTIM -1
#define POLLRDNORM  0x040       /* Normal data may be read.  */
#define POLLRDBAND  0x080       /* Priority data may be read.  */
#define POLLWRNORM  0x100       /* Writing now will not block.  */
#define POLLWRBAND  0x200       /* Priority data may be written.  */
#define MAXLINE  1024
#define OPEN_MAX  16 //一些系统会定义这些宏
#define SERV_PORT  10001
int main()
{
int i , maxi ,listenfd , connfd , sockfd ;
int nready;
int n;
char buf[MAXLINE];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr , servaddr;
listenfd = socket(AF_INET , SOCK_STREAM , 0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd , (struct sockaddr *) & servaddr, sizeof(servaddr));
listen(listenfd,10);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i=1;imaxi) maxi = i;
nready--;
if(nready<=0) continue;
}
for(i=1;i<=maxi;i++)
{
if(client[i].fd<0) continue;
sockfd = client[i].fd;
if(client[i].revents & (POLLRDNORM|POLLERR))
{
n = read(client[i].fd,buf,MAXLINE);
if(n<=0)
{
close(client[i].fd);
client[i].fd = -1;
}
else
{
buf[n]='\0';
printf("Socket %d said : %s\n",sockfd,buf);
write(sockfd,buf,n); //Write back to client
}
nready--;
if(nready<=0) break; //no more readable descriptors
}
}
}
return 0;
}
 


你可能感兴趣的:(Linux/UNIX网络编程)