[文件I/O] select

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 ,使她们表示为超时剩余时间。



























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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值