Linux------常见的几种典型IO(实现服务器并发)

典型IO

  • 阻塞IO

  • 非阻塞IO

  • 信号驱动IO

    • 定义IO信号回调, 当IO条件具备后, 操作系统发送信号通知进程, 直接进行IO
  • 异步IO

    • 为了IP发起调用, 但是IO由操作系统完成, 完成之后通过信号通知进程, 进程进行数据处理

Linux------常见的几种典型IO(实现服务器并发)_第1张图片

  • 阻塞:为了完成功能发起调用, 当前不具备完成条件 , 则调用一直等待
  • 非阻塞: 为了完成功能发起调用, 若当前不具备完成条件, 则调用直接报错返回
    • 两者区别: 发起一个调用是否能够立即返回
  • 同步: 为了完成功能发起调用, 当前不具备完成条件, 则调用一直等待, 指导功能完成后返回
  • 异步: 为了完成功能发起调用 , 但是功能的具体完成过程并不由自身完成—调研(异步IO—AIO—DIO)
    • 两者区别: 功能的完成是否由自身完成
  • 同步阻塞: 一直等待功能的完成
  • 同步非阻塞 : 循环判断是否能够完成功能 , 能够完成时 则进行具体操作
  • 异步阻塞: 等待别人完成功能
  • 异步非阻塞: 不等待别人完成功能, 功能完成后 通过信号来来通知
  • 同步好还是异步好? 视使用场景来定 :—优缺点分析
    • 同步流程控制更加简单 , 但是对资源的利用率不足(CPU的利用率) ;
    • 异步对资源的使用更加充分 , 但对流程控制更加复杂 , 同一时间占用的资源也更多
  • 就绪事件的判断:
    • 可读事件:接收缓冲区数据的大小大于低水位标记(默认一个字节)
    • 可写事件: 发送缓存区中空闲空间的大小是否大于低水位标记(默认一个字节)
    • 异常事件: 描述符是否发生了某些异常

多路转接IO(多路复用)

  • 对大量描述符进行事件监控,能够让用户直接对事件(可读/可写/异常)就绪的描述符进行操作;在网络通信中如果仅仅对就绪描述符进行操作, 则流程在一个执行流中就不会阻塞; 可以实现在一个执行流进行多个描述符并发操作
  • 多路转接IO的实现主要通过几种多路转接模型实现: select/poll/epoll
  • 适用场景:
    1. 对大量描述符进行监控, 但是同一时间只有少量活跃的场景
      1. 多线程/多进程的并发时基于Cpu的分时机制 完成—对于多个线程的并发处理是比较高效且公平
      2. 多路转接模型的并发:基于用户态的轮询需处理 , 同一时间处理大量的描述符就会不公平,因为他是一个一个处理的,处理完毕之后才会处理下一个,若果活跃描述符很多,则会造成最后一个等待时间过长.
