阅读背景:

[置顶] 【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

来源:互联网 

本博文重要针对UNP一书中的第六章内容来聊聊I/O复用技巧和其在网络编程中的实现

1. I/O复用技巧

I/O多路复用是指内核一旦发明进程指定的一个或多个I/O条件预备就绪,它就通知该进程。I/O复用实用于以下场所:

(1) 当客户处置多个描写符(通常为交互式输入或网络套接字),必需实用I/O复用

(2) 当一个客户处置多个套接字时,这类情形很少见,但也可能涌现

(3) 当一个TCP服务器既要处置监听套接字,又要处置已衔接套接字,一般就要应用I/O复用

(4) 如果一个服务器既要实用TCP,又要实用UDP,一般就要应用I/O复用

(5) 如果一个服务器要处置多个服务或多个协定,一般就要应用I/O复用

与多线程和多进程技巧相比,I/O复用技巧的最大优势就是体系开消小,体系没必要创立进程/线程,也没必要保护这些进程/进程,从而大大减小了体系的开消。

2. I/O模型

Unix下常见的I/O模型有五种,分离是:阻塞式I/O,非阻塞式I/O,I/O复用,信号驱动式I/O和异步I/O。

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

(1) 期待数据预备好

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

例如:对一次read函数操作来讲,数据先会被拷贝到操作体系内核的缓冲区去,然后才会从操作体系内核的缓冲区拷贝到运用程序的地址空间。

再比如对一次socket传播输来讲,首先期待网络上的数据达到,然后复制到内核的某个缓冲区,然后再把内核缓冲区的数据复制到进程缓冲区。

下面就以上述两个阶段来论述五种I/O模型。

2.1 阻塞式I/O模型

2.1.1 趣解模型

假定一个特定的场景,你的一个好朋友找你借钱,你身上没有充分的现金,因而,你要去银行取钱,银行人多,你只能在那里排队,在这段时光内,你不能分开队伍去干你自己的事情。时光都糟蹋在排队上面了。这就是典范的阻塞式I/O模型。

2.1.2 网络模型

默许情形下,所有的套接字都时阻塞的,以数据报套接字为例

如上图,我们把recvfrom函数视为体系调用,进程调用recvform函数后就阻塞于此,期待数据报的达到,一直到内核把数据报预备好后,就将数据从内核复制到用户进程,随后用户进程再对这些数据进行处置。

这类模型的利益就是,能够及时取得数据,没有延迟,但是就像上面趣解模型中讲到,对用户来讲,这段时光一直要处于等到状况,不能去做其他的事情,在性状方面付出了代价。

2.2 非阻塞式I/O模型

2.2.1 趣解模型

还是去取钱的例子,假定你没法忍耐一直在那里排队,而是去旁边的商场走走,然后隔一段时光回来看看还有在排队没,有的话再持续去走走,直到有一次你回来看到没有人排队了为止。这就是非阻塞式I/O模型。

2.2.2 网络模型

进程把一个套接字设置成非阻塞是在通知内核:当所要求的I/O操作非得把本进程投入眠眠能力完成时,不要把本进程投入眠眠,而是返回一个毛病。

如上图所示,前三次讯问都返回一个毛病,即内核没有数据报预备好,到第四次调用recvform函数时,数据被预备好了,它被复制到运用进程缓存区,因而recvform胜利返回,运用进程随后处置数据。

这类模型相对阻塞式来讲,

长处在于:运用进程没必要阻塞在recvfrom调用中,而是可以去处置其他事情

缺陷在于:如趣解模型中所说,你来回跑银行带来了很大的延时,可能在你来回的路上叫到了你的号。在网络模型中便可以表示在义务完成的响应延迟增大了,隔一段时光轮询一次recvform,数据报可能在两次轮询之间的任意时光内预备好,这将会致使整体数据吞吐量的下降。

2.3 I/O复用模型

2.3.1 趣解模型

现在,银行都会按一个显示屏,上面会显示轮到几号客户了。这个时候,你就不用每次都去跑进去看还有排队没,而是远远的看看显示屏上轮到你没有,如果显示了你的名字,你就去取钱就好了。这就是I/O复用模型。

2.3.2 网络模型

有了I/O复用技巧,我们可以调用select或poll函数,阻塞在这两个体系调用中的某一个之上,而不是阻塞在真实的I/O体系调用上。

如上图所示,进程受阻于select调用,期待可能多个套接字中的任一个变成可读。当select返回套接字可读这一条件时,运用进程就调用recvfrom把所读的数据报复制到运用进程缓冲区。

