【校招 --阶段二 linux操作系统】高级IO多路转接之select

IO=等待+拷贝
读IO{读recv}=读时间就绪+内核数据拷贝到用户空间(将内核数据拷贝到TCP接受缓冲区)
写IO(写send)=写时间就绪+将用户空间数据拷贝拷贝到内核空间(将TCP发送缓冲区数据拷备到内核空间)
高效IO本质就是减少尽可能的减少等待时间的比重

五种IO模型

钓鱼例子说明:
钓鱼在大多数时间都在等待,只有一瞬间在钓鱼。所以在等待时间即相当于IO等待时间,钓鱼相当于拷贝

张三:当没有鱼上沟时一直在等待什么事都不做,只有鱼上钩了才动。他一直坐在位置上等待鱼上钩(阻塞时IO)
李四:他没有在鱼上钩内一直盯着鱼竿,而是拿着一本书再看,当检测到没有鱼上钩就去看书,但是在鱼没有上钩时一直就在检测,当检测到鱼上钩时再去管鱼(非阻塞式IO)
王五:王五哪里一个铃铛绑在鱼竿顶部,就去钓鱼,但是他不想李四去检测有没有鱼上钩。在没有鱼上钩时就去干自己的事情了,当铃铛响了,才去管鱼。他是等待铃铛响了,才去没有定时去检测,当铃铛响了再去钓鱼。(信号驱动IO)
赵六:他不是带了一只鱼竿来钓鱼,而是带了100只。所以他等待时间很少,绝大多数时间花在钓鱼了(多路复用,多路转接)(read承担的工作一是等待,二是数据拷贝,他在等待一个文件描述符。我们可以把等待的过程拆出来淡出设计可以同时等待多个文件描述符只要一个文件描述符就绪了就认为read时间就徐了-衍生出来的的接口有select pll epoll只负责等待,等待多个文件描述符)
前四个人中赵六鱼上钩钓鱼效率高,前四个IO方式称为同步IO,IO由自己完成
刘七:刘七雇了一个人去钓鱼,当装满一桶鱼就给刘七打电话来取鱼,在其他时间就去干自己的事情,只有当电话来了才去取鱼。他没有等待钓鱼,也没有钓鱼的过程,钓鱼等待时间和钓鱼过程是雇的那个人做的。(异步IO)IO由别人完成

同步Io和异步IO最大区别就是同步IO数据拷贝的是自己而异步IO数据拷贝是别人完成

select pll epoll只负责IO中的一件事情就是等待

阻塞时IO

在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

阻塞IO是最常见的IO模型.

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第1张图片
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
非阻塞IO:

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一
般只有特定场景下才使用.

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第2张图片
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU
信号驱动IO:

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第3张图片
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
IO多路转接:

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第4张图片
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第5张图片
当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往
往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.

阻塞式IO

头文件
#include
#include
函数原型

int fcntl(int fd, int cmd);
参数:fd文件描述符
cmd: 传入的cmd的值不同, 后面追加的参数也不相同.

fcntl函数有5种功能:

复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

一个阻塞式IO例子:

#include
#include
#include
using namespace std;
int main(){
  while(1){
    sleep(1);
  char buff[1024];
  int local=read(0,buff,sizeof(buff));
  if(local>0){
    buff[local]=0;
    cout<<"cout:"<<buff<<endl;
  }
  cout<<"缓冲区中没有了数据,正在等待。。。。。。。。"<<endl;
  }
  return 0;
}

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第6张图片
用read从stdin中读取数据,当有数据是可以直接输出数据。但是当缓冲区中没有数据了读条件没有满足,该线程就会阻塞式等待,一直等待到缓冲区中有数据。

基于fcntl, 我们实现一个函数, 将文件描述符设置为非阻塞.

void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

轮询方式读取标准输入

#include
#include
#include
#include
#include
#include
using namespace std;
void Fcntl(int local){

    int fcnt= fcntl(local,F_GETFD);
    if(fcnt<0){
      cout<<"fcntl filed!";
     }
    fcntl(local,F_SETFL,fcnt | O_NONBLOCK);
}
int main(){

  Fcntl(0);
  while(1){
sleep(2);
  char buff[1024];

  int local=read(0,buff,sizeof(buff));
  if(local>0){

    buff[local]=0;
    cout<<"cout:"<<buff<<endl;
  }
  //read读取条件不满足
  else if(local<0&&errno==EAGAIN){

cout<<"检测缓冲区中有没有数据。。。。。。。。。。"<<"local:"<<local<<endl;
  }else{
    //read读出错
    cout<<"read error"<<endl;
  }
  

  }
  return 0;
}

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第7张图片
【校招 --阶段二 linux操作系统】高级IO多路转接之select_第8张图片

