IO 多路复用(一) select 函数

引言

在介绍 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 函数等待描述符就绪的最大时间,该值有三种选项:

  • 永远等待
    当设置成 NULL 的时候,该函数会一直等待关联的任一描述符就绪。
  • 等待指定时间
    当设置成具体值的时候,该函数会等待指定时间,直到超时或者描述符就绪。
  • 立即返回
    当该值设置成 0 的时候,函数会立即返回,通过轮询的方式,无阻塞地获取多个文件描述符状态。

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

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

触发条件
  • 有数据可读
    该套接字接收缓冲区中的字节数大于等于套接字接收缓冲区低潮标记大小。对这样的套接字读操作将不阻塞并返回一个大于 0 的值(也就是返回准备好读入的数据)。我们可以使用 SO_RCVLOWAT 套接字选项设置该套接字的低潮标记。对于 TCP 和 UDP 套接字而言,其缺省值为 1。
  • 关闭连接的读一半
    该连接的读这一半关闭(也就是接收了 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回 0(也就是返回 EOF)。
  • 给监听套接字准备好新连接
    该套接字是一个监听套接字且已完成的连接数不为 0。对这样的套接字的 accept 通常不会阻塞。
  • 待错误处理
    其上有一个套接字错误待处理,对这样的套接字的读操作将不阻塞并且返回 -1,同时把 errno 设置成确切的错误条件。这些待处理的无措也可以通过制定 SO_ERROR 套接口选项调用 getsockopt 获取并清除。

writefds

描述符的写操作就绪。

触发条件
  • 有可用于写的空间
    该套接字发送缓冲区中可用空间字节数大于等于套接字发送缓冲区低潮标记大小,并且或者(i)该套接字已连接,或者(ii)该套接字不需要连接(比如 UDP 套接字)。这意味着我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。我们可以使用 SO_SNDLOWAT 套接字选项来设置该套接字的低潮标记。对于 TCP 和 UDP 套接字而言,其缺省值通常为 2048。
  • 关闭连接的写一半
    该连接的写这一半关闭。对这样的套接字的写操作将产生 SIGPIPE 信号。
  • 该套接字早先使用非阻塞式 connect 已建立连接,并且连接已经异步建立,或者 connect 已经以失败告终。
  • 待处理错误
    其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并且返回 -1,同时把 errno 设置成确切的错误条件。这些待处理的无措也可以通过制定 SO_ERROR 套接口选项调用 getsockopt 获取并清除。

errorfds

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

1.1.2 返回值

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

  • 0
    当函数等待超时的时候返回。
  • -1
    当函数发生错误的时候返回。
  • n
    n 为已经就绪的描述符的数量。

1.2 宏

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

  • FD_ZERO(&fd_set)
    使用 fd_set 之前,必须对它进行初始化,该宏的作用就是将 fd_set 所有位清零。
  • FD_SET(n, &fd_set)
    将 n 对应的描述符添加到描述符集 fd_set 中。换句话说,注册你感兴趣的描述符到内核。
  • FD_CLR(n, &fd_set)
    将 n 对应的描述符从描述符集中移除。
  • FD_ISSET(n, &fd_set)
    判断指定的描述符 n 有没有就绪。

1.3 注意

  • 第一个参数 nfds 是最大文件描述符 +1
  • 函数返回后,相关的未就绪描述符需要重新设置
  • 函数返回后,超时时间也需要重新设置
  • 函数可以检测远程连接断开
    如:读描述符 socket 断开时,这个 socket 变为一直可读,但是读到的是文件结尾 EOF。以此用来判断远程连接是否断开。
  • 函数阻塞时,会被信号中断
  • select 与 stdio 混合使用带来的有关缓冲区问题

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容