UNP第六章 I/O复用:select和poll函数

来源:互联网 发布:数据科学家 编辑:程序博客网 时间:2024/05/21 10:37

概述

当TCP客户同时处理两个输入:标准输入和TCP套接字时,问题就在客户阻塞于(标准输入stdin)fgets调用期间,若将对应的服务器子进程杀死,则用户输入完毕并发送套接字后,才能收到收到服务器过早终止的提示。换言之,当在命令行执行了kill pid(服务器子进程)时,服务器TCP是会正确地给客户端程序发送一个FIN的,但是由于客户端程序现在正阻塞与标准输入读入的过程,它将看不到这个EOF,直到从套接字读时为止。

个人总结:如果没有I/O复用,一个线程同时只能等待一个fd,而一个fd可能代表的是一个客户端请求,那么为了同时等待很多fd就需要不少线程,系统需要维护这些线程,开销较大。复用(select/poll)是说你可以用少量线程同时在很多fd(用户请求上)等待,从而达到减少系统开销的目的。复用在这里可以理解为重复使用一个线程。

I/O复用:进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程。

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

  • 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用
  • 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要用到I/O复用模型
  • 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用
  • 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用

I/O模型

recvfrom系统调用函数用于从已连接套接口上接受数据报(通过套接字)。

阻塞式I/O

默认情形下,所有套接字都是阻塞的。
这里写图片描述
进程调用recvfrom函数,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的;recvfrom成功返回后,应用进程开始处理数据报。

非阻塞式I/O

非阻塞式I/O是指当所请求的I/O操作会使本进程进入睡眠状态的话,则进程不采取睡眠操作,而是返回一个错误。
这里写图片描述
轮询(polling)操作:一个应用进程对一个非阻塞描述符循环调用recvfrom。
在前三次调用recvfrom时,内核中无数据包准备好,因此内核转而返回一个EWOULDBLOCK错误,第四次调用内核中已有一个数据报准备好,则这个数据被将会从内核复制到用户空间中,继而recvfrom返回成功。

I/O复用模型

有了I/O复用,我们就可以调用select或者poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用之上。当应用进程阻塞于select系统调用之上时,它将等待数据报套接字变为可读;当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

信号驱动式I/O模型

信号驱动I/O的方法是让内核在描述符就绪时发送SIGIO信号通知我们(内核通知我们何时可以启动一个I/O操作)。
- 首先开启套接字的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数
- 当数据报准备好读取时,内核就为该进程产生一个SIGIO信号
- 调用recvfrom函数对数据报进行读取
注意,整个过程中进程是不被阻塞的,因为sigaction系统调用可以立即返回,而进程可以继续执行。

异步I/O模型

与信号驱动模型不同点在于,异步I/O模型是由内核通知我们I/O操作何时完成。
我们调用异步I/O函数aio_read,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核整个操作完成时如何通知我们。在等待I/O完成期间,进程不阻塞。

select函数

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);

timeout值有三种可能性

(1)timeout=NULL时,即是将select函数置于阻塞状态,那么除非有一个描述符准备好I/O,其余情况都将继续永远等待下去。
(2)timeout > 0时,在有一个描述符准备好时返回,等待一段固定时间,但这个固定时间将不超过timeval结构中指定的秒数和微秒数。
(3)timeout=0时,select函数根本不等待,检查描述符后立即返回,这种情况称为轮询。

select具体使用

#include "unp.h"void str_cli(FILE *fp, int sockfd) {    int maxfdp1;       fd_set rset;    char sendline[MAXLINE],recvline[MAXLINE];    FD_ZERO(&rset); //初始化描述符集    for( ; ;)    {        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)) {            if (Readline(sockfd, recvline, MAXLINE)==0)                err_quit("server terminated");            Fputs(recvline, stdout);        }        if(FD_ISSET(fileno(fp), &rset)){            if(Fgets(sendline, MAXLINE, fd)==NULL)                return;            Writen(sockfd, sendline, strlen(sendlin));        }    }}

代码讲解:

  1. maxfdp1定义了select将要等待的最大描述符个数,maxfdp1的值为最大描述符个数+1,即max(fileno(fp), sockfd) + 1;
  2. fd_set rset定义了select将要等待的描述符的集合;
  3. FD_ZERO(&rset)执行初始化描述符集合的操作;
  4. FD_SET(fileno(fp), &rset)向rset描述符集合中添加fileno(fp),这里是添加标准输入描述符;
  5. FD_SET(sockfd, &rset)向rset描述符集合中添加sockfd,这里是添加套接字描述符;
  6. Select(maxfdp1, &rset, NULL, NULL, NULL);执行select函数,设置第二个参数引用rset,让内核测读的描述符。即是说,让客户阻塞与select调用,等待数据报套接字或标准输入描述符变为可读;这里timeout参数为NULL,进程将一直阻塞,仅当上述两者其中之一准备好I/O时才返回;
  7. if(FD_ISSET(sockfd, &rset))监听数据报套接字的状态, 一旦套接字状态变为可读(数据报准备好了),就对数据报进行读操作;
  8. if(FD_ISSET(fileno(fd), &rset))监听标准输入的状态,一旦输入准备好了,就从键盘获取数据;7和8实际上是4、5、6的添加描述符和监听描述符的反馈信息获取。

