select() 函数允许我们在一组文件描述符上进行 I/O 多路复用。相关原型及相关操作宏定义如下:


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

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*返回:就绪的文件描述符数量,在超时的情况下返回 0,在产生错误时返回 -1 */

void FD_CLR(int fd, fd_set *set);

int FD_ISSET(int fd, fd_set *set);

void FD_SET(int fd, fd_set *set);

void FD_ZERO(fd_set *set);
/*返回:如果在 fdset 中设置 fd,则返回非零值,否则返回 0*/


select()

 函数用于在大量文件描述符上执行同步的、多路复用的 I/O 。


nfsd

 参数确定测试的文件描述符的范围,该范围为 0 到 nfds-1 。可以自己确定用于 nfds 的值,或者可以使用在 <sys/select.h> 中定义的 FD_SETSIZE 常量 (当包含 <sys/types.h> 时自动包含)。32位进程的默认 FD_SETSIZE 是 1024,但通过在包含系统提供的任何头文件之前为其定义较大的数值,可以在编译时增加该值。支持最大 FD_SETSIZE 为 65535,这是 64 位进程的默认值。不必要的检查这个较大数量的描述符会浪费 CPU 时间,因此相比于使用 FD_SETSIZE,优先选择为 nfds 提供自己的值 ---- 如果确实为nfds 提供自己的值,必须确保这个值不会超出 FD_SETSIZE .



每个 

readfds  , writefds  和  errorfds  参数指向一个文件描述符集。这些描述符集告诉 select() 函数针对每个文件描述符感兴趣的事件;readfs 是有兴趣从中读取的描述符列表,writefds 是有兴趣写入其中的描述符集,errorfds 是有兴趣从中接收一场条件的描述符列表。



任何这些指针可能都是 NULL ( 指出我们对参与该参数关联的任何事件都不感兴趣 );如果它们不是 NULL ,它们指向 fd_set 数据类型。fd_set 是不透明的数据类型,可以将其认为是位的数组:每一位对应每个文件描述符。更详细的 fd_set 类型说明见:

​http://www.groad.net/bbs/read.php?tid=1063​



操作 fd_set 的唯一方法 ( 除了声明变量或将一个 fd_set 赋予另一个 )是使用下面 4 个宏中的一个:


  • FD_ZERO     该宏清除由 fdset 指向的描述符集中的所有位。一旦已经声明了一个描述符集,必须使用这个宏清除它。
  • FD_SET         该宏启用 (设置) 由 fdset 指向的描述符集中 fd 的位。
  • FD_CLR        该宏关闭 (清除) 由 fdset 指向的描述符集中 fd 的位。
  • FD_ISSET     可以使用这个宏来确定是否设置了由 fdset 指向的描述符集中 fd 的位。

在每次调用 select() 函数前,必须重新初始化 readfds, writefds 和 errorfds 参数。


关于上面宏的解析参考:

​​​http://www.groad.net/bbs/read.php?tid=3297​​​



使用宏 FD_ZERO, FD_SET, FD_ISSET 宏的普通示例:


#include <stdio.h>
#include <string.h>
#include <sys/types.h>

int main(void)
{
fd_set read_set;
fd_set write_set;
int i;

FD_ZERO (&read_set);
FD_ZERO (&write_set);

FD_SET (0, &read_set);
FD_SET (1, &write_set);
FD_SET (2, &read_set);
FD_SET (3, &write_set);

printf ("read_set:\n");
for (i = 0; i < 4; i++) {
printf (" bit %d is %s\n", i, (FD_ISSET (i, &read_set) ? "set" : "clear"));
}
printf ("write_set:\n");
for (i = 0; i < 4; i++) {
printf (" bit %d is %s\n", i, (FD_ISSET (i, &write_set) ? "set" : "clear"));
}

return (0);
}


beyes@linux-beyes:~/C/base> ./select.exe 


read_set:


bit 0 is set


bit 1 is clear


bit 2 is set


