一、文件符集(fd_set)

  • 概念:​文件符集可以看成是一个数组,可以向其中添加很多的文件描述符
  • 数据类型:​fd_set
  • select就是通过获取文件符集进行操作的


fd_set结构体的定义如下

  • 由此我们可以看出fd_set结构体仅包含一个​整型数组​,该数组的每一个元素的每一位(bit)标记一个文件描述符
  • FD_SETSIZE宏:​限制了select能够同时处理的文件描述符的总量(在下面介绍select参数1时也会介绍)

APUE编程:22---高级I/O之(IO多路复用:select()函数)_IO复用


二、文件符集操作宏

#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
  • fd_set结构是通过位(bit)来操作的,为了简化,系统提供了下面一系列的宏来操作fd_set结构体


意义如下:

  • FD_ISSET:​判断参数1所指向的文件描述符集是否在参数2所指向的文件符集中。如果在返回非0值;否则返回0
  • FD_CLR:​将参数1所指向的文件描述符集从参数2所指向的文件符集中移除
  • FD_SET:​将参数1所指向的文件描述符集添加到参数2所指向的文件符集中
  • FD_ZERO:​将文件符集清除(每次使用文件符集之前,都需要使用FD_ZERO将文件符集初始化清空)



宏操作的原理

  • 每一个文件符集(fd_set)都有很多位:如果想添加一个文件描述符,就根据其文件描述符值放置在文件符集对应的位上(FD_SET);同理,FD_CLR是删除其中的一位,FD_ISSET是查看其中的一位是否开启,FD_ZERO是清空所有的位
  • 下面是select三个文件符集的图示:

APUE编程:22---高级I/O之(IO多路复用:select()函数)_IO复用_02


二、select函数

#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,struct timeval *restrict tvptr);
  • 功能:​根据很多附加条件,对文件符集进行高级的操作
  • 如果参数2,3,4都为NULL,那么select就变成一个定时器函数了


参数1

  • 概念:​是参数2,3,4三个描述符集中的最大文件描述符值加1
  • 为什么这样设计:​因为select会去检查文件描述符集中哪些位处于打开状态,如果此时限制一个值(也就是一个区域),这样内核就会在此范围内寻找打开的位,而不必在所有的位中进行查找
  • 图解:​例如下图我们设置为读集合写集,写集中的fd3值最大,因此我们参数1设置为4

APUE编程:22---高级I/O之(IO多路复用:select()函数)_套接字_03

  • FD_SETSIZE常量:​这是在<sys/select.h>中的一个常量,它指定了最大描述符数(经常是1024),select的参数1也可以设置为这个数,但是这个数的值太大了,寻找范围太大,浪费时间,不建议使用。但是如果​提前不知道​最大描述符值为多大,可以选择使用这个



参数5

  • 概念:​select指定等待参数2,3,4文件描述符集的时间,单位为秒和微秒数

取值如下:

  • tvptr == NULL:​永远等待。如果等待的文件描述符集中有一个已经准备好了select就返回。如果捕捉到一个信号也返回(此时sleect返回-1,errno设置为EINTR)
  • tvptr−>tv_sec == 0 && tvptr−>tv_usec == 0:​根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态不阻塞select函数的方法
  • tvptr−>tv_sec != 0 || tvptr−>tv_usec != 0:​等待指定的秒数和微秒数。如果其中一个描述符已准备好就立即返回、或者等待中捕捉到一个信号就返回(此时sleect返回-1,errno设置为EINTR),或者超时之后返回(此时select返回0)

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

注意事项:

  • 如果系统不提供微秒分辨率,则​tvptr−>tv_usec​值取整到最近的支持值(如果timeout参数所指向的timeval结构中的tv_sec成员值超过1亿秒,那么有些系统的 select函数将以EINVAL错误失败返回。当然这是一个非常大的超时值(超过3年),不大可 能有用,不过就此指出:timeval结构能够表达select不支持的值)
  • Linux允许更改timeval的值,见下图:

APUE编程:22---高级I/O之(IO多路复用:select()函数)_select函数_04

  • 有些系统不允许修改timeval的值,会把该参数变为const类型。timeout参数的const限定词表示它在函数返回时不会被select修改。举例来说,如果我们 指定一个10s的超时值,不过在定时器到时之前select就返回了(结果可能是有一个或多个描 述符就绪,也可能是得到EINTR错误),那么timeout参数所指向的timeval结构不会被更新成该 函数返回时剩余的秒数。如果我们需要知道这个值,那么必须在调用select之前取得系统时间, 它返回后再取得系统时间,两者相减就是该值(任何健壮的程序都得考虑到系统时间可能在这 段时间内偶尔会被管理员或ntpd之类守护进程调整)
  • 有些Linux版本会修改这个timeval结构。因此从移植性考虑,我们应该假设该timeval 结构在select返回时未被定义,因而每次调用select之前都得对它进行初始化。POSIX规 定对该结构使用const限定词



