• ⟅UNIX网络编程⟆⦔select函数的定义及参数


    说在前面

    • 环境:windows10
    • 参考: UNIX网络编程、linux manual page
    • 目录:这里
    • 测试平台:Manjaro-ARM-xfce-rpi4-20.02
    • 测试用例代码:这里
    • 吐槽:爷青回

    基本说明

    • 在上一节中我们对几种I/O模型进行的基本的了解,为了实现这些I/O模型,通常会用到一些函数或方法。select为其中一种。
    • select函数允许进程 (process) 指示 (instruct) 内核等待多个事件中的任何一个发生,并在一个或多个事件发生或经历一段指定的时间后才唤醒它 (process)
    • 举个栗子,可以使用select函数通知内核在以下事件发生时返回(或者说唤醒)进程
      • 集合{1, 4, 5}中的任何描述符 就绪;
      • 集合{2, 7}中的任何描述符 就绪;
      • 集合{1, 4}中的任何描述符出现 异常;
      • 时间过去了11.4秒
    • 也就是说,select可以将我们关注的描述符或者等待时长告知内核,这里的描述符不限于套接字,任何的文件描述符 (file descriptor) 都可以。

    定义

    // linux manual page
    #include <sys/select.h>
    
    int select(int nfds, fd_set *restrict readfds,
    	fd_set *restrict writefds, fd_set *restrict errorfds,
        struct timeval *restrict timeout);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // unix network programming
    #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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • timeout参数说明

      该参数描述内核等待给定的描述符中任意一个就绪的最长时间。timeval结构用于描述时长的秒数以及微秒数:

      struct timeval {
       long tv_sec; /* seconds/秒 */
       long tv_usec; /* microseconds/微秒 */
      };
      
      • 1
      • 2
      • 3
      • 4

      该参数存在三种情形:

      情形描述
      永远等待仅在有一个或多个描述符就绪时才返回;此时需要将该参数置为空指针
      等待一段固定时间timeval结构描述的时间范围内,如果有描述符就绪就返回
      不等待检查描述符后立即返回,即轮询(polling);此时需要将timeval结构描述的时长设置为0(即tv_sectv_usec为0)

      关于时间精准度:尽管timeval结构描述的最小单位是微秒(1ms=1000us),但是实际上内核所支持的单位是没有这么精准的,有些Unix内核会将超时时长向上取整为10ms的倍数。并且在到达定时器时间后,由于内核还需要消耗一定的时间进行进程调度,这个误差也会进一步扩大。


      关于时间值的最值:在有些系统中,如果timeval结构体中tv_sec超过一定大小(可能是100 million sec,1亿),select函数会返回EINVAL错误。也就是说,timeval结构可以描述select函数不支持的时间长度。


      关于const限定词:由于该参数是指针,若不加限定,那么在函数内部,该参数的值是可能会被修改的。添加const限定表示在select函数不会修改这个参数。举个栗子:如果timeout描述的是10s,但是在10s内函数已经返回,那么timeout参数在函数执行后还是10s,而不会返回剩余的秒数、或是消耗的秒数。如果需要知道剩余的时间或者消耗的时间,需要在调用前后记录时间点,并进行计算。
      有些Linux版本会修改该参数(例如本文引用的linux manual page),所以从移植性角度考虑,应假设该参数在调用select前未被定义,因此需要在每次调用前对其进行初始化。

    • timeout参数举例

      // 简单测试
      void TestTimeout() 
      {
          struct timeval t;
      
          t.tv_sec = 10; // 改成1000000000 在该平台并不会返回错误
          t.tv_usec = 0;
      
          select(0, NULL, NULL, NULL, &t);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      [pi@RaspberryPI select_simple]$ ./server.out 
      Sun Jun 26 21:21:49 2022
      Sun Jun 26 21:21:59 2022
      
      • 1
      • 2
      • 3

      // 测试timeval是否被修改
      void TestFDTimeout()
      {
          struct timeval t;
      
          t.tv_sec = 10;
          t.tv_usec = 0;
      
          fd_set fset;
      
          FD_ZERO(&fset);													// 清空
          FD_SET(fileno(stdin), &fset);									// 设置
      
          int val;
          val = select(fileno(stdin)+1, &fset, NULL, NULL, &t); 			// 检测标准输入
      
          if (FD_ISSET(fileno(stdin), &fset)) { 							// 有输入时的处理
              char in[30];
              Fgets(in, 30, stdin); 										// 取出输入
      
              char str[30];
              sprintf(str, "sec:%d usec:%d\n", t.tv_sec, t.tv_usec); 		// 打印时间 该系统下改变了timeval,为剩余时间
              Fputs(str, stdout);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      [pi@RaspberryPI select_simple]$ ./server.out 
      Sun Jun 26 22:01:51 2022
      a
      sec:5 usec:963559
      Sun Jun 26 22:01:55 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • readsetwritesetexceptset参数说明

      这三个参数用于描述内核进行检测的描述符集合,分别为读、写以及异常条件(套接字外带数据 (out-of-band data) 的到达 以及另一种异常(说是本书不讨论))。


      如何表述一个或多个描述符是一个设计问题,select函数使用描述符集(descriptor sets)来解决这个问题。描述符集通常一个整型数组,每个整数中的每一位对应一个描述符。举个栗子:如果使用32位bit的整数(uint32)数组,那么数组的一个元素对应描述符0~31,第二个元素对应32~63。这些实现细节定义在数据类型fd_set以及几个宏定义中:

      void FD_ZERO(fd_set *fdset); /* clear all bits in fdset/清除所有描述符位 */
      void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset/设置对应的描述符位 */
      void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset/清除对应的描述符位 */
      int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ?/判断对应描述符位是否被设置 */
      
      • 1
      • 2
      • 3
      • 4

      我们可以定义一个fd_set类型的变量,并使用这些宏来操作它,也可以使用赋值语句将其赋值给另一个变量;举个栗子:

      fd_set rset;
      
      FD_ZERO(&rset);
      FD_SET(1, &rset);
      FD_SET(2, &rset);
      
      • 1
      • 2
      • 3
      • 4
      • 5

      关于 fd_set的初始化:描述符集的初始化非常重要,由于我们定义的是一个自动变量,在未初始化的情况下,它的值是随机的,这将产生不可预知的后果。


      如果不关注这三个参数中的某些参数,我们可以直接将其设置为空指针。当三个参数均为空指针时,我们就得到了一个比Unix的Sleep函数更精准的定时器(poll函数也有类似的功能)。

    • readsetwritesetexceptset参数举例

      void TestFD() 
      {
          fd_set fset;
      
          printf("fdset[0] is %d.\n", fset.__fds_bits[0]);				// 访问成员
      
          FD_SET(3, &fset);												// 设置描述符3
      
          printf("fdset[0] is %d, after set 3.\n", fset.__fds_bits[0]);	// 再次访问成员
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      [pi@RaspberryPI select_simple]$ ./server.out 
      Sun Jun 26 22:16:29 2022
      fdset[0] is 0.
      fdset[0] is 8, after set 3.
      Sun Jun 26 22:16:29 2022
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • maxfdp1参数说明

      该参数用于描述待测试的描述符个数,其值是待测试的最大描述符的值加上1,即0,1,2,…,maxfdp1-1将被检测。举个例子,假设我们关注的描述符的值是{1, 2, 24},那么maxfdp1的值需要被置为25。


      头文件<sys/select.h>中定义的FD_SETSIZE常数即fd_set数据类型中的描述符总数,通常是1024,不过很少有程序用到这么大的值 (这个说法现在可能有点过时,但是对于使用select的程序来说可能确实用不到这么多) 。该参数的存在迫使使用者计算其所关注的最大描述符值并通知内核。

      这个参数的意义在于提高内核效率。每个fd_set都有表示大量描述符的空间,但是一个进程使用到的却很少;内核可以通过该参数在进程和内核之间复制必要的部分,减少对那些总为0的数据的操作,进而提高效率。

    • 返回值

      select函数会修改指针 readsetwritesetexceptset所指向的描述符集,因而这三个参数都是值-结果参数 (value-result arguments)调用函数时,传入我们关心的描述符,函数返回时,结果将指示哪些描述符已经就绪。通常我们可以使用FD_ISSET来测试哪些描述符是就绪的。描述符集中其他任何未就绪的描述符都将置为0,因此,在每次重新调用函数前,都需要将参数置为我们关注的描述符集。

      注意事项:使用select函数的常见错误,maxfdp1参数未+1;忽略了描述符集是值-结果参数(即没有重置readsetwritesetexceptset为我们关注的描述符集,而使用函数返回后的值,导致函数忽略了原本那些参数)。


      select函数的返回值表示所有描述符集( readsetwritesetexceptset)中已就绪的的描述符总数。如果在任何描述符就绪前超时,那么返回0;如果出错,返回-1。

  • 相关阅读:
    【C++】STL08关联容器-map
    spark入门学习-3-SparkSQL数据抽象
    安装clang
    开源数据集分享———猫脸码客
    Docker中安装Redis
    java程序员必会-远程debug
    sylar高性能服务器-配置(P9)代码解析+调试分析
    数据库课后习题加真题
    鸿蒙HarmonyOS实战-Stage模型(ExtensionAbility组件)
    vue 块级加载效果
  • 原文地址:https://blog.csdn.net/qq_33446100/article/details/109522113