select模型
  • 对大量描述符进行几种事件监控, 让用户能够仅仅对就绪描述符进行操作
  1. 实现流程
    • 用户定义事件集合 fd_set------三种事件集合(集合其实是一个位图) —向集合中添加描述符
      • 可读事件集合, 可写事件集合, 异常事件结合
      • 集合范围为1024, 所以最多监控1024个描述符
    • 调用select开始监控: 将集合拷贝到内核进行监控
      • 监控默认为阻塞操作/可设置
      • 监控:对集合中所有的描述符进行轮询遍历判断, 判断关心事件是否就绪
    • 若有某个描述符就绪了用户关心的事件或者阻塞超时了 , 则该返回了, 返回时候:从所有集合中将没有就绪的描述符移除, --返回给用户就绪的描述符集合
      • 在集合中将没有就绪的描述符对应的位置0
    • 用户拿到就绪描述符集合后, 通过遍历判断哪些描述符还在集合中 , 来判断获取到就绪的描述符 , 进而对其操作
    • 因为select从集合中移除了没有就绪的描述符 , 所以下次监控室需要重新添加所有描述符
  2. 接口:
    • int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);–对大量描述符进行监控-默认阻塞
      • nfds: 集合中最大描述符+1
      • readfds: 可读事件集合
      • writefds: 可写事件集合
      • exceptfds: 异常事件集合
      • timeout: NULL表示永远阻塞 ,直到有描述符就绪 , timeout.tv_sec—限时阻塞
        • 若返回值>0 表示就绪的描述符个数 , ==0 表示没有描述符就绪 , 等待炒熟; <0 监控出错
    • void FD_CLR(int fd, fd_set *set);--------从set集合中移除指定描述符fd
    • int FD_ISSET(int fd, fd_set *set);-------判断指定描述符fd是否在集合Set中
    • void FD_SET(int fd, fd_set *set);---------将指定fd描述符添加到set集合中
    • void FD_ZERO(fd_set *set);-------清空set集合内容
  3. 流程
    • 搭建tcp服务端
    • 在accept之前创建select监控集合, 并且将监听socket添加到可读事件集合中
    • 进行select监控, 当Select 返回 , 并且有就绪描述符 —返回值>0
      • 第一次只有监听socket就绪 , 因此肯定是监听socket就绪 , 则进行accept,返回一个新的socketfd
      • 将新的socketfd也添加到集合中进行监控
    • 若有事件就绪 —判断就绪的描述符是否是监听Socket , 是的话accept新socket添加监控, 否则recv
      • 这时候就和本身有两个描述符 , 当只有一个描述符就绪的时候, select会把没有就绪的给移除
    • 处理完成就绪后, 又进行监控, 为了避免丢失没有就绪的描述符监控 , 将所有描述符重新添加一遍
  4. 优缺点分析
    • Select所能监控的描述符数量是有限制的: FD_SETSIZE=1024
    • 每次监控都需要重新将监控集合拷贝到内核当中
    • 在内核中进行轮询遍历监控, 会随着描述符的增多而性能降低
    • 返回的是就绪集合 , 需要用户进行判断才能对就绪的描述符进行操作(无法直接对就绪描述符进行操作)
    • 因为每次返回时都会清空未就绪描述符合, 因此每次监控都需重新添加描述符集合
    • Select遵循posix标准 , 可以跨平台
poll模型
  1. 接口: int poll(struct pollfd *fds, nfds_t nfds, int timeout)
    • fds:时间结构数组 nfds:有效的节点个数 timeout超市等待时间(-1出错 0超时 >0等待的超时时间 毫秒)
  2. poll采用时间结构的方式对描述符进行事件监控 , 只需要一个时间结构数组, 将响应的描述符添加到数组的每一个结构节点的fd中,以及用户关心的时间添加到响应节点得events;
  3. 过程
    • 用户定义一个事件结构数组 , 然后将关心的描述符以及事件添加到数组中
    • 调用poll接口 , 将数组拷贝到内核进行监控: 在内核中进行轮询遍历监控
    • 当事件数组中有描述符事件就绪/等待超时 则poll返回 , poll返回时,将每个描述符就绪的事件添加到响应的revents中
    • 用户对数组中的每个节点的revents进行判断是否是就绪事件, 进而对其进行相应操作
  4. 优缺点分析
    • 缺点
      • linux独有无法跨平台
      • 依然需要将监控的描述符数组拷贝到内合中
      • 在内核中也是轮询遍历监控, 性能会随着描述符的增多而下降
      • 只是将就绪的事件放到了结构的revents中,只需要用户对整个集合进行遍历判断才能获取哪个描述符就绪
    • 优点
      • 没有描述符监控的数量上限的限制, 取决于硬件资源
      • 采用事件结构数组进行事件监控, 简化了select三种事件集合的操作流程
      • 不需要重新向集合中添加事件数组
