对select函数的理解

7次阅读

共计 1220 个字符,预计需要花费 4 分钟才能阅读完成。

1. 处理多个 socket 链接的方法

阻塞模式下服务端要解决多个客户链接的问题的 3 个思路:

  1. 每个客户端的 socket 对应一个内核线程,在这个线程内部进行阻塞的 read
  2. 单线程,自己记录一个 socket 列表,循环去内核中查询 socket 是否 ready
  3. 单线程,系统提供一个 ready 状态的 socket 列表,主线程从这个列表中处理 socket

思路 1,如果链接很多(C10k)线程就会很多,消耗系统资源,并增加调度成本(Java BIO)。

思路 2,每次都要遍历一边所有 socket,链接很多时效率低,可能大部分链接都没数据(select)。

思路 3,比较理想 (epoll)。

2. select 函数

2.1. 使用方法

函数原型:

select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

select() 检查 readfds, writefds 中的 io 描符是否可读、可写了,如果有 ready 状态的,函数就返回。
nfds 是总共 fd 的个数,而不是最大的 fd。

使用 select 函数步骤:

  1. 初始化 fd_set,把要监控的 fd 仍进去
  2. 调用 select, 阻塞
  3. 阻塞结束后,遍历查看 fd_set 中的 ready 的 socket

fd_set 是什么?
一个结构体:

typedef    struct fd_set {__int32_t    fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)];
} fd_set;

结构体中有一个数组,默认是是 1024,这就是 linux 中 select 的函数限制最大链接数的原因。
重新编译内核才能提高这个数字。
FD_ISSET 就是取对应位置状态值(0,1),并且在用户空间,
用户需要遍历编一遍这个数组来检查是哪个 socket 有数据。

select 的内部实现:

  1. readfds 从用户空间传递到内核空间
  2. 将当前进程加入到 readfds 中的每个 socket 的等待队列
  3. 当 socket 来数据了就把 线程唤醒 (移出等待队列)
  4. 把有数据的 fds 从内核空间搞到用户空间
  5. 用户空间看一遍 fds,知道哪个 socket 有数据了,然后 read、accept

select 的问题:

  1. 每次调用 select 就要把 readfds 传到内核,wake 的时候再拿回来需要传递 1024 * 4 bytes
  2. 每次需要把当前线程加入到所有 socket 的等待队列
  3. 每次需要遍历一遍 readfds 来查看那个 socket 有数据

每次调用 select 都会有以上两次传递和两次遍历,当链接个数多时,性能下降比较快:

select 可能的改进:

  1. readfds 一直都在内核中维护,不要每次都送进来
  2. 可以动态单个变更内核中的 readfds
  3. 就绪列表,传给内核一个指针,内核把这个指针指向就绪的 sockets (避免来回传递所有的 socket)
正文完
 0