⟅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);
// 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);
  • timeout参数说明

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

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

    该参数存在三种情形:

    情形描述
    永远等待仅在有一个或多个描述符就绪时才返回;此时需要将该参数置为空指针
    等待一段固定时间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);
    }
    
    [pi@RaspberryPI select_simple]$ ./server.out 
    Sun Jun 26 21:21:49 2022
    Sun Jun 26 21:21:59 2022
    

    // 测试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);
        }
    }
    
    [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
    
  • 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 ?/判断对应描述符位是否被设置 */
    

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

    fd_set rset;
    
    FD_ZERO(&rset);
    FD_SET(1, &rset);
    FD_SET(2, &rset);
    

    关于 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]);	// 再次访问成员
    }
    
    [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
    
  • 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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值