Linux_网络_ 五种IO模型、非阻塞接口fcntl、IO多路转接之select,实例select回显服务器

IO一般分为两步进行的:

  1. 等待IO就绪。
  2. 拷贝IO数据到内核或外设。

1. 五种IO模型

  1. 阻塞IO:内核数据准备好之前,系统调用一直等待。所有套接字系统默认是阻塞模式。

  2. 非阻塞IO:数据未准备好,系统调用直接返回,并返回EWOULDBLOCK错误码。
    注意:非阻塞IO需要程序员循环的方式(轮询)读取文件描述符,需要耗费大量的CPU资源。阻塞的本质是进程被挂起。

  3. 信号驱动IO:内核将数据准备好后,使用SIGIO信号(29号信号)通知应用,由应用进程进行IO拷贝操作
    在这里插入图片描述
    提前注册信号函数,当收到信号时进入信号处理函数中处理。
    Linux进程信号

  4. IO多路转接:select、poll、epoll
    recv,read,write,send这些IO接口进行IO时,都先要等待数据就绪,再进行拷贝操作。
    将IO等待的时间交给多路转接函数(select、poll、epoll),当数据就绪时,再执行对应recv/read等函数。在进行对应的recv等操作,让上面这些函数能够专注于拷贝而不是等待

多路转接函数可以等待多个文件描述符,将很多等待的时间进行压缩。(一次等待多个文件描述符,任意一个就绪的概率增大,IO效率提高),详情看下面的介绍。

需要注意的是:
多路转接适合于有大量链接,但每个连接都不活跃的情况(聊天软件)。
连接很活跃不适合多路转接。直接非阻塞轮询IO效果更好。

  1. 异步IO:内核将数据拷贝完成后,通知应用程序(注意:与信号驱动拷贝相比,异步IO数据从内核拷贝到用户区是由系统进行的,用户空间直接进行IO操作即可。)

在这里插入图片描述
在这里插入图片描述

综上:

  1. 前四种IO方式都属于同步IO,最后一种属于异步IO。
  2. 同步:进程需要拷贝内核数据到用户区。异步:系统帮进程将内核数据拷贝到用户区
  3. IO分为两步,大部分情况等待数据就绪的事件要大于拷贝IO的时间,缩短IO等待时间是提高IO效率的核心方式。

2. 非阻塞IO接口(fcntl)

可以在open函数打开文件时设置为非阻塞,还可以将已经打开的文件描述符设置为非阻塞

在这里插入图片描述
fcntl所有功能如下

  • 复制一个现有的描述符(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)

将已经打开的文件描述符设置为非阻塞。是第三个功能

fd:设置的文件描述符。

cmd:文件描述符的属性。可以选择具体的功能。

arg:可变参数

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数。
这样就把一个已经打开的文件描述符设置非阻塞。

eg:设置0号文件描述符属性,让标准输出变成非阻塞,观察现象。

阻塞情况:

#include<iostream>
#include<unistd.h>
#include<fcntl.h>
int main(){
  while(true){
    char buff[1024]={0};
    ssize_t size=read(0,buff,sizeof(buff)-1);
    if(size<0){
      std::cerr<<"read error "<<size<<std::endl;
      break;
    }

    buff[size]='\0';
    std::cout<<"echo: "<<buff<<std::endl;
  }
  return 0;
}

在这里插入图片描述

设置非阻塞情况:
注意非阻塞需要轮询检测是否就绪,如果因为没有就绪而返回,errno会被设置为EAGAIN或EWOULDBLOCK。
在这里插入图片描述
在这里插入图片描述
注意:IO操作可能被某些信号终断,这时进程会收到EINTR信号,也需要考虑这种情况

#include<iostream>
#include<unistd.h>
#include<fcntl.h>

bool SetNoBlock(int fd){
  int tmp_fd=fcntl(fd,F_GETFL);//获取文件描述符属性
  if(tmp_fd<0){
    std::cerr<<"fcntl error"<<std::endl;
    return false;
  }
  else{
    fcntl(fd,F_SETFL,tmp_fd|O_NONBLOCK);//使用F_SETFL将文件描述符设置回去
    return true;
  }
}

int main(){
  SetNoBlock(0);
  while(true){
    char buff[1024]={0};
    ssize_t size=read(0,buff,sizeof(buff)-1);
    if(size<0){
      if(errno==EWOULDBLOCK||errno==EAGAIN){
        std::cout<<"errno: "<<errno<<std::endl;
        sleep(1);
        continue;
      }
      else if(errno==EINTR){
        //数据被信号中断
        std::cout<<"break of"<<std::endl;
        sleep(1);
        continue;
      }
      std::cerr<<"read error "<<size<<" errno: "<<errno<<std::endl;
      break;
    }
    buff[size]='\0';
    std::cout<<"echo: "<<buff<<std::endl;
  }
  return 0;
}

在这里插入图片描述

3. IO多路转接select接口分析(sys/select.h)

select函数功能是等待多个文件描述符,有一个数据等待完成时就通知进程。进程调用read、recv等IO接口时不会被阻塞。

程序会在select这里等待,直到被监视的文件描述符有一个就绪时。

