IO多路转接之select

深入理解select模型

阻塞和非阻塞两种I/O模式:

阻塞模式: 执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字传统(connect、accept、recv或recvfrom这样的阻塞程序)默认为阻塞模式。可以通过多进程或者多线程技术进行优化处理。

阻塞模式下,进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞(死等在被阻塞的地方),函数不会立即返回

非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回错误(当非阻塞socket的TCP连接正在进行时,Linux的错误号EINPROGRESS,Windows的错误号为WSAEWOULDBLOCK)。但功能强大。它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

非阻塞non-block模式下,进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以非阻塞模式效率较高

为了解决这个问题,提出了进行I/O操作的一些I/O模型。

linux下最常见的三种就是select模式,poll模式,epoll模型。

select描述:

select系统调用是用来让我们的程序监视多个文件句柄(file descrīptor)的状态变化的。通过select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。

select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪些Socket或文件可读可写。

这里写图片描述

参数详解:

ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为要监视各文件中的最大文件描述符值加1。

readfds:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。

writefds:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。

exceptfds:这个文件集将监视文件集中的任何文件是否发生错误,其实,它可用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select函数返回的时候,exceptfds将清除其中的其他文件描述符,只留下标记有OOB数据的文件描述符。

timeout:本次select()的超时结束时间。这个参数至关重要,它可以使select处于三种状态:

若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

FD_ZERO(fd_set *set) 用来清除描述词组set的全部位

FD_SET(int fd,fd_set*set) 用来设置描述词组set中相关fd的位

FD_ISSET(int fd,fd_set *set) 用来测试描述词组set中相关fd 的位是否为真

FD_CLR(inr fd,fd_set* set) 用来清除描述词组set中相关fd 的位

select函数的返回值:

正值:表示监视的文件集中有文件描述符符合要求

零值:表示select超时

负值:表示发生了错误

注意:select()调用会清空传递给它的集合参数中的内容,也就是会清空readfds、writefd、exceptfds这三个指针参数所指定的描述符集合。因此,在每次调用select()之前,必须重新初始化并把需要监视的描述符填写到相应的描述符集合中。select()调用也会清空timeout指针所指向的struct timeval结构,所以在每次调用select()之前也要重新填充timeout指针所指向的struct timeval结构。

使用select

select()机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件发生了可读或可写事件。

用法:

fd_set set;

FD_ZERO(&set); //将set清零使集合中不含任何fd

FD_SET(fd, &set); //将fd加入set集合

FD_CLR(fd, &set); //将fd从set集合中清除/

FD_ISSET(fd, &set);//在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。

另外,如果select调用中设置了等待时间,那么每次调用时都需要重新对这个时间赋值么?就像对fd_set处理一样。

select缺点:

(1)每次调⽤用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 ’

(3)select⽀支持的⽂文件描述符数量太小了,默认是1024

设计一个简单的select服务器:

#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>

int fds[sizeof(fd_set)*8];  

static void Usage(char*proc)
{
   printf("%s [local_ip] [local port]\n");
}
int startUp(char*ip,int port)
{
   int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(2);
    }
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(port));
    server.sin_addr.s_addr=inet_addr(ip);
    if(bind(sock,(struct sockaddr*)&server,sizeof(struct sockaddr_in))<0)
    {
        perror("bind");
        exit(3);
    }
    if(listen(sock,10)<0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}





int main(int argv,char*argc[])
{

    if(argv!=3)
    {
        Usage(argc[0]);
        return 1;
    }
    int listen_sock=startUp(argc[1],argc[2]);

    int nums=sizeof(fd_set)*8;     //位图
    fd_set rds;    //创建一个与文件句柄建立联系的数组
    //初始化-1代表没有文件描述符就绪
    int i=0;
    for(i=0;i<nums;i++)
    {
        fds[i]=-1;
    }

    while(1)
    {
      int max=-1;
      struct timeval timeout={5,0};
      //struct timeval结于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件
                //发则函数返回,返回值为0。
      fds[0]=listen_sock;

      for(i=0;i<nums;i++)
      {
          if(fds[i]>-1)
          {
              FD_SET(fds[i],&rds);  //在rdss设置所要关心的文件描述符对应的事件
          }
          if(max<fds[i])
          {
              max=fds[i];
          }
      }

      switch(select(max+1,&rds,NULL,NULL,&timeout))// 执行成功则返回文件描述词状态已改变的个数 
      {
          case 0:
              printf("timeout...\n");
              break;
          case -1:
              perror("select");
              break;
        default:
              for(i=0;i<nums;i++)
              { 
                  if(i==0 && FD_ISSET(fds[i],&rds))//判断listen_sock描述符上对应的事件是否就绪
                  {
                      struct sockaddr_in client;
                      socklen_t len=sizeof(client);
                      int  newsock=accept(listen_sock,(struct socketaddr*)&client,&len);
                      //继续取出将要关心的对应的文件描述符上的对应的事件
                      if(newsock<0)
                      {
                          perror("accept");
                          return 2;
                      }
                      else
                      {
                          int j=0;
                          for(j=0;j<nums;j++)
                          {
                              if(fds[j]==-1)
                              {
                                  break;
                              }
                          }
                          if(j==nums)
                          {
                              close(newsock);
                          }
                          else
                          {
                              fds[j]=newsock;
                              //将新的所要关心的文件描述符对应的事件放到fds合适的位置
                          }
                      }
                  }
                else if(i!=0&&FD_ISSET(fds[i],&rds))
                    //如果不是监听套接字但是是其他文件描述符对应的读事件就绪了
                  {
                      char buf[1024];
                      ssize_t s=read(fds[i],buf,sizeof(buf)-1);
                      if(s>0)
                      {
                          printf("client say:%s\n",buf);
                      }
                      else if(s==0)
                      {
                          printf("client quit!\n");
                          close(fds[i]);
                          fds[i]=-1;
                      }
                      else
                      {
                          perror("read");
                          close(fds[i]);
                          fds[i]=-1;
                      }
                  }
              }
      }
    }
    return 0;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
IO多路复用是一种IO模式,它利用内核提供的多路分离函数(如select、poll、epoll)来管理多个IO操作。其中,select函数是其中一种实现方式。使用select进行多路复用时,用户首先将需要进行IO操作的socket添加到select中,并阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回,用户线程可以继续执行读取数据等操作。这样,使用select函数可以在一个线程中同时处理多个IO请求,避免了轮询等待的问题。相比于同步阻塞模式,使用select函数可以提高效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [谈谈对IO多路复用的select机制的理解](https://blog.csdn.net/qq_34798605/article/details/128260171)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [IO多路复用之select](https://blog.csdn.net/qq_52836452/article/details/127324517)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值