zoukankan      html  css  js  c++  java
  • Linux NIO 系列(04-1) select

    Linux NIO 系列(04-1) select

    Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

    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 模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。

    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 介绍与使用

    2.1 select

    #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 的整数,这个整数表示就绪描述符的数目。

    2.2 fd_set 集合操作

    以下介绍与 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);    // 测试某个位是否被置位
    

    2.3 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 模型:

    理解 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 本质上是通过设置或者检查存放 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 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    当然 select 也有优点:兼容性好,不管是 Linux 还是 Windows 都支持 select。

    附1:select 网络编程代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #define SERVER_PORT 8888
    #define OPEN_MAX  3000
    #define BACKLOG   10
    #define BUF_SIZE  1024
    
    void main() {
        int i, j, maxi;
        int listenfd, connfd, sockfd; // 定义套接字描述符
        int nready;     // 接受 pool 返回值
        int recvbytes;  // 接受 recv 返回值
    
        char recv_buf[BUF_SIZE];        // 发送缓冲区
        fd_set readSet, totalSet;       // 定义读集合,备份集合
    
        // 定义 IPV4 套接口地址结构
        struct sockaddr_in seraddr;     // service 地址
        struct sockaddr_in cliaddr;     // client 地址
        int cliaddr_len;
    
        // 初始化IPV4套接口地址结构
        seraddr.sin_family = AF_INET;   // 指定该地址家族
        seraddr.sin_port = htons(SERVER_PORT);    // 端口
        seraddr.sin_addr.s_addr = INADDR_ANY;   // IPV4的地址
        bzero(&(seraddr.sin_zero), 8);
    
        // 启动 server
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        bind(listenfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr));
        listen(listenfd, BACKLOG);
    
        // select 模型处理过程  
        // 1. 初始化套接字集合,添加监听 socket 到这个集合
        FD_ZERO(&totalSet);
        FD_SET(listenfd, &totalSet);
        maxi = listenfd;
    
        while(1) {
            // 2. 将集合的一个拷贝传递给 select 函数。当有事件发生时,select 移除未决的 socket 然后返回。
            //    也就是说 select 返回时,集合 readSet 中就是发生事件的 readSet
            readSet = totalSet;
            int nready = select(maxi + 1, &readSet, NULL, NULL, NULL);
            if (nready > 0) {
                if (FD_ISSET(listenfd, &readSet)) {
                    cliaddr_len = sizeof(cliaddr);
                    connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
                    printf("client IP: %s	 PORT : %d
    ", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
    
                    FD_SET(connfd, &totalSet);
                    maxi = connfd;
                    if (--nready == 0) {
                        continue;
                    }
                }
    
                for (i = listenfd + 1; i <= maxi; i++) {
                    sockfd = i;
                    if (FD_ISSET(sockfd, &readSet)) {
                        recvbytes = read(sockfd, recv_buf, sizeof(recv_buf));
                        if (recvbytes == 0) {           // 客户端关闭
                            close(sockfd);
                            FD_CLR(sockfd, &totalSet);
                        } else if (recvbytes == -1) {   // read 异常
                            perror("read error");
                            exit(1);
                        } else {                        // 正常读取数据
                            write(sockfd, recv_buf, recvbytes);
                            printf("receive %s
    ", recv_buf);
                        }
                    }
                }
            }
        }
    }
    

    参考:

    1. Linux编程之select

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    面向对象的设计模式2
    数据结构
    算法题目1
    5.7(1) 反射
    hashMap原理(java8)
    6.1 接口
    18.1 线程
    13.2 具体的集合
    scrapy(2)——scrapy爬取新浪微博(单机版)
    5.1 类、超类和子类
  • 原文地址:https://www.cnblogs.com/binarylei/p/11124148.html
Copyright © 2011-2022 走看看