「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」
如果你恰好正在学习NIO或者阅读过epoll相关的文章, select
这一个系统调用免不了被提出来和epoll
对比一番, 而且select
最后多少避免不了被提出来diss一顿。此外如果你的简历上写着熟悉网络编程之类的,我想你大概率会被问到select
和epoll
这两个系统调用之间有什么区别, 因此虽然不用这玩意,但还是搞懂的。本文将首先对select
中最重要的数据结构fd_set
进行分析, 之后会更新select
实践的文章, 欢迎关注。
相关文章推荐:
select
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict timeout);
select的系统调用的函数签名如上所示, 其中我们需要传入以下参数
nfds
当前监控的最大文件描述符的值 + 1 (文件描述符从0开始递增)readfds
要监控读事件的文件描述符集合writefds
要监控写事件的文件描述符集合expectfds
要监控有异常事件发生的文件描述符集合timeout
超时等待时间, 如果超过这个时间还没有监控到事件就立即返回, 如果为NULL将会一直阻塞等待
fd_set
根据上文我们可以发现, 无论是监控读事件还是写事件或者是异常事件,都需要我们传入一个类型为fd_set
的结构体,根据其名字可以很容易看出这是一个可以保存多个文件描述符的数据结构。在select.h
这一头文件中定义了四个常用的"函数"(宏)用于操作fd_set
, 分别是:
FD_ZERO(fd_set)
清空目标fd_set中的数据FD_SET(fd, fd_set)
往目标fd_set中添加文件描述符FD_CLR(fd, fd_set)
删除目标fd_set中的指定文件描述符FD_ISSET(fd, fd_set)
检查文件描述符是否在fd_set中
那么fd_set
真的如其名底层的数据是一个基于哈希表实现的数据结构吗?
在之前的文章(文件描述符与位图)中我们提到过以下事实:
- 文件描述符从0开始递增并且连续
- 位图具有很高的存储效率
- 调用
accept
方法返回一个代表客户端连接的文件描述符
基于以上事实, 也就不难理解fd_set底层为啥要采用位图实现了。
fd_set
结构体的定义如下所示, 该结构体只包含一个成员__fds_bits
, 是一个类型为__fd_mask
的数组, 用于实现位图。
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
文件位于 /usr/include/x86_64-linux-gnu/sys/select.h
如果位图中的第N位被设置成了1,则表示该文件描述符被添加到了fd_set
中。
举个列子,我们有一个大小为2个字节(16位)的位图, 如下所示。
其中第5, 7, 9, 13, 14位的数值均为1则表示文件描述符5, 7, 9, 13, 14被添加到了集合中。
除此之外,我们还需要确定一位图的底层要采用哪种数据类型来进行表示, 虽然基本思路是一致的,但是对于同一位的数据, 不同的数据类型就需要定位到数组种不同位置的元素上。
对于上述中的位图, 如果需要定位文件描述符11是否在位图中:
- 采用
char
数组来保存位图, 需要与第一个元素进行位运算bit_map[1] & (1 << 3)
- 采用
int
数组来保存位图, 需要与第0个元素进行位运算bit_map[0] & (1 << 11)
运行以下代码, 我们可以发现fd_set底层采用一个long
数组来保存位图
可以看出,在我的机子上最多只能监控1024个文件描述符。
验证
接下来,我们编写一个小demo对上文的描述进行验证。
我们往fd_set中添加1, 2, 3, 65, 63, 129, 1022
这几个元素,并遍历fd_set中的数据并将其转换为2进制进行打印。
#include <stdio.h>
#include <sys/select.h>
void print_binary(__fd_mask num) {
long i;
for (i = 0; i < 64; i++) {
printf("%c", ((num >> (63 - i)) & 1) != 0 ? '1':'0');
}
}
int main() {
fd_set fds;
FD_ZERO(&fds);
FD_SET(1, &fds);
FD_SET(2, &fds);
FD_SET(3, &fds);
FD_SET(65, &fds);
FD_SET(63, &fds);
FD_SET(129, &fds);
FD_SET(1022, &fds);
FD_SET(1025, &fds);
int i;
for(i = 0; i < sizeof(fd_set) / sizeof(__fd_mask); i++) {
__fd_mask num = fds.__fds_bits[i];
printf("__fds_bits[%d]=%ld, bits=", i, num);
print_binary(num);
printf("\n");
}
return 0;
}
观察可以发现, 对应位置的比特位均被设置为了1. 但是, 由于位图大小的限制数值为1025
的文件描述符并没有被添加到位图中。
上图输出的二进制数据排序为从高位到低位
总结
- select底层采用一个长整型数组来保存位图数据
- 由于位图的大小由限制,因此只能监控有限数量的文件描述符, 在我的机子上是1024个文件描述符