浅谈select模型的实现过程和特点

目录

一、select模型的事件就绪条件

1、读事件就绪

2、写事件就绪

二、select模型的执行过程

1、第一部分:select调用前

2、第二部分:select调用后

三、select的优缺点

1、优点

2、缺点


一、select模型的事件就绪条件

所谓的事件就绪,并非只有数据就绪这种情况,比如监听套接字listen_fd收到了新的连接请求,这也算 读事件就绪的一种,用户可以通过accpet函数将新连接拷贝到上层进行维护,所谓的读不正是将数据从内核拷贝到上层的过程吗?

1、读事件就绪

  • 最典型的读事件是 缓冲区里的数据就绪。 此时缓冲区里数据所占字节数 大于 低水位标记SO_RCVLOWAT,然后上层便可以无阻塞的读该文件描述符。
  • 监听套接字 listen_fd 收到了新的连接请求。这也算是读事件就绪的一种,然后上层便可以调用accept函数将新连接从内核拷贝到用户层,并返回新连接的 fd
  • 对端的连接断开(recv/read的返回值为0)。
  • socket上有未处理的错误

2、写事件就绪

  • 最典型的写事件是 缓冲区里有足够的剩余空间。此时缓冲区里剩余空间所占字节数大小 大于 低水位标记SO_SNDLOWAT ,然后上层便可以无阻塞的写入内容。
  • 调用 close 或者 shutdown 函数,此时写操作被关闭了。
  • socket使用非阻塞connect 连接成功或者失败之后。
  • socket上有未读取的错误。

二、select模型的执行过程

select的执行过程  以select函数调用为界,分为两部分。第一部分是告诉 select 哪些fd是需要被关注的;第二部分是 事件就绪以后的拷贝工作(调用read/recv/write/send函数)。

1、第一部分:select调用前

定义readfds_array数组的原因

在接收数据的时候,可能存在下面两种情况

  • 一个文件描述符只是接收了一部分数据,此时数据就绪了,剩余尚未接收的数据本该留到下次接收,但是第二个参数readfds 输出时会清空所有的内容。
  • 一个文件描述符读/写事件就绪后,第二个参数readfds 清空所有的内容,将就绪结果告知用户,但是下一次还想要继续写入内容,下一次又该怎么办呢??

根本原因在于 第二个参数 readfds 是一个输入输出型参数。输入时,用户告诉内核,哪些文件描述符需要被关注;输出时,readfds将原本的内容清空,换上输出结果,以此告诉用户,有哪些文件描述符上的事件已经就绪。

因此,我们需要一个数组来临时存储下一次仍然需要被关注的fd ,这个数组便是readfds_array

int main()
{
  //创建套接字
  int listen_fd = TcpSocket::CreateSocket();
  //绑定端口号
  TcpSocket::Bind(listen_fd, 8080);
  //设为监听套接字
  TcpSocket::Listen(listen_fd);

#define NUM 1024
  int readfds_array[NUM];         //定义一个数组用于保存下一次需要关注的fd
  for(int i=0; i< NUM ;i++){
    readfds_array[i] = -1;        //初始化数组,元素值为-1表示当前位置未被占用
  }
  readfds_array[0] = listen_fd;   //新连接到来的时候,属于读事件就绪
  fd_set *readfds = nullptr;
  while (1)
  {
    int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
    FD_ZERO(readfds);              //清空读事件集合
    for (size_t i = 0; i < NUM; i++)    //以循环的方式将数组里的fd添加到 读事件集合中
    {    
      if (readfds_array[i] == -1)
        continue;

      //到这一步,readfds_array[i]一定存了文件描述符
      FD_SET(readfds_array[i], readfds);
      //获取到文件描述符的最大值
      max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
    }

    //设置阻塞时间为5s
    struct timeval timeout = {5,0};
    int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
    switch (n)
    {
    case -1:
      //select调用出错
      break;
    case 0:
      //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
      break;
    default:
      /******************* 第二部分 *****************/
      //说明有文件描述符上的事件就绪
      break;
    }
  }

  return 0;
}

2、第二部分:select调用后

这部分要重点了解的是事件就绪后如何处理的问题。(只不过我们这里只关注读事件就绪)。