bit 3 is clear


write_set:


bit 0 is clear


bit 1 is set


bit 2 is clear


bit 3 is set



在 select() 中,最后一个参数 timeout 可以确定:对于感兴趣的一个文件描述符上发生某件事情 select 将等待多长时间。由 timeout 指向的对象是一个 timeval 结构,该结构具有如下成员:

struct timeval  {
time_t tv_sec; /* 时间, 以秒为单位 */
suseconds_t tv_usec; /* 时间,以微妙为单位 */
};


需要考虑的条件有 3 个:



1、timeout 参数为 NULL



这将造成 select 永久等待;只有在至少一个文件描述符就绪时,或者在捕获一个信号时,select 才会返回。在后面一种情况下,select() 将返回 -1,并且设置 errno 为 EINTR 。



2、timeout->tv_sec 等于 0 并且 timeout->tv_usec 等于 0


在这种情况下,测试所有的描述符,并且 select 会立刻返回。这允许我们在 select 中轮询多个文件描述符,而不会阻塞。



3、timeout->tv_sec 不为0 或者 timeout->usec 不为0


这种情况指定给秒数和微秒数的超时值。select 函数只在超时时才会返回,除非一个描述符已经就绪,在这种情况下,它将立刻返回。如果超时到期,select 返回 0。也可能是信号中断等待。



如果 readfds、writefds 和 errofds 都为 NULL,可以使用 select 实现另一种版本的 sleep() ,该 sleep 具有微秒级的粒度。


select 函数返回如下 3 种类型值的一种:

  • 返回 -1,表示产生错误。例如,可能已经捕捉到信号,或者将要测试一个描述符没有引用有效的打开文件。
  • 返回 0,表示没有任何描述符就绪。如果由 timeout 指定的时间限制在任何描述符就绪之前到期,就会发生这种情况。
  • 正值,表示就绪描述符的数量。在这种情况下,清除描述符集,除了对于小于 nfds 的每个文件描述符,如果在调用 select 时设置它,则设置对应的位,并且针对该文件描述符,关联条件为真。

如果从描述符中的 read() 不会阻塞,则 readfds 描述符集中的描述符就绪。如果对描述符的 write 不会阻塞,则 writefds 描述符集中的描述符就绪。如果存在挂起描述符的错误条件,则 errorfds 描述符集中的描述符就绪。



关于 select 应该指出的一件事情是,select 正在监控的文件描述符是否阻塞并没有关系。如果 readfds 描述符集中的一个描述符处于非阻塞模式,并且指定 2 秒的 timeout,select 将阻塞最多 2 秒。还句话就是说,如果一个描述符处于非阻塞模式,那么当它无法得到它所期望的操作或资源时,它不会被阻塞而是直接返回,这样一来,也就永远不会发生“就绪”这种状态;所以所指定的 timeout 值也必定会超时到期;但是如果是得到了所期望的资源或者操作时,那自然就会处于了 "就绪“ 态,这样描述符集中的相应位置仍将置位。



select 将只会在发生错误到期时返回,或者在数据针对一个已选择的描述符就绪时返回。如果指定无限的等待时间 (设置 timeout 为 NULL),也会发生相同的情况。



有这样的一个错误概念存在:文件的末尾代表关于 select 的错误条件,但这并不正确。如果针对 readfds 描述符集中的文件描述符检测文件的末尾,可以将其认为是 select 可读的,随后在该描述符上对 read() 的调用将返回 0 。


#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
char buffer[128];
int result, nread;

fd_set inputs, testfds;
struct timeval timeout;

FD_ZERO (&inputs); /*每一位都初始化为 0*/
FD_SET (0, &inputs); /* 监视0描述符 */