进程阻塞在select,如果进程还有其他的义务的话便可以体现到I/O复用技巧的利益,那个义务先返回可读条件,就去履行哪一个义务。从单一的期待变成多个义务的同时代待。

这类模型较之前的模型来讲,可以没必要屡次轮询内核,而是等到内核的通知。

2.4 信号驱动式I/O模型

2.4.1 趣解模型

你还是不满意银行的服务,虽然没必要排队,但你在商场逛的也不放心啊,你还是要盯着显示屏,深怕没有看到显示屏上面你的名字,因而,银行也退出了全新的服务,你去银行取钱的时候,银行目前人多不能及时处置你的业务,而是叫你留下手机号,等到空闲的时候就短信通知你可以去取钱了。这就是信号驱动式I/O模型。

2.4.2 网络模型

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

如上图所示,进程树立SIGIO的信号处置程序(就要趣解模型中的留下手机号),并通过sigaction体系调用安装一个信号处置函数,该体系调用将立即返回,进程持续工作,知道数据报预备好后,内核发生一个SIGIO信号,告知运用进程和预备好,因而就在信号处置程序中调用recvfrom读取数据报,并通知主重复数据已预备好待处置,也能够立即通知主重复让他读取数据报。

这类模型的利益就是,在数据报没有预备好的期间,运用进程没必要阻塞,持续履行主重复,只要期待来自负号处置函数的通知便可。

2.5 异步I/O模型

2.5.1 趣解模型

你细细的想了想自己取钱时为了甚么,不过时借给你的朋友,银行都退出了网上银行服务,你只须要知道你的好朋友的银行卡号,然后在网银中申请转账,银行后台会给你处置,然后把钱打到你朋友的账户下面,等这些都处置好后,银行会给你发一条短信,告知你转账胜利,这个时候你便可以够跟你的好朋友说,钱已打给你了。这就是异步I/O模型,取钱借钱的繁杂事就交给银行后台给你处置吧。

2.5.2 网络模型

POSIX规范中供给一些函数,这些函数的工作机制是:告知内核启动某个操作,并让内核在全部操作完成后通知我们。

如上图所示,我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描写符,缓冲区指针,缓冲区大小和文件偏移,并告知内核完玉成部操作后通知我们。

不同于信号驱动式I/O模型,信号是在数据已复制到进程缓冲区才发生的。

2.6 各种I/O模型的比拟

以一张图来解释五种I/O操作的差别:

同步I/O操作:致使要求进程阻塞,直到I/O操作完成

异步I/O操作:不致使进程阻塞

可知,前四种都属于同步I/O操作慢体系都会阻塞与recvfrom操作,而异步I/O不会。

3. select函数

select函数用于I/O复用,该函数许可进程指导内核期待多个事件中的任何一个发生,并只在有一个或多个事件发生或阅历一段指定的事件才唤醒它。

3.1 函数原型

它的函数原型时:


int select(int maxfdp1, fd_set *readset, fd_set *writeset , fd_set *exceptset , const struct timeval *timeout);

对timeout参数:

(1) timeout==NULL,表示要永久期待下去,直到有一个描写符预备好I/O时才返回

(2) *timeout的值为0,表示不期待,检讨描写符就立即返回,这称为轮询。

(2) *timeout的值不为0,表示期待一段固定的时光,再有一个描写符预备好I/O时返回,但是不能超过由该参数制订的时光。

对readset,writeset和exceptset三个参数:

这三个描写符解释了可读,可写和处于异常条件的描写符聚集

对描写集fd_set构造,供给了以下四个操作函数


#include <sys/select.h>

int FD_ISSET(int fd,fd_set *fdset); //设定描写集中的某个描写符

void FD_CLR(int fd,fd_set *fdset);//关掉描写集中的某个描写符

void FD_SET(int fd,fd_set *fdset);//打开描写集中的某个描写符

void FD_ZERO(fd_set *fdset);//消除聚集内所有元素

对maxfdp1参数:

指定待测试的描写符个数,它的值时待测试的最大描写符编号加1,即从上面三个描写符集中的最大描写符编号加1。

对返回值:

select返回值有三种情形:

(1) 返回值为-1时,表示出错,如果在指定的描写符一个都没有预备好时捕捉一个信号,则返回-1

(2) 返回0,表示没有描写符预备好,指定的时光就超过了。

(3) 返回正数,表示已预备好的描写符个数,在这类情形下,三个描写符集中照旧打开的位对应于已预备好的描写符

3.2 应用select函数修正的str_cli函数