参数2,3,4

  • 概念:​这三个参数是指定select操作的三个文件描述符集

分类:

  • 参数2(读集):​此文件描述符集所指向的文件描述符是内核测试“读”的描述符
  • 参数3(写集):​此文件描述符集所指向的文件描述符是内核测试“写”的描述符
  • 参数4(异常集):​这是一个异常条件集(表示里面的描述符都是未决异常条件)。如果放在此参数上,表示这个异常条件集已经准备好可以进行操作(现在异常条件包括:某个套接字的带外数据的到达,或者在处于数据包模式的伪终端发生了某些条件)

APUE编程:22---高级I/O之(IO多路复用:select()函数)_IO复用_05

注意事项:

  • 这3个参数根据需要选择,如果哪一项不需要,可以设置为NULL
  • 如果3个参数都设置为NULL:​那么select就变成了一个定时器函数(而不是文件描述符操作函数了)。select提供比sleep更精确的时间(秒和微妙,而sleep只提供秒)
  • 目前支持的异常条件只有两个,见下图

APUE编程:22---高级I/O之(IO多路复用:select()函数)_select函数_06



select返回值:

  • -1:​select函数出错,并设置errno。分为以下情况
  • select函数出错
  • select函数阻塞等待期间捕捉到信号,此种情况下errno为EINTR
  • 0:​表示没有描述符准备好。分为以下两种情况返回:
  • 在select非阻塞状态下没有描述符集准备好,返回0
  • 在select等待模式下超时了,也返回0
  • 一个正值:​为参数2,3,4已经准备好的文件描述符数之和。(注意:如果同一描述符同时准备好读和写,那么返回值中会对其重重复计数。这种情况下,不同的参数仍为该文件描述符开启对应的位)


三、select使用事项


注意事项①

  • select使用前需要加入文件符集,所以如果是一个长时间循环运行的程序,​需要在每次select前重新设置文件符集​,然后将文件符集加入到select函数中
  • 例如:下面的案例中,用while(1)循环使用select函数,那么每次进行select之前都需要初始化一些文件符集,然后将这个文件符集加入到select函数中去



注意事项②

  • 文件符集中的文件描述符是否阻塞不会影响select的阻塞,两者没有任何关系



注意事项③

  • select和poll的可中断性
  • 中断的系统调用的自动重启是由4.2BSD引入的,但当时select函数是不重启的。这种特性在大多数系统中一直延续了下来,即使指定了SA_RESTART选项也是如此。但是,在SVR4上,如果指定了SA_RESTART,那么select和poll也是自动重启的。为了在将软件移动到SVR4派生的系统上时阻止这一点,如果信号有可能会中断select和poll,就要使用signal_intr函数(见下代码)
  • 但是本人博客文章所说明的各种实现在​接收到一信号时都不重启poll和select,​即便使用了SA_RESTART标志也是如此

Sigfunc *signal_intr(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif

if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}


四、select描述符的就绪条件


①满足下列四个条件中的任何一个时,一个套接字准备好读

  • 该套接字​接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记(SO_RCVLOWAT)的当前大小​。此时我们​可以不阻塞的读取该套接字,并且读操作返回的字节数大于0​(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1
  • socket通信对方关闭连接(也就是接收了FIN的TCP连接)​。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)
  • 该​套接字是一个监听(accept)套接字且已完成的连接数不为0​(也就是监听套接字上有新的连接请求(客户端调用connect请求连接)),之后我们就可以调用accept()函数对客户端进行接收。对这样的套接字的accept通常不会阻塞,不过我们将在15.6节讲解accept可能阻塞的一种时序条件
  • 其上​有一个套接字错误待处理​。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除