while (1) {
testfds = inputs;
timeout.tv_sec = 2;
timeout.tv_usec = 500000; /* 2.5 秒超时等待 */

result = select(FD_SETSIZE, &testfds, (fd_set *)NULL, (fd_set *)NULL,
&timeout);

switch(result) {
case 0:
printf("timeout\\n");
break;
case -1:
perror("select");
exit(1);
default:
if (FD_ISSET(0, &testfds)) { /* 测试描述符是否就绪(标准输入是否有输入并可读) */
ioctl(0, FIONREAD, &nread);
if (nread == 0) {
printf("keyboard done\\n"); /* 标准输入无数据(按下 ctrl + d 组合键) */
exit(0);
}
nread = read(0, buffer, nread); /* 处理读取内容 */
buffer[nread] = 0;
printf("read %d from keyboard: %s", nread, buffer);
}
break;
}
}

运行输出

beyes@linux-beyes:~/C/base> ./select2.exe 


oo


read 3 from keyboard: oo


timeout


kk


read 3 from keyboard: kk


dkdkdk


read 7 from keyboard: dkdkdk


lk


read 3 from keyboard: lk


keyboard done


说明:
输入的内容包括一个回车,所以当输入 oo 或 kk 这样的两个字符时,会提示读取到了 3 个字符,这是因为,经过回车后,数据才会被真正送往终端(包括回车符自身)。假如输完字符后直接按下 ctrl + d 键中断程序运行,之前输入的字符(存储在缓冲区中,还没有真正送往标准输入)就会被强制都送到标准输入中,因此这时会先提示你输入了多少个字符,然后再显示 keyboard done 。

在程序中注意到,在循环中,每次超时候都会重新设置一下超时时间。因为 linux 会修改 timeout 指针所指向结构体的时间值(表示余下的时间),但许多版本的 UNIX 系统却不会这么做。在许多现存代码里,在使用 select() 前会初始化一下 timeval 值,然后继续使用 select(),此后不会再对 timeval 值再次初始化设置。但是在 linux 上,这样做就会引发错误,因为 linux 在每次超时发生时都会修改 timeval 值。所以,这里需要很小心对待,办法是在循环中重复对其初始化(如测试代码中所示)。注意,这两种方式(重复初始化和仅初始化一次)都是对的,它们仅是不同而已。

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

void display_time(const char *string)
{
int seconds;

seconds = time((time_t *)NULL);
printf("%s, %d\\n", string, seconds);
}

int main()
{
fd_set readfds;
struct timeval timeout;
int ret;

/*监视文件描述符0(标准输入,键盘输入)是否有数据输入*/
FD_ZERO(&readfds);
FD_SET(0, &readfds);

/*设置超时时间10秒*/
timeout.tv_sec = 10;
timeout.tv_usec = 0;

while (1) {
display_time("before select");
ret = select(1, &readfds, NULL, NULL, &timeout);
display_time("after select");

switch (ret) {
case 0:
printf("No data in ten seconds.\\n");
exit(0);
break;
case -1:
perror("select");
exit(1);
break;
default:
getchar();
printf("Data is availabel now. \\n");
}
}
return (0);
}

运行输出:

beyes@linux-beyes:~/C/base> ./test_select.exe 
before select, 1251106276
ddd
after select, 1251106283
Data is availabel now. 
before select, 1251106283
after select, 1251106286
No data in ten seconds.

说明:
由于此程序的时间初始化在 while 循环体外面,也就是说对超时时间只初始化了一次,那么不论如何,在 10s 后,程序最终都会退出。在上面的输出中,中间按下 ddd 及回车后,select 检测到标准输入读操作就绪,于是程序不再阻塞在 select() 这里,而是往下走,并调用了 display_time() 函数,然后重新循环,直到再次调用 select() 后被阻塞。这里注意的是,从输出看到第一次解除阻塞时经过的时间为 7s (1251106283),那么对于之前初始化的 10s 超时只剩余 3s 钟,所以在到达 10s 后程序最终退出。这也就是上帖所说的,linux 和许多 UNIX 在此的处理不一样,它是会随着时间的流逝不断的修改超时时间 timeou.tv_sec 和 timeout.tv_usec ,使她们表示为超时剩余时间。