#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);//socket描写符
        maxfdp1 = max(fileno(fp), sockfd) + 1;//最大描写符编号+1
        Select(maxfdp1, &rset, NULL, NULL, NULL);//调用select,阻塞于此
  //如果返回的套接字可读,就用readline读入回射文本
        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */
            if (Readline(sockfd, recvline, MAXLINE) == 0)
                err_quit("str_cli: server terminated prematurely");
            Fputs(recvline, stdout);
        }
  //如果尺度输入可读,就先用fgets读入一行文本
        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
            if (Fgets(sendline, MAXLINE, fp) == NULL)
                return;     /* all done */
            Writen(sockfd, sendline, strlen(sendline));
        }
    }
}

3.3 批量输入

在上一节提到的str_cli版本中,依然存在一个问题。假定客户在尺度输入中批量输入数据,在输入完最后一个数据后,碰到了EOF,str_cli返回到main函数,main函数随后终止。但是,在这个进程中,尺度输入的EOF终止符其实不意味着我们也同时完成了从套接字的读入,可能仍有要求在去往服务器的路上,或仍有应对在返回客户的路上。

缘由就处在于此:

    if (Fgets(sendline, MAXLINE, fp) == NULL)
                return;     /* all done */

当碰到EOF终止符的时候,str_cli函数选择了立即返回,而此时,我们更须要的是找到一个条件来断定套接字的读取是不是完成。

3.4 shutdown函数

shutdown函数供给了关闭TCP衔接其中一半的办法,也正是为懂得决上一小节发明的问题。

假定在尺度输入碰到EOF终止符时,我们只关闭发送这一端,也就是给服务器发送一个FIN,告知它我们已完成了数据发送,但是依然坚持套接字描写打开以便读取。

这点跟close函数有点像,但是斟酌到close函数有以下两个限制:

(1) close把描写符的援用计数减1,仅在该计数变成0时才关闭该套接字。但是应用shutdown可以不管援用计数就激起TCP的正常衔接终止序列

(2) close终止读和写两个方向的数据传送。shutdown只是关闭单方向的读或写。

其函数原型以下:

int shutdown(int sockfd , int howto);//若胜利则返回0,若出错返回-1

关于该函数的第二个参数howto:

(1) SHUT_RD 关闭衔接的读这一半,套接字中不再有数据可吸收,而且套接字吸收缓冲区中的现有数据都被抛弃

(2) SHUT_WR 关闭衔接的写这一半,对TCP套接字来讲,这称为半关闭,当前留在套接字发送缓冲区的数据将被发送,后跟TCP正常的衔接终止序列。

(3) SHUT_RDWR 关闭读半部和写半部,这与调用shutdown两次等效。

3.5 str_cli函数再修正版

#include    "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
    int         maxfdp1, stdineof;
    fd_set      rset;
    char        buf[MAXLINE];
    int     n;

    stdineof = 0;
    FD_ZERO(&rset);
    for ( ; ; ) {
        if (stdineof == 0)//套接字读取完成标识符
            FD_SET(fileno(fp), &rset);//关闭select描写符集中的尺度输入描写符
        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 ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                if (stdineof == 1)
                    return;     /* normal termination */
                else
                    err_quit("str_cli: server terminated prematurely");
            }

            Write(fileno(stdout), buf, n);
        }

        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
            if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {//若读取的字节数为0
                stdineof = 1;//表明套接字读取数据完成
                Shutdown(sockfd, SHUT_WR);  /* send FIN *///关闭读这一半
                FD_CLR(fileno(fp), &rset);
                continue;
            }

            Writen(sockfd, buf, n);
        }
    }
}

4. TCP回射服务器程序(采取select函数)

【unix网络编程第三版】浏览笔记(四):TCP客户/服务器实例中我们采取fork生成子进程来处置每一个客户的需求。

如今,有了select函数,就没必要创立那末多子进程了,避免了为每一个客户创立一个子进程的所有开消,本节就将其改写成任意个客户的单进程版本。

select函数的描写符集中须要存储每一个客户的衔接套接字。因而我们很容易想到用采取一个数组client[FD_SETSIZE]来保留所有已衔接的套接字。

每次有新客户衔接的时候,就在client数组中找到第一个可用项来保留该衔接套接字。

具体解释见代码注释:

