该类型的IO是以系统提供的函数select为核心工作的

认识select函数

1
2
3
4
#include <sys/select.h>

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

功能

select系统调用能够同时监测多个文件描述符的状态变化,这个系统调用是阻塞式的,退出阻塞等待的条件是被监视的文件描述符中有一个或多个发生了变化。

参数解释

  • nfds: 文件描述符数组长度,值为最大的文件描述符+1。因为文件描述符从0开始
  • readfds: 本质上是位图,表示待监视的可读文件描述符的集合,返回时标记发生变化的fd
  • writefds: 本质上是位图,表示待监视的可写文件描述符集合,返回时标记发生变化的fd
  • exset: 本质上是位图,表示待监视的异常文件描述符的集合,返回时标记发生变化的fd
  • timeout: 用于设置select()的等待时间

struct timeval

1
2
3
4
5
6
7
8
9
10
struct timeval
{
#ifdef __USE_TIME_BITS64
__time64_t tv_sec; /* Seconds. */
__suseconds64_t tv_usec; /* Microseconds. */
#else
__time_t tv_sec; /* Seconds.秒 */
__suseconds_t tv_usec; /* Microseconds. 毫秒 */
#endif
};

struct timeval是一个储存时间的结构体,它提供了两个成员变量用于分别存储毫秒,因此该参数有三种传参类型

  1. NULL,表示禁用select的超时功能,若没有文件描述符发生变化,将永久阻塞
  2. {0,0},表示阻塞时间为0,即不等待也不会阻塞,仅用于检测文件描述符状态变化
  3. 非零,若在给定的时间内无文件描述符的变化,就会发生超时返回

fd_set

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 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;

虽然在形式上看fd_set内部存储了一个数组,但本质来讲就是一个位图,用相应位置的比特位0/1状态表示该文件描述符是否在集合中

同时select.h也提供了一系列接口来帮助操作这些封装在fd_set里的位图

把它们按C接口的风格表示如下.实际上它们都是封装过的宏函数

1
2
3
4
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

返回值分析

  • 返回非零: 执行成功,返回文件描述符状态改变的个数
  • 返回0: 代表超时返回,没有检测到状态改变
  • 返回-1: 发生错误,错误的原因会设置在errno,此时输出型参数readfds,writefds,exceptfds,timeout的值变得不可预测

errno对应的错误为:

  • EADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

select就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
  • SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

特点/使用要点

  • 可监控的文件描述符个数取决于sizeof(fd_set)
  • 需要额外的数据结构array保存放到select监控集中的fd,原因如下
    • 传入的fd_set是输出型参数,返回后待监控的发生变化的fd对应的标志位会置1,而未发生变化的fd对应的标志位会清空,导致原本传入的参数信息丢失,所以需要额外存储

缺点

由此我们可以总结出select的一些缺点

  1. 每次调用前都要设置待监测的fd集合
  2. 每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  3. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  4. select支持的文件描述符数量太小