高级IO--select

高级IO:

IO概念 / 类型 / 处理流程 / 多路转接IO的实现模型

四种典型IO:

阻塞IO, 非阻塞IO, 信号驱动IO,异步IO

IO过程:

发起IO调用,等待IO条件就绪, 进行数据拷贝

 阻塞IO:发起IO调用,若IO就绪条件不满足,则一直等待

优点:流程简单,一个IO完成后进行下一个
缺点:无法充分利用资源,效率较为低下

 非阻塞IO:发起IO条件,若IO就绪条件不满足在,则报错返回

优点:相较于阻塞IO,对资源利用较为充分,可以利用就绪等待时间干点其他事情或者发起另一个IO调用
缺点:相较于阻塞IO,流程稍微复杂,IO调用需要循环发起(总不能因为当前没有就绪就不进行IO),IO不够实时

 信号驱动IO:定义IO就绪信号处理方式,在处理方式中进行IO请求,进程可以一直干其他事情,等到收到IO就绪信号的时候,会打断进程当前操作去处理IO

优点:相较于非阻塞IO,更加实时,资源利用更加充分
缺点:流程更加复杂,需要定义信号处理,既有主控流程又有信号处理流程,涉及到信号是否可靠的问题

 异步IO:IO顺序不确定,IO过程由系统完成,不自己进行。
  定义IO完成信号处理方式自定义,发起异步IO调用,告诉系统要完成什么功能,剩下的IO功能完全由系统完成,完成后通知信号通知进程

优点:对资源利用最为充分,以最高效率进行任务处理
缺点:资源消耗比较高,流程最为复杂

阻塞与非阻塞的区别:常用于讨论调用函数是否阻塞,表示这个函数无法立即完成功能时,是否立即报错返回

阻塞:为了完成一个功能,发起调用,若不具备完成功能的条件,则调用一直等待
非阻塞:为了完成一个功能,发起调用,若不具备完成功能的条件,则报错返回

同步与异步的区别:通常用于讨论功能的完成方式,表示一个功能是否是顺序化且由自己来完成的

同步:功能完成的流程通常是顺序化的,并且是进程自身完成的。
异步:功能完成的流程通常是不确定的,并且功能不是由进程自身完成的,由系统完成的。

异步的种类:异步阻塞 ----等着别人完成功能 / 异步非阻塞----不等待别人完成功能

同步好还是异步好:

同步流程简单,同一时间占用资源少
异步处理效率高,同一时间占用资源多

 多路转接IO:主要用于进行了大量的IO就绪时间监控,能够让我们的进程只针对就绪的IO进行IO操作

就绪事件:IO的就绪
 可读事件:一个描述符当前是否有数据可读
 可写事件:一个描述符当前是否可以写入数据
 异常事件:一个描述符是否发送了某些异常

只对就绪的描述符进行IO操作有什么好处?----避免阻塞,并且提高效率

1.在默认的socket中,例如tcp服务端只能于一个客户端的socket通信一次,因为我们不知道哪个为客户端新建的socket有数据到来或者监听socket有新连接,有可能就会对没有新连接到来的监听socket进行accept操作而阻塞或者对没有数据到来的普通socket进行recv阻塞
2.在tcp服务端中,将所有的socket设置为非阻塞,若没有数据到来,则立即报错返回,进行下一个描述符的操作,这种操作中,有一个不好的地方,对没有就绪时间的描述符进行了操作,降低了处理效率

如何实现多路转接IO:
 操作系统提供了三种模型:select模型,poll模型,epoll模型

select模型:

使用流程,接口介绍以及原理理解:

1.用户定义自己想要监控的事件的描述符集合(就绪IO事件有三种,但并不是每一个描述符都要监控所有事件),例如定义可读事件的描述符集合,将哪些描述符添加到这个集合中,就表示要对这个描述符监控可读事件,因此想要监控什么事件就定义什么集合