#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;       //初始化maxfd,在传入select函数时须要+1
    maxi = -1;                  //记载client数组中最后一个非-1数所占的序号
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;     //初始化client数组,为-1表示该项可用
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);


    for ( ; ; ) {
        rset = allset;  //初始化描写符集
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);//注意此处为最大描写符编号+1,返回已预备好的描写符个数

        if (FD_ISSET(listenfd, &rset)) {    //检测到有新客户衔接
            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; //存储套接字描写符
                    break;
                }
            if (i == FD_SETSIZE)//限制最大衔接个数
                err_quit("too many clients");

            FD_SET(connfd, &allset);    /* add new descriptor to set */
            if (connfd > maxfd)
                maxfd = connfd;     //重置maxfd为最大描写符编号+1
            if (i > maxi)
                maxi = i;               //client数组中最后一个描写符所占的序号

            if (--nready <= 0)
                continue;               //没有已衔接套接字了
        }

        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) {
                        /*connection closed by client */
                    Close(sockfd);//直接关闭套接字
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else
                    Writen(sockfd, buf, n);

                if (--nready <= 0)
                    break;              //没有已衔接套接字了
            }
        }
    }
}

5. pselect函数

pselect函数由POSIX创造,是select的变种。

#include <sys/select.h> 
int pselect(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,const struct timespec *restrict tsptr,const sigset_t *restrict sigmask);

相对select函数,pselect函数有以下几点不同:

(1) pselect应用timespec构造,新构造的tv_nsec指定纳秒数,而原构造里的tv_usec指定奥妙级

(2) pselect增长了第六个参数:一个指向信号掩码的指针。该参数许可程序先制止递交某些信号,再测试由这些当前被制止的信号处置函数设置的全局变量,然后调用pselect,告知它重新设置信号掩码。

(3)pselect的超时值设为了const,保证了调用pselect不会修正此值。

6. poll函数

poll函数的功效与select类似,不过在处置流装备时,它能够供给额外的信息。

#include <poll.h> 
int poll(struct pollfd *fdarray,nfds_t nfds,int timeout);//若有就绪描写符就返回其数量,如超时则返回0,若出错就返回-1

对第一个参数:为指向一个构造数组第一个元素的指针,每一个数组元素都是一个pollfd构造,用于指定测试某个给定描写符fd的条件。

struct pollfd {
        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */
};

要测试的条件由events成员指定,函数在相应的revents成员中返回该描写符的状况。

这里每一个描写符都有两个变量,一个为调用值,一个为返回成果,避免了应用值成果参数。

该构造中events和revents成员所用的常值以下表:

该表中,前四个处置输入,中间三个处置输出,最后三个处置异常。

就TCP/UDP而言,以下几种情形引发poll返回特定的revent

(1) 所有正规TCP数据和所有UDP数据都被以为时普通数据

(2) TCP的带外数据被以为时优先级带数据

(3) 当TCP衔接的读半部关闭时,也被以为时普通数据,随后的读操作将返回0

(4) TCP衔接存在毛病既可以为是普通数据,也能够为时毛病,不管哪类情形,随后的读操作都会返回-1,并把errno设为适合的值

(5) 在监听套接字上有新的衔接可用既可以为时普通数据,也能够为时优先级数据。

(6) 非阻塞式connect的完成被以为是使相应套接字可写

对第二个参数nfds:表示构造数组中元素的个数

对第三个参数timeout:指定poll函数返回前期待多长时光。

timeout值 解释
INFINT 永久期待
0 立即返回,不阻塞进程
大于0 期待指定数量的毫秒数

在select函数中,FD_SETSIZE和每一个描写符集中最大描写符数量这些都触及到固定值。但是在poll函数中分配一个pollfd数组并把该数组中元素的数据通知内核成了调用者的义务,内核不再须要知道这些固定大小的数据类型。

7. TCP回射服务器再修正版

#include    "unp.h"
#include    <limits.h>      /* for OPEN_MAX */

int
main(int argc, char **argv)
{
    int                 i, maxi, listenfd, connfd, sockfd;
    int                 nready;
    ssize_t             n;
    char                buf[MAXLINE];
    socklen_t           clilen;
    struct pollfd       client[OPEN_MAX];
    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);

    client[0].fd = listenfd;
    client[0].events = POLLRDNORM;
    for (i = 1; i < OPEN_MAX; i++)//由调用者指定OPEN_MAX
        client[i].fd = -1;      //初始化为-1,表示可用
    maxi = 0;               //client数组中已用项的最大序号
    for ( ; ; ) {
        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++)//与select不同,这里的最大值均由调用者指定
                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;           //没有已衔接套接字了
        }

        for (i = 1; i <= maxi; i++) {   //检讨client数组中所有项
            if ( (sockfd = client[i].fd) < 0)
                continue;
            //有些实现在一个衔接上吸收到RST时返回的时POLLERR事件,而其他实现返回的只是POLLRDNORM事件
            if (client[i].revents & (POLLRDNORM | POLLERR)) {//查看返回的revents状况
                if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                            //由用户来关闭该套接字
#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) {
                        //由用户来关闭该套接字
#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;              //没有已衔接套接字了
            }
        }
    }
}

分享到: