I/O多路转接之select

select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。关于文件句柄,其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。

select函数原型

1
2
3
#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);

参数介绍:

  • 参数nfds是需要监视的最大文件描述符值 +1
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合
  • timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  • NULL:表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

fd_set接口:

fd_set结构实际是一个位图,在这里做输出输入型的参数,使用位图中对应的位来表示要监视的文件描述符

1
2
3
4
void FD_CLR(int fd,fd_set *set);				//用来清除描述词组set中相关fd的位
void 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的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0.

函数返回值

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测
    • 错误值可能为:*EBADF文件描述词为无效的或该文件已关闭
    • *EINTR此调用被信号所中断
    • *EINVAL参数n为负值
    • *ENOMEM核心内存不足

常见的程序片段如下:

1
2
3
4
5
fd_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset))
{...}

select执行过程

理解select模型的关键在于理解fd_set,为了方便理解我们取fd_set的长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd

  1. 执行fd_set set;FD_ZERO(&set),则set可用位为0000 0000
  2. 若fd=5,执行FD_SET(fd,&set),后set变为0001 0000(第5位置为1)
  3. 若再加入fd=2,fd=1,则set变为0001 0011
  4. 执行select(6,&set,0,0,0)阻塞等待
  5. 若fd=1,fd=2上都发生可读时间,则select返回,此时set变为0000 0011

注意:没有事件发生的fd=5被清空

socket就绪条件

读就绪
  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误
写就绪
  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

select的特点

  • 可监控的文件描述符个数取决于sizeof(fdset)的值。比如说服务器sizeof(fdset) = 512,每bit表示一个文件描述符,则该服务器上支持的最大文件描述符是512*8=4096
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd
    • 一是用于select返回后,array作为源数据和fdset进行FDISSET判断
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最新),扫描array的同时取得fd最大值maxfd,用于selelct的第一个参数

select的缺点

  • 每次调用select,都需要手动设置fd集合,从接口实用角度来说也非常不便
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小,适用于拥有大量链接但只有部分活跃的场景

代码:

0%