select原理解析
IO复用
非阻塞IO模型,是一个线程一个socket,需要轮询的通过IO系统调用来判断数据是不是准备好了,这个模型会导致IO API的频繁调用,会浪费CPU时间片,并且客户端连接多的时候,线程开启太多,服务端压力很大,为此使用IO复用来代替非阻塞IO判断数据是不是准备好了(内核代替判断),并且一个线程里面的一个IO复用可以挂载多个IO句柄,减少线程开辟.
select
select用于检测一组socket中某个或者几个是不是又事件就绪,事件可以分为下三种:
- 读事件就绪
- 对于listenfd,一般不会检测listenfd 的写事件没有意义
- 有新连接来了
- 对于普通fd
- 有新数据来了
- 对端关闭连接,此时调用recv活read返回0
- 对于listenfd,一般不会检测listenfd 的写事件没有意义
- 写事件就绪,只针对普通的socketfd
- 普通的socketfd,可以利用改fd进行发送数据,且数据一定可以写入内核缓冲区,并不是要发的数据一次性发完
- 异常事件就绪
- socket 上收到带外数据
函数签名:
- socket 上收到带外数据
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
- 参数一: int nfds,一组绑定到fd_set上的socket的最大句柄数值+1,比如说,绑定了句柄20,30,40,则nfds = 40+1。linux下要严格执行,但是win下这个nfds没有意义,win自己忽略不计,但是为了保证平台兼容,也会找到最大句柄数,并+1传入,因为win下的socket句柄并不是整型。
tip: 句柄不是内核对象的地址,而是内核句柄表中的索引 - 参数二: 检测一组socket的读事件的时候,将socket传入readfds
- 参数三: 检测一组socket的写事件的时候,将socket传入writefds
- 参数四: 检测一组socket的异常的时候,将socket传入exceptfds,一般设置为NULL
- 参数五: 超时时间,可以设置为NULL、0、>0三种情况:
- 为NULL的时候,select会一直等待,直到绑定到上面的某个socket有事件,会返回,返回值会告诉你有几个socket有事件
- 为0的时候,秒和微秒都是0,select不管有没有事件都会返回,有返回事件数,没有也是直接返回,不会阻塞。
- >0的时候,在设定时间内,有事件就会返回,没有事件就等到这么长时间再返回。
如果三个fd_set都没有事件,且超时时间设置为NULL,就会永久阻塞
- 返回值: 返回值是返回有有事件的socket数量,如果三个fd_set都设置,那么有两个有事件,返回2,只有一个有事件返回1,如果三个都不设置就是个超时函数。
函数参数结构体:
- 超时时间结构体:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- fd_set结构体:
windows:
数组实现
typedef struct fd_set {
u_int fd_count; /* how many are SET? */ 表示fd_array里面有效的fd数量
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
linux:
// 标志位来实现
typedef struct
{
一个long int linux下是8个字节 一个字节是8bit 16个元素就是8*8*16=1024bit,
每个bit对应fd的事件状态,0表示没有事件,1表示有事件,1024是支持的最大fd数量
long int __fds_bits[16]; // 标志位
} fd_set;
使用实例:
- 如何设置
fd_set writeset;
//linux标志位全部清0,windows是将writeset有效fd数量清0
FD_ZERO(&writeset);
// linux,向_fds_bits中的某个bit上设置一个标志,windows,把一个socket挂在fd_set上
FD_SET(m_hSocket, &writeset);
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 100;
select(m_hSocket + 1, NULL, &writeset, NULL, &tv);
与select搭配的四个宏:
- FD_CLR(int fd,fd_set* set);用来清除描述词组set中相关fd 的位
- FD_ZERO(fd_set *set);用来清除描述词组set的全部位
- FD_SET(int fd,fd_setset);用来设置描述词组set中相关fd的位
- FD_ISSET(int fd,fd_set set);用来测试描述词组set中相关fd 的位是否为真
优点:
- 非阻塞IO每次查询数据是否准备好了,都需要切换内核态,轮询消耗CPU。select函数直接将查询多个socketfd的动作交给了内核,避免了CPU消耗和减少了内核态的切换。
- 非阻塞IO一个线程一个socket连接,客户端连接数量大的时候,服务器压力很大,并且很难实现高并发
缺点:
- 每次调用 select 函数,都需要把 fd 集合(fd_set)从用户态拷贝到内核态(内核态切换),这个开销在 fd 较多时会很大,同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
- 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
- select 函数在每次调用之前都要对传入参数进行重新设定,这样做也比较麻烦。因为select函数返回后,会改变fd_set数组的标志为,如果是发送了事件标志为1,没发送标志为0.
- 如果有事件发生,select需要轮询所有挂载的socketfd,取寻找是哪个触发了事件。无差别轮询,时间复杂度O(n), 挂载的socketfd越多,效率越低。
关于select传入fd_set的过程:
- 应用程序调用select之后,会将fet_set拷贝到内核空间,应用程序会阻塞等待函数返回。
- 内核根据fd_set得到需要处理的句柄,根据句柄是否准备好数据,给fd_set置位。
- 线程/进程被唤醒,拿到内核处理过的fd_set,就可以通过FD_ISSET来判断套接字是不是准备好了。