UNP学习笔记之四-select和poll

对于I/O操作,有以下几种模型:

1、阻塞I/O(Blocking I/O):udp协议中的recvfrom在接受数据时进行等待就是使用此模型。

2.非阻塞I/O:recvfrom不阻塞,在数据未准备好时返回错误。

3.I/O复用:select和poll,轮询描述符是否准备好,如果准备好了就调用recvfrom获取数据。select可以同时监听多个描述符,但是受限于系统分配的描述符大小。

4.信号驱动I/O:使用sigaction注册一个信号回调函数,在数据准备好的时候回调该函数。

5.异步I/O:POSIX中进行了定义,异步I/O是在所有的数据读取操作进行完成后异步通过信号通知给程序。

 

5种不同的I/O模型的纵向对比:

 

接下来说我们的主角,select和poll,先说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);

Returns: positive count of ready descriptors, 0 on timeout, –1 on error

第一个参数表示检测的描述符中最大的个数,一般的取值是最大的描述符的值再加上1,max fd plus 1.中间3个参数分别表示需要监听的读,写,错误的描述符集合。为空则表示不感兴趣。如果3个都为空,则可以用来作为sleep函数,精度可以控制到毫秒,因为最后一个参数struct timeval可以表示到毫秒。

select的返回值>0表示准备好的描述符的个数,但是具体是哪几个描述符准备好,则需要使用FD_ISSET去判断。

以下4种情况socket准备好读取数据:

1、socket接受缓冲区的数据大小大于或者等于socket接受缓冲区定义的最小接受长度,可以使用SO_RCVLOWAT选项设置最小接受长度,tcp和udp默认的长度均为1.

2、连接的一方已经关闭连接。read操作不会阻塞且立即返回0.

3、socket是一个监听socket时,对应的完成连接数>0,这个socket对应的accept函数不会被阻塞。

4、socket产生错误时,read操作不会阻塞且返回-1.

以下4种情况socket准备好写数据:

1、socket发送缓冲区大小大于或者等于socket发送缓冲区定义的最小发送长度,并且具备以下条件:(1)socket已经连接。(2)socket不需要连接(udp)。如果我们设置socket为非阻塞,write操作不会阻塞且立即返回一个正值。使用SO_SNDLOWAT可以设置最小发送长度。默认大小为2048(udp和tcp)

2、连接的一方已经关闭。write操作会产生SIGPIPE.

3、使用非阻塞的connect函数已经完成或者connect函数失败。

4、socket产生错误。

 

close有两个限制可由函数shutdown来避免:
close将描述字的访问计数减1,仅在此计数为0时才关闭套接口
shutdown可激发TCP的正常连接终止序列, 而不管访问计数。
close终止了数据传送的两个方向:读和写。
shutdown终止的数据传送的两个方向:读和写, 或其中任一方向:读或写

定义:
int shutdown( int sockfd, int howto) ;
howto选项:
SHUT_RD 关闭连接的读一半
SHUT_WR 关闭连接的写这一半
SHUT_RDWR 关闭连接读读和写

我们来看看一个进程使用select来处理并发的情况:

#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;            //首先把监听fd加入到select中
    maxi = -1;                    //表示最大的可用接入fd的下标
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;            //接入fd的数组集合,-1表示未使用
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);  //加入监听fd到select中
    

    for ( ; ; ) {
        rset = allset;            
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);//注册select,只注册了可读集合,并且非阻塞

        if (FD_ISSET(listenfd, &rset)) {    //如果监听fd准备好,则新分配一个接入fd
            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;    //加入到接入fd数组中
                    break;
                }
                if (i == FD_SETSIZE)//如果接入fd数组不存在-1的index,则表示已经接入过多的连接
                    err_quit("too many clients");

                FD_SET(connfd, &allset);    //把最新的接入fd加入select中
                if (connfd > maxfd)
                    maxfd = connfd;            //更新max fd
                if (i > maxi)
                    maxi = i;                //更新最大的可用index

                if (--nready <= 0)
                    continue;                //如果nready只是等于1,则表明只有一个链接。
        }

        for (i = 0; i <= maxi; i++) {    //查询所有可用的接入fd,处理数据
            if ( (sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {                  
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else
                    Writen(sockfd, buf, n);

                if (--nready <= 0)
                    break;                //如果nready只是等于1,则表明只有一个链接。
            }
        }
    }
}

不过这样的程序会存在Dos攻击,链接阻塞在read,防止dos攻击的方法是使用非阻塞io,或者使用另外的进程和线程处理逻辑,设置超时等。

 

接下来我们看看poll,定义如下:

#include <poll.h>

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

poll函数的第一个参数是指向名为pollfd的结构体指针。pollfd结构体定义如下:

struct pollfd {
  int     fd;       /* descriptor to check */
  short   events;   /* events of interest on fd */
  short   revents;  /* events that occurred on fd */
};

pollfd里面,events表示等待的事件,revents表示实际发生的事件。根据结果可以做不同的处理。下表是poll的事件定义:

poll的第二个参数表示fdarray的个数,第三个参数是超时时间,单位是毫秒。

poll的例子与select基本类似,这里就不多说,具体参见unp。

最后说poll比select优秀的地方:

(1)不再有fd个数的上限限制,可以将参数ufds想象成栈低指针,nfds是栈中元素个数,该栈可以无限制增长
(2)引入pollfd结构,将fd信息、需要监控的事件、返回的事件分开保存,则poll返回后不会丢失fd信息和需要监控的事件信息,也就省略了select模型中前面的循环操作,返回后的循环仍然不可避免。另每次poll阻塞操作都会自动把上次的revents清空。

 

以下是在学习过程中找到的一些好的文章和讨论:

http://bbs3.chinaunix.net/thread-1283223-1-1.html

http://linux.chinaunix.net/techdoc/develop/2008/09/27/1034861.shtml

http://blog.csdn.net/pmunix/archive/2008/01/13/2041512.aspx

http://www.cs.uwaterloo.ca/~brecht/theses/Ostrowski-MMath.pdf

posted @ 2009-07-11 10:42  一只灰色的羊  阅读(2844)  评论(2编辑  收藏  举报