文章目录
  1. 前言
  2. select系统调用
    1. 文件描述符就绪条件
    2. 处理带外数据

前言

I/O复用使得程序同时监听多个文件描述符。通常,网络程序在下列情形下需要使用I/O复用技术。

  • 客户端要同时处理多个socket;
  • 客户端要同时处理用户输入与网络连接;
  • TCP服务器要同时处理监听socket与连接socket;
  • 服务器要同时处理TCP请求与UDP请求;
  • 服务器要同时监听多个端口,或处理多个服务;

I/O复用虽然同时监听多个文件描述符,但它本身是阻塞的。

select系统调用

在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写与异常等事件。select系统调用原型如下。

1
2
3
4
5
6
7
8
9
10
/*
引用方式: #include <sys/select.h>
nfds: 被监听的文件描述符总数
rfds: 调用select时指定网络程序关注可读事件的文件描述符; select返回时内核将其修改以通知网络程序可读事件就绪的文件描述符
wfds: 调用select时指定网络程序关注可写事件的文件描述符; select返回时内核将其修改以通知网络程序可写事件就绪的文件描述符
efds: 调用select时指定网络程序关注异常事件的文件描述符; select返回时内核将其修改以通知网络程序异常事件就绪的文件描述符
timeout: 调用select函数前设置超时时间 => select返回后由内核设置select函数的超时时间
返回就绪文件描述符总数: 成功 || 返回-1并设置errno: 失败
*/
int select(int nfds, fd_set * rfds, fd_set * wfds, fd_set * efds, struct timeval * timeout);

其中,fd_set结构体定义如下。

1
2
3
typedef struct{
__fd_mask __fds_bits[1024 / (8 * (int) sizeof (__fd_mask))];
} fd_set;

fd_set是一个__fd_mask数组,数组中每个元素的每一位标记一个文件描述符,因此select能同时处理的文件描述符的总量不能超过1024。可使用如下宏来访问fd_set结构体的位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
#include <sys/select.h>
FD_ZERO: 清除fdset所有位
FD_SET: 设置fdset的位fd
FD_CLR: 清除fdset的位fd
FD_ISSET: 测试fdset的fd位是否被设置
*/
FD_ZERO(fd_set * fdset);

FD_SET(int fd, fd_set * fdset);

FD_CLR(int fd, fd_set * fdset);

int FD_ISSET(int fd, fd_set * fdset);

此外,timeval结构体定义如下。

1
2
3
4
struct timeval{
__time_t tv_sec;/* 秒数 */
__suseconds_t tv_usec;/* 微秒数 */
};

注意:在select调用失败时,timeout的值是不确定的。

select给我们提供了一个微妙级的定时方式,若向timeout变量的tv_sec成员与tv_usec成员传递0,则select将立即返回。若timeout设置为NULL,则select将一直阻塞直到某个文件描述符就绪。

若程序在select等待期间接收到信号,则select立即返回-1,并设置errno为EINTR。

由于内核修改了3个fd_set,因此网络程序下次调用select之前需要重置这3个fd_set。

文件描述符就绪条件

网络编程中,如下情况socket可读。

  • socket内核接收缓冲区中的字节数大于等于其低水位标记SO_RCVLOWAT,此时程序可无阻塞地读这个socket,读操作返回的字节数大于0;
  • socket通信的对方关闭连接,对socket的读操作返回0;
  • 监听socket上有新的连接请求;
  • socket上有未处理的错误,调用getsockopt来读取错误;

如下情况socket可写。

  • socket内核发送缓冲区中的空闲字节数大于等于其低水位标记SO_SNDLOWAT,此时程序可无阻塞地写这个socket,写操作返回的字节数大于0;
  • socket写操作被关闭(对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号);
  • socket使用非阻塞connect连接成功或失败(超时)后;
  • socket上有未处理的错误,调用getsockopt来清除错误;

socket能处理的异常情况只有一种:socket上接收了带外数据。

处理带外数据

socket接收到普通数据与带外数据都将使select返回,但socket处于不同的就绪状态:前者处于可读状态(FD_ISSET(fd, rfds)为1)、后者处于异常状态(FD_ISSET(fd, efds)为1)。