epoll模型(Linux下性能最好的多路IO就需通知方法)
  1. 接口
    • int epoll_create(int size)-----size在linux2.6.2之后已经优化了 , 只需要大于1即可
      • 在内核中创建eventpoll结构, 结构中包含主要信息:(双向链表, 红黑树)
    • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
      • 向内核的eventpoll结构中的红黑树添加用户关心的描述符
      • op: add , mod , del
      • fd: 用户关心的描述符
      • event: 对fd描述符所要监控的事件
        • struct epoll_event{uint32_t events用户关心事件 ; union epoll_data_t data
      • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
        • 它是一个异步阻塞操作(对描述符的监控有操作系统来进行;
    1. 过程:
      • 调用epoll_creat接口在内核中创建eventpoll结构体
      • 当需要监控描述符时, 为描述符创建一个epoll_event事件结构,将描述符与对应事件结构添加到内核中eventpoll结构体中的红黑树
        • struct epoll_event{uint32_t events用户关心事件 ; union {void* ptr ,int fd}data;}
        • 为什么要用红黑树
          • 红黑树性能优越-----为什么性能好------
            • epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。
        • 调用epoll_wait接口开始监控
          • 监控室一个异步阻塞操作 , 监控有操作系统完成, 但是进程调用epoll_wait会等待监控的完成
          • 操作系统为每一个需要惊恐的描述符事件 , 都定义了一个事件回调. 当描述符就绪的时候自动调用回调函数 , 将就绪的描述符事件节点添加到eventpoll结构中的就绪双向链表中
          • epoll_wait只是每隔一段时间来判断就绪双向链表是否为空 , 判断是否有就绪
        • 若就绪双向链表不为空,则有就绪, 则将就绪的事件结构映射到用户调用的epoll_wait所传输的epoll_event事件结构数组中
        • 用户可直接通过传入的epoll_event事件结构数组, 直接对其内部data中的描述符进行操作
      1. 在events中:就绪事件EPOLLIN可读/EPOLLOUT可写 就绪触发方式:EPOLL LT水平触发/EPOLL ET边缘触发
        • 边缘触发:每次有新数据进来时才会进行触发
          • 对于可行读时间: 只有一次新数据到来的时候才会触发一次事件, 若数据没有读完 ,缓冲区中有剩余数据, 不会触发第二次, 只能等待下一次数据到来的时候才会触发就绪
          • 对于可写事件:只有缓冲区中剩余空间大小从没有变为有的时候才会触发一次事件
        • 水平触发:
          • 对于可写事件:只要发送缓冲区的剩余空间大小大于低水位标记就会触发事件
          • 对于可读事件, 只要接收缓冲区中的数据大小大于低水位标记就会触发事件
        • 对于可读事件 , 若采用了边缘触发,则需用户一次性将缓冲区中的数据全部读完 (因为剩下的数据不会就绪通知第二次)
      2. 优缺点分析
        • 优点
          • 采用事件结构方式监控, 简化多个监控集合的操作
          • 没有描述符数量上限的限制
          • epoll监控的事件只需向内核拷贝一次, 不需每次都拷贝
          • 监控采用异步阻塞, 在内核中进行事件回调方式 , 性能不会随着描述符的增多而降低
          • 直接返回就绪事件结构 ,用户可以通过就绪事件结构中的描述符直接操作 , 不需要做空遍历
        • 缺点
          • 不能跨平台
      3. 惊群问题
        • 当每个子进程都持有监听套接字时, 假设在某一时刻没有任何连接请求, 所有子进程 进入休眠状态, 这时又突然来了一个请求, 但是此时操作系统不知道该唤醒哪一个进程来处理, 索性就全部唤醒, 然而最后就只有一个进程获得处理权, 导致其他子进程都获取失败, 导致了资源浪费
简单的epoll模型实现
/*封装一个Epoll类, 向外提供简单接口实现描述符的监控
 * 是一个基于Epoll的并发服务器*/
#include 
#include 
#include 
#include 
#include 
#include "tcpsocket.hpp"

#define MAX_EPOLL 1024

class Epoll 
{
  public:
    Epoll()
    :_epfd(-1)
    {}
    ~Epoll()
    {}
    
    bool Init()
    {
      _epfd = epoll_create(MAX_EPOLL);
      if(_epfd <0 )
      {
        std::cerr << "create epoll error\n";
        return false;
      }
      return true;
    }
    bool Add(TcpSocket &sock)
    {
      struct epoll_event ev;
      int fd = sock.GetFd();
      ev.events = EPOLLIN ;
      ev.data.fd = fd;
      int ret = epoll_ctl(_epfd , EPOLL_CTL_ADD , fd ,&ev);
      if(ret < 0)
      {
        std::cerr << "add error\n";
        return false;
      }
      return true;

    }
    bool Del(TcpSocket &sock)
    {
      int fd = sock.GetFd();
      int ret = epoll_ctl(_epfd , EPOLL_CTL_DEL , fd , NULL);
      if(ret < 0)
      {
        std::cerr << "remove monitor error\n";
        return false;
      }
      return true;
    }
    bool Wait(std::vector<TcpSocket> &list , int timeout = 3000)
    {
      struct epoll_event evs[MAX_EPOLL];
      int nfds = epoll_wait(_epfd , evs , MAX_EPOLL , timeout);
      if(nfds < 0)
      {
        std::cerr << "epoll monitor error\n";
        return false;
      }
      else if(nfds == 0)
      {
        std::cerr << "epoll wait timeout\n";
        return false;
      }
      for(int i = 0 ; i < nfds ; i++)
      {
        int fd = evs[i].data.fd;
        TcpSocket sock;
        sock.SetFd(fd);
        list.push_back(sock);
      }
      return true;
    }
    
  private:
    int _epfd;
};

int main(int argc , char *argv[])
{
  if(argc != 3)
  {
    std::cout << "./tcp_epoll ip port\n";
    return -1;
  }
  std::string srv_ip = argv[1];
  uint16_t srv_port = atoi(argv[2]);
  TcpSocket lst_sock;
  CHECK_RET(lst_sock.Socket());
  CHECK_RET(lst_sock.Bind(srv_ip , srv_port));
  CHECK_RET(lst_sock.Listen());
  
  Epoll e;
  CHECK_RET(e.Init());
  CHECK_RET(e.Add(lst_sock));
  while(1)
  {
    std::vector<TcpSocket> list;
    if(e.Wait(list) == false)
    {
      sleep(1);
      continue;
    }
    for( int i = 0 ; i < list.size() ; i++ )
    {
      if(lst_sock.GetFd() == list[i].GetFd())
      {
        TcpSocket cli_sock;
        if(lst_sock.Accept(cli_sock) == false)
        {
          continue;
        }
        CHECK_RET(e.Add(cli_sock));
      }
      else{
        std::string buf;
        if(list[i].Recv(buf) == false )
        {
          list[i].Close();
          return -1;
        }
        std::cout << "list[i] say :" << buf <<"\n";
        buf.clear();
        std::cin >> buf;
        if(list[i].Send(buf) == false)
        {
          list[i].Close();
          return -1;
        }  
    }

    }
  }
  lst_sock.Close();
  return 0;
}

/*封装一个tcpsocket类向外提供简单接口,能够实现客户端服务端编程流程*/
/*1.创建套接字 2.绑定地址信息 3.开始监听/发起连接请求 4获取已完成链接 5发送数据 6接收数据 7关闭套接字*/
#include 
#include 
#include 
#include 
#include 
#include 
#define CHECK_RET(q) if((q) == false){return -1;}
class TcpSocket
{
  public:
    TcpSocket()
      :_sockfd(-1)
    {}
    ~TcpSocket(){
      Close();
    }
    bool Socket()
    {
      _sockfd = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);
      if(_sockfd < 0)
      {
        std::cerr << "scoket error\n";
        return false;
      }
      return true;
    }
    bool Bind(std::string &ip , uint16_t port)
    {
      struct sockaddr_in addr;
      addr.sin_family  = AF_FILE;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(&ip[0]);
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = bind(_sockfd , (struct sockaddr*)&addr , len);
      if(ret <0 )
      {
        std::cerr << "bind error\n";
        return false;
      }
      return true;
    }

    bool Listen(int backlog = 5)
    {
      int ret = listen(_sockfd , backlog);
      if(ret < 0 )
      {
        std::cerr << "listen error\n";
        return false;
      }
      return true;
    }

    bool Connect(std::string &ip , uint16_t port)
    {
      int ret;
      struct sockaddr_in addr;
      addr.sin_family = AF_FILE;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(&ip[0]);
      socklen_t len = sizeof(struct sockaddr_in);
      ret = connect(_sockfd , (struct sockaddr*)&addr , len);
      if(ret < 0)
      {
        std::cerr << "connect error\n";
        return false;
      }
      return true;
    }
    void SetFd(int fd)
    {
      _sockfd = fd;
    }
    int GetFd()
    {
      return _sockfd;
    }
    bool Accept(TcpSocket &newsock)
    {
      struct sockaddr_in addr;
      socklen_t len = sizeof(struct sockaddr_in);
      //accept是一个阻塞函数;
      //当已完成连接队列中没有新连接的时候就会阻塞
      int fd = accept(_sockfd , (struct sockaddr*)&addr , &len);
      if(fd < 0)
      {
        std::cerr << "accept error\n";
        return false;
      }
      //接收一般发生在服务端,获取一个新的链接,若获取失败则应获取下一个不应退出.
      newsock.SetFd(fd);
      return true;
    }
    bool Send(std::string &buf)
    {
      int ret = send(_sockfd , &buf[0] , buf.size() , 0);
      if(ret < 0)
      {
        std::cerr << "send error\n";
        return false;
      }
      return true;
    }
    bool Recv(std::string &buf)
    {
      //recv返回值为0不是为了表示返回值没有数据,而是为了表示链接断开(没有数据会阻塞)
      char tmp[4096] = {0};
      int ret = recv(_sockfd , tmp , 4096 , 0);
      if(ret < 0)
      {
        std::cerr << "recvfrom error\n";
        return false;
      }
      else if (ret == 0)
      {
        std::cerr << "peer shutdown\n";
        return false;
      }
      buf.assign(tmp, ret);
      return true;
    }

    bool Close()
    {
      if(_sockfd >= 0)
      {
        close(_sockfd);
        _sockfd = -1;
      }
      return true;
    }
  private:
    int _sockfd;
};

简单的select模型实现
/*演示select的最基本使用 , 对标准输入进行可读事件监控 , 就绪只有对其进行操作*/
#include 
#include 
#include 
#include 
#include 


int main()
{
  fd_set set;
  FD_ZERO(&set);//清空集合
  FD_SET(0 , &set);//将标准输入添加到监控
  int maxfd = 0;
  while(1)
  {
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    FD_SET(0 , &set);//每次都要重新添加所有描述符
    int nfds = select(maxfd + 1 , &set , NULL , NULL ,&tv);
    if(nfds < 0)
    {
      printf("select error\n");
      return  -1;
    }
    else if (nfds == 0)
    {
      printf("wait timeout\n");
      continue;
    }
    printf("input-------\n");
    //select返回的是就绪集合
    int i;
    for(i = 0 ; i <= maxfd ; i++)
    {
      char buf[1024] = {0};
      int ret = read(i ,buf , 1023);
      if(ret < 0)
      {
        printf("read error\n");
        return -1;
      }
      printf("get buf:[%s]\n", buf);
    }
  }
  return 0;
}

你可能感兴趣的:(Linux------常见的几种典型IO(实现服务器并发))