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*/
#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); }
在 select() 中,最后一个参数 timeout 可以确定:对于感兴趣的一个文件描述符上发生某件事情 select 将等待多长时间。由 timeout 指向的对象是一个 timeval 结构,该结构具有如下成员:
struct timeval { time_t tv_sec; /* 时间, 以秒为单位 */ suseconds_t tv_usec; /* 时间,以微妙为单位 */ };
#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; } }运行输出
说明:
输入的内容包括一个回车,所以当输入 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 ,使她们表示为超时剩余时间。