在这里插入图片描述

  1. nfds:select在等待的多个描述符中,最大的文件描述符+1。对每个文件描述符进行检测。
  2. fd_set:是一个位图,可以将特定的文件描述符添加到位图中。
    FD_SET:将文件描述符设置到fd_set位图中。
    FD_ISSET:判断文件描述符是否在fd_set位图中。
    FD_ZERO:清除fd_set位图的所有位。
    FD_CLR:将特定的文件描述符清从fd_set中清除
  3. select是系统调用函数,参数传递给系统。这里以readfds读文件描述符这个位图为例
    fd_set是输入输出型参数,输入要系统监测的文件描述符。函数返回后fd_set保存的是就绪的文件描述符。
    用户调用select:传入readfds位图,让系统检测所有在位图中的文件描述符,是否是读就绪状态。有任意文件描述符就绪就返回。
    函数返回,系统到用户区:fd_set位图被系统修改,需要检测的文件描述符中,就绪的文件描述符被设置到这个位图中返回给用户区
  4. readfds,writefds,exceptfds:分别代表让操作系统监测读事件,写事件,异常(重点监测文件描述符出错)事件。
  5. seclect中的timeout是一个设置等待时间的结构体
    timeout=nullptr时select会阻塞等待
    timeout={0};非阻塞轮询
    timeout={a,b} a秒 b毫秒之后返回,无论是否有事件就绪
    在这里插入图片描述
  6. select返回值:正常返回就绪时文件描述符个数,返回0代表超时,出错返回-1错误原因存于errno。可能错误如下:
    EBADF 文件描述词为无效的或该文件已关闭
    EINTR 此调用被信号所中断
    EINVAL 参数n 为负值。
    ENOMEM 核心内存不足

select工作流程

在这里插入图片描述
所以套接字下的select多路转接的伪代码格式(以监测读事件为例)为下图:

int fds[sizeof(fd_set)*8];//select中fd_set最大可以监测的文件描述符
fd_set readfds;

int listen_sock=sock(...);
//套接字链接事件到来,在多路转接中都统一当作读事件就绪,如果没有accept,就认为listen_sock没有就绪
listen_sock add fds;
listen_sock set readfds;

int maxfd=0;

for(int i=0;i<fds.size();i++){
   //将有效的文件描述符添加到readfds位图上
   fds[i] set readfds;
   更新maxfd;
}

int ret=select(maxfd+1,&readfds,NULL,NULL,NULL);//阻塞等待
if(ret>0){
	//有事件就绪
	for(int i=0;i<fds.size();i++){
		if(fds[i] in readfds && fds[i]==listen_sock){
		    //链接事件就绪
		    int sock_fd=accept(...);
		    sock_fd add fds;
	    }
		else if(fds[i] in readfds){
			//内核告诉用户这个文件描述符是就绪的,读事件就绪
			read(...);
		}
	}
}

demo select回显服务器

简单套接字封装:sock.h

#pragma once

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

namespace NetWork_Sorket{
  class Sork{
    public:
      static int Socket(){
        //创建监听套接字
        int listenSock=socket(AF_INET,SOCK_STREAM,0);
        if(listenSock<0){
          std::cout<<"socket error"<<std::endl;
          exit(-1);
        }
        int opt=1;
        //设置套接字属性
        setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
        return listenSock;
      }

      static bool Bind(int listenSock,int port){
        //绑定,IP=INADDR_ANY
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(port);
        local.sin_addr.s_addr=INADDR_ANY;
        if(bind(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
          std::cout<<"bind error"<<std::endl;
          exit(-2);
        }
        return true;
      }

      static int Listen(int listenSock,int Len){//全连接队列长度
        //监听
        if(listen(listenSock,Len)<0){
          std::cout<<"listen error"<<std::endl;
          exit(-3);
        }
        return true;
      }
  };
}

sever:

#pragma once

#include"sock.h"

#define LISTEN_SIZE 5
#define RFDS_SIZE (sizeof(fd_set)*8)  //最大可以等待的套接字个数1024
#define DEF_FD -1 //默认无效套接字

#include<sys/select.h>
#include<vector>
#include<algorithm>

namespace Select{
  class SelectSever{
    private:
      int listenSork;
      int port;
    public:
      SelectSever(int _port):port(_port){
        listenSork=NetWork_Sorket::Sork::Socket();

      }

      void InitSever(){
        NetWork_Sorket::Sork::Bind(listenSork,port);
        NetWork_Sorket::Sork::Listen(listenSork,LISTEN_SIZE);
      }

