C

IO 多路复用(一) select 函数

2020-12-15  本文已影响0人  Tubetrue01

引言

在介绍 IO 多路复用的时候,始终都会围绕 select、pselect、poll、epoll 这几个函数展开。为了对它们进行详细的说明,这里会分为几篇文章(避免篇幅过大)。

1.0 select 函数

select 是用于 I/O 多路复用的一个系统调用函数。在 C 程序中,该系统调用在 sys/select.h 或 unistd.h 中声明。它允许进程指示内核等待多个事件中的任何一个发生,当任何事件发生或者超时后它将被唤醒。

1.1 函数声明

#include <sys/select.h>

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

1.1.1 参数

timeout

我们先从最简单的参数入手 timeout, 该参数是一个 timeval 类型的结构体,该结构体声明如下:

struct timeval
{
    long tv_sec;   /* seconds */
    long tv_usec;  /* microseconds */
}

该参数用来指定 select 函数等待描述符就绪的最大时间,该值有三种选项:

nfds

乍一看这个参数,以为是文件描述符的数量呢,毕竟有个 n,结果惨遭打脸。查了一些资料,大体意思是当前注册的描述符最大值加 1。比如有 {1、3、5 } 三个描述符被注册,那么 nfds 的值是: 5 + 1 = 6;为什么这么设计呢?

分析(如果不对,大家可以帮我指出,谢谢):

  1. 描述符以位存储(下面会介绍)。
  2. 位数又以 0 开始(类似数组从 0 开始)
  3. 位数:7----6----5----4----3----2----1----0
    开关:0----0----1----0----1----0----1----0
  4. 需要遍历的描述符的索引为 5,也就是数组第 6 个元素

总结

其实 nfds 还是描述符的数量,只不过需要最大的描述符加 1,为什么加 1,其实是因为索引为 0 的缘故。数组遍历是需要知道数组元素的数量的。


在介绍接下来的参数之前,首先需要介绍另一个很重要的概念,即 fd_set。它也是一个结构体,声明如下(取自 macOS):

#define __DARWIN_FD_SETSIZE     1024
#define __DARWIN_NBBY           8     /* bits in a byte */
#define __DARWIN_NFDBITS        (sizeof(__int32_t) * __DARWIN_NBBY) /* bits per mask */
#define __DARWIN_howmany(x, y)  ((((x) % (y)) == 0) ? ((x) / (y)) : (((x) / (y)) + 1)) /* # y's == x bits? */

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

简化之后:

typedef struct fd_set {
  int fds_bits[32];
} fd_set;

经过简化之后,fd_set 可以理解为一个数组,这个数组中存放的是文件描述符(Unix 下任何设备、管道、FIFO 等都是文件形式,全部包括在内),而对于 fd_set,系统提供了一系列的宏进行操作,不急,稍后会对这些宏进行介绍。我们继续回到 fd_set 中来,查看它的声明,发现这个结构体只有一个字段 fds_bits,它是一个由 32 个 int 类型的数组。那它具体怎么存放呢?为了弄清这个问题,还是看看它的源码吧。

#define __DARWIN_FD_SET(n, p) do {  \
int __fd = (n);                     \
   ((p) -> fds_bits[(unsigned long)__fd /__DARWIN_NFDBITS] |= ((__int32_t)(((unsigned long)1)<<((unsigned long)__fd % __DARWIN_NFDBITS))));   \
} while(0)

老样子,我们对它进行简化:

#define FD_SET(n, fd_set)  do {                     
    int fd = n;                                     
    fd_set -> fds_bits[fd / 32] |= 1 << (fd % 32);               
  } while(0)

分析:

  1. n 是我们想要监听的描述符,fd_set 就是那个结构体。
  2. fd_set -> fds_bits[fd/32],这个怎么理解呢?首先 fds_bits 是一个由 32 个 int 数值组成的数组,其中每个 int 占 4 个字节。fd/32 则确定 fd 这个描述符在 32 个 int 中的第几个 int 上。
  3. fd%32 确定的是 fd 在 int 的第多少位。那么就将该位置置成 1。

总结

fd_set 这个结构体在我的系统 macOS 上可以存储最多 32 * 32 = 1024 个描述符。每一个 bit 位代表一个描述符,位的编号代表具体的描述符数值。


readfds

你需要告诉内核,你想要关注对这个描述符的目标行为是什么。比如说:当这个描述符可以读取数据的时候,告诉调用者。那么这个参数就是用来告诉内核,我只关心读操作。那么问题来了,什么时候会出发读就绪呢?

触发条件

writefds

描述符的写操作就绪。

触发条件

errorfds

如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
接收和发送低潮标记的目的在于:允许应用进程控制在 select 返回可读或可写条件之前,有多少数据可读或者有多少空间可用于写。比如,如果我们知道除非至少存在 64 个字节的数据,否则我们的应用进程没有任何有效的工作可以做,那么我们可以把低潮标记设置为 64,以防少于 64 个字节的数据准备好读时,select 就唤醒我们。
任何 UDP 套接字只要其发送低潮标记小于等于发送缓冲区大小(缺省应该总是这种关系)就总是可写的,这是因为 UDP 套接字不需要连接。

1.1.2 返回值

该函数有三种返回值,分别是:

1.2 宏

为了便于操作 fd_set,系统提供相应的宏。

1.3 注意


参考

上一篇 下一篇

猜你喜欢

热点阅读