细说八股 | 为什么select只能处理有限数量的连接?

219 阅读4分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

如果你恰好正在学习NIO或者阅读过epoll相关的文章, select这一个系统调用免不了被提出来和epoll对比一番, 而且select最后多少避免不了被提出来diss一顿。此外如果你的简历上写着熟悉网络编程之类的,我想你大概率会被问到selectepoll这两个系统调用之间有什么区别, 因此虽然不用这玩意,但还是搞懂的。本文将首先对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位)的位图, 如下所示。

image.png

其中第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的文件描述符并没有被添加到位图中。

image.png

上图输出的二进制数据排序为从高位到低位

总结

  • select底层采用一个长整型数组来保存位图数据
  • 由于位图的大小由限制,因此只能监控有限数量的文件描述符, 在我的机子上是1024个文件描述符