当缓冲区中没有数据时主线程没有阻塞等待而是一直检测缓冲区中有没有数据,当有数据就会从内核缓冲区拷贝到用户缓冲区然后输出。
local为什么会是-1?

在这里插入图片描述

这里显示-1是一个错误,具体是什么错误会在errno中写入。只有当一个库函数失败时,errno才会被设置。当函数成功运行时,errno的值不会被修改,当read读取条件不满足时errn0的值为11。这个errno==EAGAIN表示的是你的read本来是非阻塞情况,现在没有数据可读,这个时候就会置全局变量errno为EAGINA,表示可以再次进行读操作。

如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

在linux进行非阻塞的socket接收数据时经常出现Resource temporarily unavailable,errno代码为11(EAGAIN),这是什么意思?这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。对非阻塞socket而言,EAGAIN不是一种错误。

IO多路转接之SELECT

系统提供select函数来实现多路复用输入/输出模型.

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

头文件#include
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:等待众多文件描述符中最大文件描述符值+1比如最大文件描述符就是9 nfds就是10
timeout:表示等待类型阻塞式等,非阻塞式等,隔一段时间等待。设置为NULL等待方式为阻塞式等
【校招 --阶段二 linux操作系统】高级IO多路转接之select_第9张图片
struct timeval 结构体一个是秒一个是微秒,将等待时间设置为0等待方式为非阻塞式方式,如果等待时间不为0就是每等待时间轮询一次。
readfds/writefds/exceptfds,:即使输入型参数,又是输出型参数,输入的是想告诉操作系统,输出是想告诉用户
输入的是告诉操作系统要等待哪些文件描述符如果读或者写条件满足就要返回,输出是想告诉用户那些文件描述符就绪了可以读或者写了。readfds/writefds/exceptfds对应的是读文件描述符,写文件描述符和异常文件描述符的集合;
文件描述符集,因为一次要等待多个文件描述符,所以一次等待了很多就徐的文件描述符所以就把这些就绪文件描述符添加到这里
fd_set是一个位图结构
提供了一组操作fd_set的接口, 来比较方便的操作位图.【校招 --阶段二 linux操作系统】高级IO多路转接之select_第10张图片

提供了一组操作fd_set的接口, 来比较方便的操作位图.

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

每一次调用select之前都要对readfds/writefds/exceptfds重新设定,因为在select之后的read或者write之后可能会改变readfds/writefds/exceptfds第一次传入的值。

select本质就是等待,就绪时间通知方式
返回值:大于0已经有文件描述符就绪了
等于0已经超时了
是-1这次等待是错误的

select本质本质就是在等待,等待实际是为了做条件检测,当底层的读或者写条件就绪了,就会通知上层可以读或者写了

socket就绪条件

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;(内核空间有足够空间可以从用户空间拷贝,写事件就绪)
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE
信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未读取的错误;

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;(用户空间有足够的数据供拷到内核空间,读事件就绪)
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
监听的socket上有新的连接请求;
socket上有未处理的错误;
已建立的套接字上,如果还有新的链接来请求建立链接请求,这个链接请求是以读事件告知给操作系统,然后操作系统告知给用户。

select的特点

可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件
描述符,则我服务器上支持的最大文件描述符是512*8=4096.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,

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

备注: fd_set的大小可以调整,可能涉及到重新编译内核

select缺点

每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小.

函数介绍

函数原型
ssize_t recv(int socket, void *buf, size_t len, int flags)
参数一:指定接收端套接字描述符;
参数二:指向一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
参数三:指明buf的长度;
参数四:一般置为0;
返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回接收数据的长度。

数据读取把数据从内核缓冲区拷贝到用户缓冲区,参数len期望读取多少字节。如果len时期望读取1024字节但是内核缓冲区中数据只有100字节单数不会阻塞,因为达到内核缓冲区读就绪的最低水位线,所以不会阻塞

