网络编程3:select函数用法和原理

select原理解析

IO复用

非阻塞IO模型,是一个线程一个socket,需要轮询的通过IO系统调用来判断数据是不是准备好了,这个模型会导致IO API的频繁调用,会浪费CPU时间片,并且客户端连接多的时候,线程开启太多,服务端压力很大,为此使用IO复用来代替非阻塞IO判断数据是不是准备好了(内核代替判断),并且一个线程里面的一个IO复用可以挂载多个IO句柄,减少线程开辟.

select

select用于检测一组socket中某个或者几个是不是又事件就绪,事件可以分为下三种:

  • 读事件就绪
    • 对于listenfd,一般不会检测listenfd 的写事件没有意义
      • 有新连接来了
    • 对于普通fd
      • 有新数据来了
      • 对端关闭连接,此时调用recv活read返回0
  • 写事件就绪,只针对普通的socketfd
    • 普通的socketfd,可以利用改fd进行发送数据,且数据一定可以写入内核缓冲区,并不是要发的数据一次性发完
  • 异常事件就绪
    • 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来判断套接字是不是准备好了。
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值