事件就绪有可能是读事件、写事件就绪、异常事件就绪。此时我们并不知道是哪种事件的哪个fd就绪,所以要逐一去判断。如果 readfds_array[i] == -1,说明该位置没有存放文件描述符,直接进行下一次循环。

int main()
{
  //创建套接字
  int listen_fd = TcpSocket::CreateSocket();
  //绑定端口号
  TcpSocket::Bind(listen_fd, 8080);
  //设为监听套接字
  TcpSocket::Listen(listen_fd);

#define NUM 1024
  int readfds_array[NUM];         //定义一个数组用于保存下一次需要关注的fd
  for(int i=0; i< NUM ;i++){
    readfds_array[i] = -1;        //初始化数组,元素值为-1表示当前位置未被占用
  }
  readfds_array[0] = listen_fd;   //新连接到来的时候,属于读事件就绪
  fd_set *readfds = nullptr;
  while (1)
  {
    int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
    FD_ZERO(readfds);              //清空读事件集合
    for (size_t i = 0; i < NUM; i++)
    {
      if (readfds_array[i] == -1)
        continue;

      //到这一步,readfds_array[i]一定存了文件描述符
      FD_SET(readfds_array[i], readfds);
      //获取到文件描述符的最大值
      max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
    }

    //设置阻塞时间为5s
    struct timeval timeout = {5,0};
    int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
    switch (n)
    {
    case -1:
      //select调用出错
      break;
    case 0:
      //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
      break;
    default:
      /******************* 第二部分 *****************/
      //说明有文件描述符上的事件就绪
      for (size_t i = 0; i < NUM; i++)
      {
        if (readfds_array[i] == -1)   continue;
          
        //判断是不是读事件就绪(判断是否在读事件集合)
        if(FD_ISSET(readfds_array[i],readfds)){
          //说明该文件描述符上的读事件就绪

          
          if(readfds_array[i] == listen_fd){
            //收到了新的连接,此时可以调用accept
            int sock = TcpSocket::Accept(readfds_array[i]);
            if(sock > 0){
              //说明获取成功,此时需要把这个文件描述符加入到 readfds_array[i]中
              //因此需要遍历readfds_array数组,看看有哪个位置未被使用,第0个位置固定给监听套接字使用
              int pos = 1;
              for (; pos < NUM; pos++)
              {
                if (readfds_array[i] == -1)   break;      //-1表示该位置未被使用
              }

              //如果pos<NUM,说明有位置可以放;反之,没有位置可以放
              if (pos < NUM)
              {
                readfds_array[pos] = sock;
              }
              else{
                //没有位置可以放说明服务器满载了
                close(sock);
              }
            }
          }
          else{
            //收到了对方发来的数据,此时可以调用recv/read
            char buffer[1024] = {0};
            ssize_t s = recv(readfds_array[i],buffer,sizeof(buffer)-1,0);
            if (s > 0)
            {
              // 将数据拷贝到上层,接下来可以处理数据了
            }
            else if (s == 0)
            {
              //对端关闭连接了
              close(readfds_array[i]);
              readfds_array[i] = -1;      //空出位置给其他fd使用
            }
            else{
              //读取失败
              close(readfds_array[i]);
              readfds_array[i] = -1;      //空出位置给其他fd使用
            }
          }
        }

        //判断是不是写事件就绪
        // ... ...

        //判断是不是异常事件就绪
        // ... ...

      }
      
      break;
    }
  }

  return 0;
}

三、select的优缺点

1、优点

可以一次等待多个fd,可以让我们等待的时间重叠,一定程度上可以提高IO的效率。尽管多线程也可以实现,但是多线程的运行是受到CPU调度约束的,阻塞等待的时候会被加入到等待队列,事件就绪的时候再回到运行队列,频繁换队列存在一定的损耗。

2、缺点

缺点一

每次都要重新设置哪些文件描述符需要被关注。以第二个参数readfds为例,readfds是一个输入输出型参数,输入时告诉哪些文件描述符需要被关注;输出的时候,内核通知我们哪些文件描述符上的事件就绪了。

但是这样一来,我们最开始设置的输入就被输出结果给覆盖了,因此,下一次调用select的时候我们要重新设置第二个参数。

缺点二

每次调用select都需要把fd集合从用户层拷贝到内核,这个开销在fd较多时会很大。同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

缺点三

fd_set是一个位图结构,每次可以让select 关注的文件描述符数是有限的,只有1024个。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值