简单的select服务器

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NUM sizeof(fd_set)*8
class SelectServer{
private:
  int post;
  int listen_sock;//监听套接字
int fd_set_arrar[NUM];//将要关注的套接字存进一个数组方便操作
public:
  SelectServer(int _post=8080){
  post=_post;
  }
  void Init(){
    //创建监听套接字
    listen_sock=socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock<0){
      cerr<<"socket error。。。"<<endl;
      exit(0);
    }
    struct sockaddr_in in_addr;
    in_addr.sin_family=AF_INET;
    in_addr.sin_port=htons(post);
    in_addr.sin_addr.s_addr=INADDR_ANY;
    int op=1;
    setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&op,sizeof(op));//端口复用

    if(bind(listen_sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
        cerr<<"bind error....."<<endl;
        exit(1);
        }
    if(listen(listen_sock,5)<0){
      cerr<<"listen error。。。"<<endl;
      exit(2);
    }
    for(int i=0;i<NUM;i++){
      fd_set_arrar[i]=-1;
    }
    fd_set_arrar[0]=listen_sock;//将监听套接字存进套接字数组方便更新
    
  }


  void severic(fd_set *fd){
    for(int i=0;i<NUM;i++){
      if(fd_set_arrar[i]==-1){
        //无效套接字
        continue;
      }
      //检测套接字数组中有没有就绪套接字
      //读就绪分为底层连接就绪和普通读就绪事件
      if(FD_ISSET(fd_set_arrar[i],fd)){
        //读条件就绪
  cout<<"获取到就绪套接字"<<fd_set_arrar[i]<<endl;
      if(fd_set_arrar[i]==listen_sock){
      //底层建立连接也是读操作
      //所以也要建测是不是监听套接字
      //来了一个链接事件
      //所以要获取链接
      struct sockaddr_in addr;
      socklen_t len=sizeof(addr);
      cout<<"监听套接字"<<fd_set_arrar[i]<<endl;
       int sock=accept(fd_set_arrar[i],(struct sockaddr*)&addr,&len);
       if(sock<0){
         cerr<<"获取连接失败"<<endl;

       }
       cout<<"get a new link..."<<endl;
       //将新建套接字添加到套接字数组
       Add_arrayy(sock);
      }
       else{
        //来了一个普通就绪读事件
      }
      }
      
      
    }
  }
  void Add_arrayy(int sock){
    int i=0;
    for(;i<NUM;i++){
      if(fd_set_arrar[i]==-1){
        break;
      }
    }
    if(i>=NUM){
      cout<<"fd_set_arrar hava full..."<<endl;
      close(sock);
    }else{
    fd_set_arrar[i]=sock;
    cout<<"sock :"<<sock<<"add arrar success"<<endl;
   }
  }
  void strar(){
    //因为服务器在启动时默认打开一个网络套接字,所以在maxfd子初始赋值为监听套接字的值
   // int  maxfd=listen_sock;
   // 
   int maxfd=-1;
    
    struct timeval timeout={5,0};//每5等待第一次
    while(1){
      fd_set rnfds;
      //清空rnfds
      
      FD_ZERO(&rnfds);
      //设置rnfds
    
      //每次对fdns重新设定
        cout<<"fd_set_array:";
      for(int i=0;i<NUM;i++){
        if(fd_set_arrar[i]!=-1){
        FD_SET(fd_set_arrar[i],&rnfds);
        //重新设定maxfd
        cout<<fd_set_arrar[i];
        if(maxfd<fd_set_arrar[i]){
          maxfd=fd_set_arrar[i];
        }

      }
    }
      cout<<endl;
     timeout={5,0};//因为既是输入型参数,又是输出型参数 如果等待超时返回值为0就会将等待时间置为0
    // 所以必须在每次select重新设置
    //
    cout<<"selsect begin"<<endl;
       switch(select(maxfd+1,&rnfds,NULL,NULL,NULL)){
         case 0:
           cout<<"等待时间超时"<<endl;
           break;
         case -1:
           cout<<"slect error"<<endl;
           break;
         default:
          severic(&rnfds);
           break;

       }
    }
  }   
  ~SelectServer(){
    close(listen_sock);
  }
};
void openov(string rip){
cout<<rip<<endl;
}
int main(int argc,char*argv[]){
  if(argc!=2){
  openov(argv[1]);
  exit(0);
  }
SelectServer *ss=new SelectServer(atoi(argv[1]));
ss->Init();
ss->strar();
  return 0;
}

【校招 --阶段二 linux操作系统】高级IO多路转接之select_第11张图片
总结:

一个select所接受的文件描述符是有上限的
每次都要进行文件描述符集重新添加,进而导致效率降低
从内核到用户空间,从用户空间到内核空间都要进行大量的数据拷贝
slect高效体现在等待减少等待时间

你可能感兴趣的:(校招,linux,linux,unix,select,高级IO)