      void Start(){
        fd_set rfds;//读文件描述符集
        std::vector<int>fd_Array(RFDS_SIZE,DEF_FD);//保存所有文件描述符,DEF_FD代表没有文件描述符。
        fd_Array[0]=listenSork;//将监听套接字写入数组第一个元素,之后将其写入到rfds让select等待链接就绪

        while(true){
          //对所有合法的文件描述符,每次循环重新设置到rfds

          FD_ZERO(&rfds);//每次处理后将rfds位图清空
          //遍历数组,将有效文件描述符设置到rfds
          for(auto& fd:fd_Array){
            if(fd==DEF_FD){
              continue;
            }
            else{
              //合法fd,添加到文件描述符集中
              FD_SET(fd,&rfds);
            }
          }
          int MaxFd=*(std::max_element(fd_Array.begin(),fd_Array.end()));//获取数组最大的文件描述符值
          //设定select时间参数(输入,输出参数),每次循环需要重新设定
          //struct timeval timeout={5,0};//每隔5秒一次
          /*
           * seclect中的timeout=nullptr时select会阻塞等待
           * timeout={0};非阻塞轮询
           * timeout={a,b}as bms之后返回,无论是否有事件就绪
           * */
          switch(select(MaxFd+1,&rfds,nullptr,nullptr,/*&timeout*/nullptr)){
            case 0://超时
              std::cout<<"over time"<<std::endl;
              break;
            case -1://等待出错
              std::cout<<"select error"<<std::endl;
              break;
            default://正常事件处理
              //std::cout<<"select!"<<std::endl;
              //事件处理,所有事件就绪情况在rfds中
              EventProc(rfds,fd_Array);
              break;
          }//end switch
        }//end sever
      }
      ~SelectSever(){
      }
    private:
      void EventProc(const fd_set& rfds,std::vector<int>&fd_Array){
        //判定特定的fd是否在rfds中,证明fd文件描述符已经就绪。
        for(auto&fd:fd_Array ){
          if(fd==DEF_FD){
            continue;
          }
          if(FD_ISSET(fd,&rfds)&&fd==listenSork){
            //监听套接字已经就绪
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int sock=accept(fd,(struct sockaddr*)&peer,&len);//不会阻塞
            if(sock<0){
              std::cout<<"accept error"<<std::endl;
              continue;
            }
            //链接建立后,还要判断sock文件描述符是否就绪,将sock放入select中让select等待数据就绪,服务器不需要阻塞
            //将文件描述符添加到fd_Array中,找到数组未使用的位置
            int peer_port=htons(peer.sin_port);
            std::string peer_ip=inet_ntoa(peer.sin_addr);
            std::cout<<"accept! "<<peer_ip<<" : "<<peer_port<<std::endl;
            std::vector<int>::iterator pos=find(fd_Array.begin(),fd_Array.end(),DEF_FD);
            if(pos==fd_Array.end()){//数组已满
              close(sock);//无法处理,直接关闭接受的sock
              std::cout<<"select sever is full ! close sock:"<<sock<<std::endl;
            }
            else{
              *pos=sock;
            }
          } 
          else{
            //处理正常的fd,先判断fd是否就绪
            if(FD_ISSET(fd,&rfds)){
              //读事件就绪,实现不阻塞的读
              char buff[1024]={0};
              ssize_t size=recv(fd,buff,sizeof(buff)-1,0);
              if(size>0){
                buff[size]='\0';
                std::cout<<"echo# "<<buff<<std::endl;
              }
              else if(size==0){
                std::cout<<"client quit!"<<std::endl;
                //数组对应位置设置为DEF_FD,关闭文件描述符
                close(fd);
                fd=DEF_FD;
              }
              else{
                std::cerr<<"recv error"<<std::endl;
                close(fd);
                fd=DEF_FD;
              }
            }
            else{//fd未就绪
              //...
            }
          }
        }//end for
      }//end fuction
  };
}
#include"sever.h"
#include<string>
#include<iostream>
#include<stdlib.h>

//  ./sever port

static void usrHelp(char*name){
  std::cout<<"UsrHelp: "<<name<<"+port "<<std::endl;
}

int main(int argc,char*argv[]){
  if(argc!=2){
    usrHelp(argv[0]);
    exit(-3);
  }
  Select::SelectSever*sever=new Select::SelectSever(atoi(argv[1]));
  sever->InitSever();
  sever->Start();
  return 0;
}

运行结果:
在这里插入图片描述

注意: 上述服务器存在很严重的问题。

  1. 因为上述服务器一次读取1024个字节,但是无法确定对端发送的数据是否被完全读完。
  2. 对端连续发送多条数据,服务器每次读取相同的大小,可能读不到完整的数据出现粘包问题。

解决方法:

  1. 需要定制客户端和服务器协议,TCP基于字节流,定制对应的读取规则。
  2. 给每一个文件描述符创建缓冲区。

这里为了练习select,不讨论这些复杂情况。

select函数的优缺点

缺点:

  1. select函数等待的文件描述符有上限,最大sizeof(fd_set*8)=1024个
  2. select需要和内核交互数据,涉及到很多数据的来回拷贝。当select管理的链接较多时,会因为拷贝导致效率降低。
  3. 每次调用select都需要重新添加文件描述符,select会多次遍历保存文件描述符的数组,效率下降。
  4. 系统监测文件描述符就绪时,系统需要遍历文件描述符表。当链接数目变多时,系统遍历成本变高。

优点:

  1. select可以同时等待多个文件描述符,只负责等待,由具体的accept,read,recv等函数完成具体的IO操作且不会被阻塞。IO效率提高

版权声明:本文为dodamce原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。