停-等方式的时间线:交互式输入
这里写图片描述

在这种方式下,我们假设RTT为8个时间片,客户在时刻0发出请求,其应答在时刻4由服务器发出并在时刻7由客户端接收到。这种停-等方式的交互对管道的利用率较低,因为管道是全双工的,当被划分成8个时间片时,这种方式在每个特定时刻对管道的利用率为1/8。

填充客户与服务器之间的管道:批量输入
为了提高交互式输入下对于客户-服务器管道的利用率,我们采用将管道充满的方式,即是一个客户请求发出后继而马上发出第二个客户请求,这就可以导致时刻7管道充满,如图所示。

这里写图片描述
注意:假设输入文件只有9行,而最后一行对应的请求9在时刻8发出,写完这个请求后,客户端确实不需要再往管道写入请求了,但是并不能立即关闭连接,因为管道中还用尚未到达服务器的请求和客户端尚未收到的应答。问题在于我们对标准输入中的EOF的处理:str_cli函数就此返回main函数,但是在批量输入方式下,标准输入中的EOF并不意味着我们同事也完成了从套接字的读入:请求和应答此时均可能存在于管道之中。

shutdown函数

有了shutdown函数,我们可以把TCP状态设置为半关闭(halfclose),可以解决上述问题:给服务器发送一个FIN告诉它我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。

#include <sys/socket.h>int shutdown(int sockfd, int howto);

该函数的行为依赖于howto参数的值:
SHUT_RD:关闭连接的读这一半–套接字中不再有数据可接受,而且套接字接受缓冲区中的现有数据都被丢弃。
SHUT_WR:关闭连接的写这一半–对于TCP套接字,这称为半关闭。
SHUT_RDWR:连接的读半部和写半部都关闭–等同于第一次调用指定SHUT_RD,第二次调用指定SHUT_WR。

pselect函数

#include <sys/select.h>#include <signal.h>#include <time.h>int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec*timeout, sigset_t *sigmask);//明显比select函数多了一个sigmask

pselect相对于通常的select有两个变化

  • pselect使用timespec结构,而不使用timeval结构。timespec结构中第二个参数的单位是纳秒数而timeval中为微秒数。
  • pselect函数增加了第六个参数*sigmask:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。

poll函数

poll提供的功能与select类似。

#include <poll.h>int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

这里先介绍一下pollfd的结构

struct pollfd {    int fd;    short events;    short revent;}

其中fd用来指定测试某个给定的描述符,events设置在此描述符上监听的事件,revents返回描述符的状态。
在poll函数中,fdarray用来存放需要等待的描述符集合,也就是结构体数组指针,nfds指定结构体数组中元素的个数,timeout参数指定poll函数返回前等待多长时间。

Timeou值 说明 INFTIM 永远等待 0 立即返回,不阻塞进程 大于0 等待指定数目的毫秒数

在使用poll函数的时候,服务器端程序需要维护一个pollfd类型的结构体数组client,client数组的每一个元素是一个pollfd,其中存放着描述符的信息。首先服务器程序将client数组的第一个元素用于监听套接字,即client[0].fd = connfd,然后将其余元素的fd值设置为-1,即client[i] = -1。我们还将第一项的监听事件设置为POLLRDNORM(即普通数据可读),这样当有新的连接准备好被接受时poll将通知我们。

 client[0].fd = listenfd; client[0].events = POLLRDNORM;    for (i = 1; i < OPEN_MAX; i++)        client[i].fd = -1;

然后我们调用poll函数,以等待新的连接或者现有连接上有普通数据(因为poll规定正规TCP数据和UDP为普通数据)可读。当一个新的连接被接受后,我们在client数组中查找第一个描述符成员为-1的可用项。注意这个过程从client[1]开始,因为client[0]设置为监听套接字。找到一个可用项之后,我们把新连接的描述符保存到其中,并设置POLLRDNORM事件。

nready = Poll(client, maxi+1, INFTIM);        if (client[0].revents & POLLRDNORM) {               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;                      break;                }            if (i == OPEN_MAX)                err_quit("too many clients");            client[i].events = POLLRDNORM;            if (i > maxi)                maxi = i;                           if (--nready <= 0)                continue;                            }

select和poll区别

1.select函数使用fd_set来存放描述符的集合,采用值-参数的方式等待描述符的就绪。poll采用pollfd结构体来存放一个描述符和在描述符上监听的事件,而服务器程序通过维护一个pollfd组成的数组来存放监听套接字和已连接套接字的描述符。
2.select和poll根本的不同在于fd_set是一个位掩码,因此fd_set有固定的长度,也就是select能够同时监听的描述符有上限。然而在调用poll的时候,用户可以自定义pollfd结构体数组的大小,理论上来讲pollfd数组中能够存放的描述符是无限制的(当然数目很大时会使效率降低)。
3.select函数具有良好的兼容性,相对于select()而言,支持poll()的系统更少。

原创粉丝点击