- 环境:windows10
- 参考: UNIX网络编程、linux manual page
- 目录:这里
- 测试平台:Manjaro-ARM-xfce-rpi4-20.02
- 测试用例代码:这里
- 吐槽:爷青回
select
为其中一种。select
函数允许进程 (process) 指示 (instruct) 内核等待多个事件中的任何一个发生,并在一个或多个事件发生或经历一段指定的时间后才唤醒它 (process) 。select
函数通知内核在以下事件发生时返回(或者说唤醒)进程:
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_sec 和tv_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
readset
、writeset
、exceptset
参数说明这三个参数用于描述内核进行检测的描述符集合,分别为读、写以及异常条件(套接字外带数据 (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
函数也有类似的功能)。
readset
、writeset
、exceptset
参数举例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
函数会修改指针 readset
、writeset
、exceptset
所指向的描述符集,因而这三个参数都是值-结果参数 (value-result arguments)。调用函数时,传入我们关心的描述符,函数返回时,结果将指示哪些描述符已经就绪。通常我们可以使用FD_ISSET
来测试哪些描述符是就绪的。描述符集中其他任何未就绪的描述符都将置为0,因此,在每次重新调用函数前,都需要将参数置为我们关注的描述符集。
注意事项:使用select
函数的常见错误,maxfdp1
参数未+1;忽略了描述符集是值-结果参数(即没有重置readset
、writeset
、exceptset
为我们关注的描述符集,而使用函数返回后的值,导致函数忽略了原本那些参数)。
select
函数的返回值表示所有描述符集( readset
、writeset
、exceptset
)中已就绪的的描述符总数。如果在任何描述符就绪前超时,那么返回0;如果出错,返回-1。