1.定义指定事件的集合 / 2.初始化集合 / 3.将需要监控这个事件的描述符添加到这个集合中
集合:fd_set结构体,结构体中只有一个成员,就是一个数组----作为位图使用。
向这个集合中添加一个描述符,描述符就是一个数字,添加描述符其实就是这个数字对应的bit置1,表示添加到集合中。
这个数组中有多少个bit或者说select最多能监控多少描述符,取决于_FD_SELECT,默认是1024.
void FD_ZERO(fd_set* set); -----清空指定描述符集合;
void FD_SET(int fd, fd_set* set);----将fd描述符添加到set集合中
2.发起调用,将集合中数据拷贝到内核中进行监控,监控采用轮询遍历判断方式进行。
int select(int ndfs, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
ndfs:所有集合中最大的那个描述符数值+1,为了减少内核中遍历次数
readfds:可读事件集合
writefds:可写事件集合
exceptfds:异常事件集合
timeout: select是一个默认阻塞操作(struct timeval{ tv_sec; // 秒 tv_usec //微秒), 若timeout = NULL 表示永久阻塞,直到有描述符就绪在返回,若timeout中的数据为0,则表示非阻塞,没有就绪则立即返回,若timeout有数据,若指定时间内没有描述符就绪则超时报错返回
返回值 : 返回值小于0,表示监控出错,返回值等于0,表示没有描述符就绪,返回值大于0表示就绪的描述符个数
3. select调用返回后,进程遍历了哪些描述符还在集合中,就可以知道哪些描述符就绪了哪些事件,进而进行对应操作。
其他操作:void FD_CLR(int fd, fd_set* set);----从set集合中移除fd描述符;
     int FD_ISSET(int fd, fd_set* set);----判断fd描述符是否在集合中

 1 #include <stdio.h>
 2 #include<unistd.h>
 3 #include<stdlib.h>
 4 #include<sys/select.h>
 5 
 6 int main(int argc, char* argv){
 7     struct timeval tv;
 8     while(1){
 9         //select(最大文件描述符+1, 可读事件集合, 可写事件集合, 异常事件集合, 超时等待事件);
 10         fd_set set;
 11         FD_ZERO(&set);
 12         FD_SET(0, &set);
 13         tv.tv_sec = 3;
 14         tv.tv_usec = 0;
 15         printf("Start monitoring\n");
 16         int ret = select(0+1, &set, NULL, NULL, &tv);//select返回时会删除集合中没有就绪的描述符
 17         if(ret < 0){
 18             perror("select error");
 19             return -1;
 20         }
 21         else if(ret == 0){                                                          
 22             perror("monitoring timeout");         
 23             continue;                
 24         }        
 25         printf("Descriptor ready or timeout waiting\n");
 26         if(FD_ISSET(0, &set)){                                                                                                                                                       
 27             printf("Start reading data\n");
 28             char buf[1024] = {0};     
 29             int ret = read(0, buf, 1023);
 30             if(ret < 0){  31                 perror("read error\n");                          
 32                 return -1;                                     
 33             }                                           
 34             printf("buf:%s\n", buf);
 35         }                                             
 36     }                                       
 37     return 0;                                                                                                  
 38 }    

封装一个select类

#include <cstdio>
#include <vector>
#include <sys/select.h>
#include "tcpsocket.hpp"

#define MAX_TIMEOUT 3000
class Select{
    public:
	        Select():_maxfd(-1){
            FD_ZERO(&_rfds);//初始化清空集合
        }//初始化操作
        bool Add(TcpSocket &sock) {
            int fd = sock.GetFd();
            FD_SET(fd, &_rfds);
            _maxfd = _maxfd > fd ? _maxfd : fd;//每次修改集合都要重新判断最大描述符
            return true;
        }//将sock中的描述符添加到保存集合中
        bool Del(TcpSocket &sock) {
            int fd = sock.GetFd();
            FD_CLR(fd, &_rfds);
            for (int i = _maxfd; i >= 0; i--) {
                if (FD_ISSET(i, &_rfds)) {//移除之后。从后往前判断第一个还在集合中的就是最大的
                    _maxfd = i;
                    break;
                }
            }
            return true;
        }//从保存集合中移除这个描述符,不再监控这个描述符
		//int select(int ndfs, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
        bool Wait(std::vector<TcpSocket> *list, int outtime = MAX_TIMEOUT) {
            struct timeval tv;
            tv.tv_sec = outtime / 1000;//outtime以毫秒为单位
            tv.tv_usec = (outtime % 1000) * 1000; //计算剩余的微妙
            fd_set set;
            set = _rfds;//不能直接使用_rfds,因为一旦_rfds被修改就需要外部重新添加描述符了
            int ret = select(_maxfd+1, &set, NULL, NULL, &tv);
            if (ret < 0) {
                perror("select error");
                return false;
            }if (ret == 0) {
                printf("wait timeout\n");
                return true;
            }
            for (int i = 0; i < _maxfd+1; i++) {
                if (FD_ISSET(i, &set)) {
                    //还在集合中证明是就绪的,因为没有额外保存描述符,因此只能从0开始判断
                    TcpSocket sock;
                    sock.SetFd(i);
                    list->push_back(sock);
                }
            }
            return true;
        }//进行监控,并且直接向外提供就绪的TcpSocket
    private:
        //保存要监控可读事件的描述符---每次监控使用的集合都是这个集合的复制版(select会修改集合)
        fd_set _rfds; 
        //每次监控都需要输入的最大描述符
        int _maxfd;
};

实现一个tcp并发服务器

//多路转发模型的并发服务器
#include<iostream>
#include"select.hpp"

int main(int argc, char* argv[]){
  if(argc != 3){
    std::cout << "Usage:./main ip port\n";
    return -1;
  }
  std::string ip = argv[1];
  uint16_t port = std::stoi(argv[2]);
  TcpSocket lst_sock;
  CHECK_RET(lst_sock.Socket());
  CHECK_RET(lst_sock.Bind(ip, port));
  CHECK_RET(lst_sock.Listen());

  Select s;
  s.Add(lst_sock);
  while(1){
    std::vector<TcpSocket> list;
    bool ret = s.Wait(&list);
    if(ret == false){
      continue;
    }
    for(auto sock : list){
      if(sock.GetFd() == lst_sock.GetFd()){
        //就绪的描述符和监听套接字描述符,表示需要获取新连接
        TcpSocket new_sock;
        ret = lst_sock.Accept(&new_sock);
        if(ret == false){
          continue;
        }
        s.Add(new_sock);
      }else{
        //就绪的描述符不是监听套接字,就是通信套接字,则进行recv
        std::string buf;
        ret = sock.Recv(&buf);
        if(ret == false){
          sock.Close();
          continue;
        }
        std::cout << "client say: " << buf << std::endl;
        std::cout << "sever say: ";
        std::cin >> buf;
        ret = sock.Send(buf);
        if(ret == false){
          sock.Close();
		      s.Del(sock);
		      continue;
        }
      }
    }
  }
  lst_sock.Close();
  return 0;
}

select优缺点分析:

1.select所能够监控的描述符数量有最大上限,取决于宏__FD_SIZE,默认是1024
2.select进行监控的原理,是在内核中进行轮询遍历,性能会随着描述符的增多而下降
3.select返回时移除集合中未就绪的描述符,每次监控都需要重新添加描述符,重新拷贝到内核
4.select只能返回就绪的描述符集合,无法直接返回就绪的描述符,需要用户进行遍历判断哪个描述符还在集合中才能确定是否就绪

1.select遵循posix标准,可以跨平台移植
2.select的超时等待时间设置,可以精细到微妙

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值