select函数

     该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
     作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:
     •集合{1、4、5}中的任何描述符准备好读;
     •集合{2、7}中的任何描述符准备好写;
     •集合{1、4}中的任何描述符有异常条件待处理;
     •已经历了10.2秒。
     也就是说,我们调用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);
                                        返回值:若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1

     我们从该函数的最后一个参数timeout开始介绍,它告知内核等待所指定描述符中的任何一个就绪可花多长时间。其timeval结构用于指定这段时间的秒数和微妙数。
     struct timeval {
          long tv_sec;      /* seconds */
          long tv_usec;    /* microseconds */]
     };
     这个参数有以下三种可能:
     (1)永远等待下去:仅在有一个描述符准备好I/0时才返回。为此,我们把该参数设置为空指针;
     (2)等待一段固定时间:在有一个描述符准备好I/0时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微妙数;
     (3)根本不等待:检查描述符后立即返回,这称为轮询(polling)。为此,该参数必须指向一个timeval结构,而且其中的定时器值(由该结构指定的秒数和微妙数)必须为0。
     前两种情形的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数返回。
     源自Berkeley的内核绝不自动重启被中断的select,然而SVR4可以自动重启被中断的select,条件是在安装信号处理函数时指定了SA_RESTART标志。
     timeout参数的const限定词表示它在函数返回时不会被select修改。举例来说,如果我们指定一个10s的超时值,不过在定时器到时之前select就返回了(结果可能是有一个或多个描述符就绪,也可能是得到EINTR错误),那么timeout参数所指向的timeval结构不会被更新成该函数返回时剩余的秒数。如果我们需要知道这个值,那么必须在调用select之前取得系统时间,它返回后再取得系统时间,两者相减就是该值。
     有些Linux版本会修改这个timeval结构。POSIX规定对该结构使用const限定词。
     中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述符。目前支持的异常条件只有两个:
     (1)某个套接字的带外数据的到达;
     (2)某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
     如何给这三个参数中的每一个参数指定一个或多个描述符值是一个设计上的问题。select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。举例来说,假设使用32位整数,那么该数组的第一个元素对应于描述符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数据类型的描述符集,并用这些宏设置或测试该集合中的每一位,也可以用C语言中的赋值语句把它赋值成另外一个描述符集。
     我们所讨论的每个描述符占用整数数组中一位的方法仅仅是select函数的可能实现之一。不过把描述符集中的每个描述符称为位(bit)是常见的,例如“打开读集合中表示监听描述符的位”。poll函数使用一个完全不同的表示方法:一个可变长度的结构数组,其中每个结构代表一个描述符。
     举个例子,以下代码用于定义一个fd_set类型的变量,然后打开描述符1、4和5对于的位:
     fd_set      rset;
     FD_ZERO(&rset);
     FD_SET(1, &rset);
     FD_SET(4, &rset);
     FD_SET(5, &rset);
     描述符集的初始化非常重要,因为作为自动变量分配的一个描述符集如果没有初始化,那么可能发生不可预期的后果。
     select函数的中间三个参数readset、writeset和exceptset,如果我们对某一个的条件不感兴趣,就可以把它设置为空指针。事实上,如果这三个指针均为空,我们就有了一个比Unix的sleep函数更为精确的定时器(sleep睡眠以秒为最小单位)。poll函数提供类似的功能。
     maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0,1,2...一直到maxfdp1-1均将被测试。
     头文件<sys/select.h>中定义的FD_SETSIZE常值是数据类型fd_set中的描述符总数,其值通常是1024,不过很少有程序用到那么多的描述符。maxfdp1参数迫使我们计算出所关心的最大描述符并告知内核该值。以前面给出的打开描述符1、4和5的代码为例,其maxfdp1值就是6。是6而不是5的原因在于:我们指定的是描述符的个数而非最大值,而描述符是从0开始的。
     存在这个参数以及计算其值的额外负担纯粹是为了效率原因。每个fd_set都有表示大量描述符(典型数量为1024)的空间,然而一个普通进程所用的数量却少得多。内核正是通过在进程与内核之间不复制描述符集中不必要的部分,从而不测试总为0的那些位来提高效率的。
     select函数修改由指针readset、writeset和exceptset所指向的描述符集,因而这三个参数都是值-结果参数。调用该函数时,我们指定所关心的描述符的值,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1。
     使用select时最常见的两个编程错误是:忘了对最大描述符加1;忘了描述符集是值-结果参数。第二个错误导致调用select时,描述符集内我们认为是1的位却被置为0。
     该函数的返回值表示跨所有描述符集的已就绪的总位数。如果在任何描述符就绪之前定时器到时,那么返回0。返回-1表示出错(譬如本函数被一个所捕获的信号中断)。
 
描述符就绪条件
     我们一直在讨论等待某个描述符准备好I/0(读或写)或是等待其上发生一个待处理的异常条件(带外数据)。尽管可读性和可写性对于普通文件这样的描述符显而易见,然而对于引起select返回套接字“就绪”的条件我们必须讨论得更明确些。
     (1)满足下列四个条件中的任何一个时,一个套接字准备好读。
     a)该套接字接收缓冲区中的数据字节数大于等于该套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。
     b)该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
     c)该套接字是一个监听套接字且已完成的连接数不为0(已连接队列不为空)。对这样的套接字的accept通常不会阻塞。
     d)其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
     (2)下列四个条件中的任何一个满足时,一个套接字准备好写。
     a)该套接字发送缓冲区中的可用空间字节数大于等于该套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。
     b)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。
     c)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
     d)其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
     (3)如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
     注意:当某个套接字上发生错误时,它将由select标记为既可读又可写。
     接收低水位标记和发送低水位标记的目的在于:允许应用程序控制在select返回可读或可写条件之前有多少数据可读或有大空间可用于写。
     任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。
     图1汇总了上述导致select返回某个套接字就绪的条件。
     
                                        图1 select返回某个套接字就绪的条件小结
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值