select中文释义表示选择
为实现socket非阻塞。通过将fd句柄放在集合中(一维数组),通过轮询的方式来判断当前是否有事件发生。
select作用
监视文件描述符的变化(可读、可写或异常)
在Linux系统中,一切都以文件的形式存在,包括网络通讯也是。因此select通常用来监视socket文件句柄。
简单理解为
监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了
我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了.
监视这些文件描述符的变化,读变化表示可以从这些文件中读取数据(简单理解为接收数据),写变化表示可以向这些文件中写入数据(发送数据)。
头文件
#include <sys/select.h>
函数原型
int select (int maxfd + 1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval * timeout);
参数说明
参数一:最大的文件描述符加1
参数二:用来检查可读性的一组文件描述字。(接收的数据)
参数三:用来检查可写性的一组文件描述字。(发送的数据)
参数四:用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
参数五:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。timeval结构的定义:
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
}
timeout: 有三种可能:
- timeout = NULL (阻塞:直到有一个fd位被置为1函数才返回)
- timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)
- timeout所指向的结构,时间设为0(非阻塞:函数检查完每一个fd后立即返回)
readset writeset exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为NULL。如果三个指针都为NULL,我们就有了一个比sleep()函数更为精确的定时器(sleep()以毫秒为最小单位,这个以微秒为单位)。
参数一
使用最大的文件描述符可以保证select能够轮询到每个监视的文件描述符,加1的原因是fd_set是一维数组。从数组下标0开始存储。
并且参数一还有一个最大的作用是提高效率,使得select不必轮询检查默认fd_set的所有1024位。(#define __FD_SETSIZE 1024)
返回值
>0:就绪描述字的正数目(返回对应位仍然为1的fd的总数)
-1:出错
0 :超时(没有可读写或错误的文件)
select缺陷
-
单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
-
内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
-
select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
-
select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。
拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
如何查看当前Linux系统 select最大支持多少?
在Linux下,我们使用ulimit -n 命令可以看到单个进程能够打开的最大文件句柄数量(socket连接也算在里面)。系统默认值1024。”
FD相关接口
select文件描述符基本流程
定义fd_set变量,用来存储fd文件描述符。
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
#define __FD_SETSIZE 1024
需要配合FD_ZERO使用,否则会出现不可预知的后果。就跟平时定义个结构体,memset的原理一样。要清空fd_set,便于存储fd。
fd_set set;
FD_ZERO(&set);
FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*测试fd是否在set集合中*/
接口使用实例
FD_ZERO(&set); /*将set的所有位置0,如set在内存中占8位则将set置为00000000*/
FD_SET(0, &set); /*将set的第0位置1,如set原来是00000000,则现在变为100000000,这样fd==1的文件描述字就被加进set中了*/
FD_CLR(4, &set); /*将set的第4位置0,如set原来是10001000,则现在变为10000000,这样fd==4的文件描述字就被从set中清除了*/
FD_ISSET(5, &set); /*测试set的第5位是否为1,如果原来set是10000100,则返回非零,表明fd==5的文件描述符在set中,否则返回0*/
注意:fd的最大值必须<FD_SETSIZE。
accept函数主要会使用server端的fd,然后返回新增fd用于client进行通讯。
accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。