zoukankan      html  css  js  c++  java
  • Linux编程之select 【转载】

    select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

    select 机制的优势

    为什么会出现select模型?

    先看一下下面的这句代码:

    int iResult = recv(s, buffer,1024);

    这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

    再看代码:

    int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
    iResult = recv(s, buffer,1024);

    这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。

    看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。

    select模型的出现就是为了解决上述问题。
    select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。

    如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

    从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    select流程伪代码如下:

    {
        select(socket);
        while(1) 
        {
            sockets = select();
            for(socket in sockets) 
            {
                if(can_read(socket)) 
                {
                    read(socket, buffer);
                    process(buffer);
                }
            }
        }
    }

    select相关API介绍与使用

    #include <sys/select.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

    参数说明:

    maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

    readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

    timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间

    timeval结构体定义如下:

    struct timeval
    {      
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    };

    返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

    以下介绍与select函数相关的常见的几个宏:

    #include <sys/select.h>   
    int FD_ZERO(int fd, fd_set *fdset);   //一个 fd_set类型变量的所有位都设为 0
    int FD_CLR(int fd, fd_set *fdset);  //清除某个位时可以使用
    int FD_SET(int fd, fd_set *fd_set);   //设置变量的某个位置位
    int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

    select使用范例:
    当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:

    fd_set rset;   
    int fd;   
    FD_ZERO(&rset);   
    FD_SET(fd, &rset);   
    FD_SET(stdin, &rset);

    然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。

    select(fd+1, &rset, NULL, NULL,NULL);

    select返回后,用FD_ISSET测试给定位是否置位:

    if(FD_ISSET(fd, &rset)   
    { 
        ... 
        //do something  
    }

    下面是一个最简单的select的使用例子:

    #include <sys/select.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    

    int main()
    {
    fd_set rd;
    struct timeval tv;
    int err;

    FD_ZERO(&amp;rd);
    FD_SET(<span class="hljs-number">0</span>,&amp;rd);
    
    tv.tv_sec = <span class="hljs-number">5</span>;
    tv.tv_usec = <span class="hljs-number">0</span>;
    err = select(<span class="hljs-number">1</span>,&amp;rd,<span class="hljs-literal">NULL</span>,<span class="hljs-literal">NULL</span>,&amp;tv);
    
    <span class="hljs-keyword">if</span>(err == <span class="hljs-number">0</span>) <span class="hljs-comment">//超时</span>
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"select time out!
    "</span>);
    }
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(err == <span class="hljs-number">-1</span>)  <span class="hljs-comment">//失败</span>
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"fail to select!
    "</span>);
    }
    <span class="hljs-keyword">else</span>  <span class="hljs-comment">//成功</span>
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"data is available!
    "</span>);
    }
    
    
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    

    }

    我们运行该程序并且随便输入一些数据,程序就提示收到数据了。

    深入理解select模型:

    理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

    (1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。

    (2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

    (3)若再加入fd=2,fd=1,则set变为0001,0011

    (4)执行select(6,&set,0,0,0)阻塞等待

    (5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

    基于上面的讨论,可以轻松得出select模型的特点:

    (1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

    (2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

    (3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。

    用select处理带外数据

    网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。

    什么是带外数据?

    带外数据(out—of—band data),有时也称为加速数据(expedited data),
    是指连接双方中的一方发生重要事情,想要迅速地通知对方。
    这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
    带外数据设计为比普通数据有更高的优先级。
    带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

    我们写的select程序经常都是用于接收普通数据的,当我们的服务器需要同时接收普通数据和带外数据,我们如何使用select进行处理二者呢?

    下面给出一个小demo:

    #include <stdio.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <fcntl.h>
    #include <stdlib.h>
    

    int main(int argc, char* argv[])
    {
    if(argc <= 2)
    {
    printf("usage: ip address + port numbers ");
    return -1;
    }

    <span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span>* ip = argv[<span class="hljs-number">1</span>];
    <span class="hljs-keyword">int</span> port = atoi(argv[<span class="hljs-number">2</span>]);
    
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"ip: %s
    "</span>,ip);
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"port: %d
    "</span>,port);
    
    <span class="hljs-keyword">int</span> ret = <span class="hljs-number">0</span>;
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">address</span>;</span>
    bzero(&amp;address,<span class="hljs-keyword">sizeof</span>(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&amp;address.sin_addr);
    address.sin_port = htons(port);
    
    <span class="hljs-keyword">int</span> listenfd = socket(PF_INET,SOCK_STREAM,<span class="hljs-number">0</span>);
    <span class="hljs-keyword">if</span>(listenfd &lt; <span class="hljs-number">0</span>)
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Fail to create listen socket!
    "</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
    }
    
    ret = bind(listenfd,(struct sockaddr*)&amp;address,<span class="hljs-keyword">sizeof</span>(address));
    <span class="hljs-keyword">if</span>(ret == <span class="hljs-number">-1</span>)
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Fail to bind socket!
    "</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
    }
    
    ret = listen(listenfd,<span class="hljs-number">5</span>); <span class="hljs-comment">//监听队列最大排队数设置为5</span>
    <span class="hljs-keyword">if</span>(ret == <span class="hljs-number">-1</span>)
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Fail to listen socket!
    "</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
    }
    
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">client_address</span>;</span>  <span class="hljs-comment">//记录进行连接的客户端的地址</span>
    <span class="hljs-keyword">socklen_t</span> client_addrlength = <span class="hljs-keyword">sizeof</span>(client_address);
    <span class="hljs-keyword">int</span> connfd = accept(listenfd,(struct sockaddr*)&amp;client_address,&amp;client_addrlength);
    <span class="hljs-keyword">if</span>(connfd &lt; <span class="hljs-number">0</span>)
    {
        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Fail to accept!
    "</span>);
        close(listenfd);
    }
    
    <span class="hljs-keyword">char</span> buff[<span class="hljs-number">1024</span>]; <span class="hljs-comment">//数据接收缓冲区</span>
    fd_set read_fds;  <span class="hljs-comment">//读文件操作符</span>
    fd_set exception_fds; <span class="hljs-comment">//异常文件操作符</span>
    FD_ZERO(&amp;read_fds);
    FD_ZERO(&amp;exception_fds);
    
    <span class="hljs-keyword">while</span>(<span class="hljs-number">1</span>)
    {
        <span class="hljs-built_in">memset</span>(buff,<span class="hljs-number">0</span>,<span class="hljs-keyword">sizeof</span>(buff));
        <span class="hljs-comment">/*每次调用select之前都要重新在read_fds和exception_fds中设置文件描述符connfd,因为事件发生以后,文件描述符集合将被内核修改*/</span>
        FD_SET(connfd,&amp;read_fds);
        FD_SET(connfd,&amp;exception_fds);
        
        ret = select(connfd+<span class="hljs-number">1</span>,&amp;read_fds,<span class="hljs-literal">NULL</span>,&amp;exception_fds,<span class="hljs-literal">NULL</span>);
        <span class="hljs-keyword">if</span>(ret &lt; <span class="hljs-number">0</span>)
        {
            <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Fail to select!
    "</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;
        }
        
        
        <span class="hljs-keyword">if</span>(FD_ISSET(connfd, &amp;read_fds))
        {
            ret = recv(connfd,buff,<span class="hljs-keyword">sizeof</span>(buff)<span class="hljs-number">-1</span>,<span class="hljs-number">0</span>);
            <span class="hljs-keyword">if</span>(ret &lt;= <span class="hljs-number">0</span>)
            {
                <span class="hljs-keyword">break</span>;
            }
            
            <span class="hljs-built_in">printf</span>(<span class="hljs-string">"get %d bytes of normal data: %s 
    "</span>,ret,buff);
            
        }
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(FD_ISSET(connfd,&amp;exception_fds)) <span class="hljs-comment">//异常事件</span>
        {
            ret = recv(connfd,buff,<span class="hljs-keyword">sizeof</span>(buff)<span class="hljs-number">-1</span>,MSG_OOB);
            <span class="hljs-keyword">if</span>(ret &lt;= <span class="hljs-number">0</span>)
            {
                <span class="hljs-keyword">break</span>;
            }
            
            <span class="hljs-built_in">printf</span>(<span class="hljs-string">"get %d bytes of exception data: %s 
    "</span>,ret,buff);
        }
        
    }
    
    close(connfd);
    close(listenfd);
    
    
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    

    }

    用select来解决socket中的多客户问题

    上面提到过,,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户连接。现在,我们同样可以使用select来处理多客户问题,而不用fork。

    服务器端

    #include <sys/types.h> 
    #include <sys/socket.h> 
    #include <stdio.h> 
    #include <netinet/in.h> 
    #include <sys/time.h> 
    #include <sys/ioctl.h> 
    #include <unistd.h> 
    #include <stdlib.h>
    

    int main()
    {
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8888);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
    listen(server_sockfd, 5); //监听队列最多容纳5个
    FD_ZERO(&readfds);
    FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
    while(1)
    {
    char ch;
    int fd;
    int nread;
    testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
    printf("server waiting ");

        <span class="hljs-comment">/*无限期阻塞,并测试文件描述符变动 */</span>
        result = select(FD_SETSIZE, &amp;testfds, (fd_set *)<span class="hljs-number">0</span>,(fd_set *)<span class="hljs-number">0</span>, (struct timeval *) <span class="hljs-number">0</span>); <span class="hljs-comment">//FD_SETSIZE:系统默认的最大文件描述符</span>
        <span class="hljs-keyword">if</span>(result &lt; <span class="hljs-number">1</span>) 
        { 
            perror(<span class="hljs-string">"server5"</span>); 
            <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>); 
        } 
    
        <span class="hljs-comment">/*扫描所有的文件描述符*/</span>
        <span class="hljs-keyword">for</span>(fd = <span class="hljs-number">0</span>; fd &lt; FD_SETSIZE; fd++) 
        {
            <span class="hljs-comment">/*找到相关文件描述符*/</span>
            <span class="hljs-keyword">if</span>(FD_ISSET(fd,&amp;testfds)) 
            { 
              <span class="hljs-comment">/*判断是否为服务器套接字,是则表示为客户请求连接。*/</span>
                <span class="hljs-keyword">if</span>(fd == server_sockfd) 
                { 
                    client_len = <span class="hljs-keyword">sizeof</span>(client_address); 
                    client_sockfd = accept(server_sockfd, 
                    (struct sockaddr *)&amp;client_address, &amp;client_len); 
                    FD_SET(client_sockfd, &amp;readfds);<span class="hljs-comment">//将客户端socket加入到集合中</span>
                    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"adding client on fd %d
    "</span>, client_sockfd); 
                } 
                <span class="hljs-comment">/*客户端socket中有数据请求时*/</span>
                <span class="hljs-keyword">else</span> 
                { 
                    ioctl(fd, FIONREAD, &amp;nread);<span class="hljs-comment">//取得数据量交给nread</span>
                    
                    <span class="hljs-comment">/*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */</span>
                    <span class="hljs-keyword">if</span>(nread == <span class="hljs-number">0</span>) 
                    { 
                        close(fd); 
                        FD_CLR(fd, &amp;readfds); <span class="hljs-comment">//去掉关闭的fd</span>
                        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"removing client on fd %d
    "</span>, fd); 
                    } 
                    <span class="hljs-comment">/*处理客户数据请求*/</span>
                    <span class="hljs-keyword">else</span> 
                    { 
                        read(fd, &amp;ch, <span class="hljs-number">1</span>); 
                        sleep(<span class="hljs-number">5</span>); 
                        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"serving client on fd %d
    "</span>, fd); 
                        ch++; 
                        write(fd, &amp;ch, <span class="hljs-number">1</span>); 
                    } 
                } 
            } 
        } 
    } 
    
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    

    }

    客户端

    //客户端
    #include <sys/types.h> 
    #include <sys/socket.h> 
    #include <stdio.h> 
    #include <netinet/in.h> 
    #include <arpa/inet.h> 
    #include <unistd.h> 
    #include <stdlib.h>
    #include <sys/time.h>
    

    int main()
    {
    int client_sockfd;
    int len;
    struct sockaddr_in address;//服务器端网络地址结构体
    int result;
    char ch = 'A';
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888);
    len = sizeof(address);
    result = connect(client_sockfd, (struct sockaddr *)&address, len);
    if(result == -1)
    {
    perror("oops: client2");
    exit(1);
    }
    //第一次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the first time: char from server = %c ", ch);
    sleep(5);

    <span class="hljs-comment">//第二次读写</span>
    write(client_sockfd, &amp;ch, <span class="hljs-number">1</span>); 
    read(client_sockfd, &amp;ch, <span class="hljs-number">1</span>); 
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"the second time: char from server = %c
    "</span>, ch);
    
    close(client_sockfd); 
    
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; 
    

    }

    运行流程:

    客户端:启动->连接服务器->发送A->等待服务器回复->收到B->再发B给服务器->收到C->结束

    服务器:启动->select->收到A->发A+1回去->收到B->发B+1过去

    测试:我们先运行服务器,再运行客户端

    select总结:

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1、单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

    2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

    3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

  • 相关阅读:
    linux下php调试工具xdebug安装配置
    linux下php开发环境搭建(nginx+php+mysql)
    centos7使用docker部署gitlab-ce-zh应用
    CentOS7上Docker安装与卸载
    struts2 中 paramsPrepareParamsStack 拦截器
    ModelDriven & Preparable 接口
    OLW Test
    sqlserver 错误:2,错误40
    C#时间截
    http post发送
  • 原文地址:https://www.cnblogs.com/xjyxp/p/11254414.html
Copyright © 2011-2022 走看看