②下列四个条件中的任何一个满足时,一个套接字准备好写

  • 该套接字​发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记(SO_SNDLOWAT)的当前大小​,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,此刻我们可以无阻塞地写该socket,并且写操作返回的字节数大于0(如由传输层接受的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和 UDP套接字而言,其默认值通常为2048
  • socket通信对方的写操作被关闭​。对这样的套接字的写操作将产生SIGPIPE信号(见文章:
  • 使用​非阻塞式connect的套接字已建立连接​,或者connect已经以失败告终
  • 其上​有一个套接字错误待处理​。对这样的套接字的写操作将不阻塞并返回-1(也就是返 回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定 SO_ERROR套接字选项​调用getsockopt来获取错误并清除

③select处理的异常情况只有一种,如下:

  • 套接字存在带外数据或者仍处于带外标记


  • 注意:当​某个套接字上发生错误时,它将由select标记为既可读又可写
  • 接收低水位标记和发送低水位标记的目的在于:​允许应用进程控制在select返回可读或可 写条件之前有多少数据可读或有多大空间可用于写。举例来说,如果我们知道除非至少存在64 个字节的数据,否则我们的应用进程没有任何有效工作可做,那么可以把接收低水位标记设置 为64,以防少于64个字节的数据准备好读时select唤醒我们
  • 任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小​(默认应该总是这种关系) 就总是可写的,这是因为UDP套接字不需要连接
  • 下图汇总了上述导致select返回某个套接字就绪的条件:

APUE编程:22---高级I/O之(IO多路复用:select()函数)_描述符_07

五、select的最大描述符

  • 早些时候我们说过,​大多数应用程序不会用到许多描述符。​譬如说我们很少能找到一个使用几百个描述符的应用程序。然而使用那么多描述符的应用程序确实存在,它们往往使用 select来复选描述符。最初设计select时,操作系统通常对每个进程可用的最大描述符数设置 了上限(4.2BSD的限制为31),select就使用相同的限制值。然而当今的Unix版本允许每个进 程使用事实上无限数目的描述符(往往仅受限于内存总量和管理性限制),因此我们的问题是: 这对select有什么影响?


FD_SETSIZE常量

  • 许多实现有类似于下面的声明,它取自4.4BSD的头文件:

/*
* select uses bitmasks of file descriptors in longs. These macros
* manipulate such bit fields (the filesystem macros use chars).
* FD_SETSIZE may be defined by the user, but the default here should
* be enough for most uses.
*/
#ifndef FD_SETSIZE
#define FD_SETSIZE 256
#endif

  • 这使我们想到,可以在包括该头文件之前把FD_SETSIZE定义为某个更大的值以增加 select所用描述符集的大小。不幸的是,这样做通常行不通(FD_SETSIZE常值的声明一直是在头文件中(4.4BSD和4.4BSD-Lite2),不过更新的源自BSD的内 核和源自SVR4的内核把它改放在头文件中。值得注意的是,有些应用程序(典型例子是需要 复选大量描述符的事件驱动型服务器程序,所需描述符量超过1024个)开始改用poll代替select,这样可以避 免描述符有限的问题。还要注意的是,select的典型实现在描述符数增大时可能存在扩展性问题 )
  • 为了弄清楚到底出了什么差错,请注意TCPv2的图16-53声明了3个在内核中的描述符集, 并把内核的FD_SETSIZE定义作为上限使用。因此增大描述符集大小的唯一方法是先增大 FD_SETSIZE的值,再重新编译内核。不重新编译内核而改变其值是不够的
  • 有些厂家正在将select的实现修改为允许进程将FD_SETSIZE定义为比默认值更大的某个 值。BSD/OS已改变了内核实现以允许更大的描述符集,并定义了四个新的FD_xxx宏用于动态分 配并操纵这样的描述符集。​然而从可移植性考虑,使用大描述符集需要小心


六、演示案例

  • 用select实现TCP服务端与客户端通信
//客户端
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>

#define MAXBUF 1024
int main(int argc, char **argv)
{
int sockfd, len;
struct sockaddr_in dest;
char buffer[MAXBUF + 1];
fd_set rfds;
struct timeval tv;
int retval, maxfd = -1;

if (argc != 3)
{
printf("argv format errno,pls:\n\t\t%s IP port\n",argv[0], argv[0]);
exit(EXIT_FAILURE);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("Socket");
exit(EXIT_FAILURE);
}

bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(atoi(argv[2]));
if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//连接
if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0)
{
perror("Connect ");
exit(EXIT_FAILURE);
}

printf("\nget ready pls chat\n");
while (1)
{
FD_ZERO(&rfds); //清空rfds描述符集
FD_SET(0, &rfds); //将stdin加入rfds描述符集
FD_SET(sockfd, &rfds);//将sockfd加入rfds描述符集
maxfd = sockfd;
tv.tv_sec = 1; //设置等待时间
tv.tv_usec = 0; //设置等待时间
retval = select(maxfd + 1, &rfds, NULL, NULL, &tv); //操控rfds这个文件符集
if (retval == -1) //出错{
printf("select %s", strerror(errno));
break;//直接退出
}
else if (retval == 0)//没有文件描述符准备好
continue;//再回去查看
else{//如果有描述符准备好了
if (FD_ISSET(sockfd, &rfds)) //判断sockfd是否在rfds文件符集中并且是否有数据
{
bzero(buffer, MAXBUF + 1);
len = recv(sockfd, buffer, MAXBUF, 0);//接受
if (len > 0)
printf ("recv message:'%s',%d byte recv\n",buffer, len);
else
{
if (len < 0)
printf ("message recv failure\n");
else
{
printf("the othe quit ,quit\n");
break;
}
}
}
if (FD_ISSET(0, &rfds)) //判断stdin是否在rfds文件符集中并且是否有数据
{
bzero(buffer, MAXBUF + 1);
fgets(buffer, MAXBUF, stdin);
if (!strncasecmp(buffer, "quit", 4)) {
printf("i will quit\n");
break;
}
len = send(sockfd, buffer, strlen(buffer) - 1, 0);//发送
if (len < 0) {
printf ("message send failure");
break;
} else
printf("send success,%d byte send\n",len);
}
}
}
close(sockfd);
return 0;
}
//服务端
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/types.h>

#define MAXBUF 1024

int main(int argc, char **argv)
{
int sockfd, new_fd;
socklen_t len;
struct sockaddr_in my_addr, their_addr;
unsigned int myport, lisnum;
char buf[MAXBUF + 1];
fd_set rfds;
struct timeval tv;//等待时间
int retval, maxfd = -1;

if (argv[2])
myport = atoi(argv[2]);
else
myport = 7838;
if (argv[3])
lisnum = atoi(argv[3]); //监听数目
else
lisnum = 2;
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}

bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(myport);
if (argv[1])
my_addr.sin_addr.s_addr = inet_addr(argv[1]);
else
my_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(sockfd, lisnum) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}

while (1)
{
printf ("\n----wait for new connect\n");
len = sizeof(struct sockaddr);
if ((new_fd =accept(sockfd, (struct sockaddr *) &their_addr,&len)) == -1) {
perror("accept");
exit(errno);
} else
printf("server: got connection from %s, port %d, socket %d\n", inet_ntoa(their_addr.sin_addr),ntohs(their_addr.sin_port), new_fd);
while (1)
{
//每次循环都需要进行下面几步
FD_ZERO(&rfds);//初始化
FD_SET(0, &rfds);//把标准输入放入rfds中
FD_SET(new_fd, &rfds);//把new_fd放入rfds中
maxfd = new_fd;//最大的fd
tv.tv_sec = 1;
tv.tv_usec = 0;
retval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
if (retval == -1) //出错返回-1
{
perror("select");
exit(EXIT_FAILURE);
} else if (retval == 0) {//超时返回0
continue;
}
else
{
if (FD_ISSET(0, &rfds))//看看标准输入有没有数据
{
bzero(buf, MAXBUF + 1);
fgets(buf, MAXBUF, stdin);//得到
//刚才标准输入的内容为什么不见了?到fgets的时候还要阻塞?
//因为select把标准输入的内容放入到自己的缓冲区里面了。
//将放入到select中的句柄都设置为非阻塞状态
if (!strncasecmp(buf, "quit", 4)) {
printf("i will quit!\n");
break;
}
len = send(new_fd, buf, strlen(buf) - 1, 0);//发送
if (len > 0)
printf ("send successful,%d byte send!\n",len);
else {
printf("send failure!");
break;
}
}
if (FD_ISSET(new_fd, &rfds)) //如果new_fd中是否有数据
{
bzero(buf, MAXBUF + 1);
len = recv(new_fd, buf, MAXBUF, 0);//接收
if (len > 0)//打印
printf ("recv success :'%s',%dbyte recv\n", buf, len);
else{
if (len < 0)
printf("recv failure\n");
else{
printf("the ohter one end ,quit\n");
break;
}
}
}
}
}
close(new_fd);
printf("need othe connecdt (no->quit)");
fflush(stdout);
bzero(buf, MAXBUF + 1);
fgets(buf, MAXBUF, stdin);
if (!strncasecmp(buf, "no", 2))
{
printf("quit!\n");
break;
}
}
close(sockfd);
return 0;
}