【网络篇】 多路复用模型 ---- select,poll以及epoll特性,优缺点分析以及适用场景(超详细讲解~~~)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

在以往模拟实现的tcp服务器,服务器的监听套接字会为每一个客户端创建一个新的套接字,用于与制定客户端通信进行通信,但因为不知道哪个描述符什么时候有数据到来,因此流程中固定先获取新建连接,然后接受数据,但是这样有可能会造成程序的阻塞~~

1.在没有新建连接时调用accept获取新建连接;
2.在制定客户端没有数据是调用recv接收数据;

以前的解决办法:多执行流(多进程&多线程),给每一个客户端通信都创建一个执行流负责通信,这样一个执行流不会影响其他客户端

但是这样方式,有一个缺陷就是,占用资源较多;此时就需要引入多路复用模型了·!!!


1、多路复用模型初识

多路复用模型,又名多路转接模型
功能: 针对大量描述符进行IO事件监控,让进程可以只针对就绪描述符进行IO操作,提高IO效率,避免针对未就绪描述符操作而导致效率降低或阻塞

针对前言中的情况,使用多路复用模型,首先其对大量IO事件进行监控,让进程可以知道哪个描述符有数据到来,也就是哪个就绪就去处理哪个!!!同时此时也就不用创建进程或线程,也就自然不会出现上述的阻塞现象!但是,并不是提升了效率,对于多执行流性质,其是充分利用了CPU资源,但多路复用,则是类似于一种单执行流形式~~~
(当然,多路转接模型也可以搭配线程池使用,哪个描述符就绪就把哪个描述符抛入线程池中进行处理~~)

多路转接模型的应用场景:
- 1.有大量描述符需要进行IO就绪事件监控,但同一时间只有少量活跃的场景;(或者搭配多执行流处理)因为同一时间活跃的非常多,那么排在后面的描述符,半天都得不到处理~
- 2.针对当描述符有收发数据的超时控制场景

而多转接模型的实现,有三种模式,分别是select模型,poll模型,epoll模型

2、select模型

select模型:就是进行大量的描述符事件监控

2.1、操作流程

  1. 针对不同的IO事件(可读事件,可写事件,异常事件),定义不同的描述符集合,如果要对哪个描述符监控哪个事件,就把这个描述符添加到对应集合中;
  2. 调用监控接口,将集合拷贝到内核中,进行轮询遍历进行监控,当有描述符就绪,或者监控超时都没有就绪,则监控返回;
    而监控呢,就是在内核中遍历一遍,没有就绪则把对应进程加载至内核的IO事件队列中,然后进程会陷入等待直至被唤醒,再次遍历集合判断哪个描述符是否就绪了指定事件;
    注:监控调用返回之前,会将集合中所有未就绪的描述符,从集合中移除(监控调用返回后,集合中只有就绪的描述符!)
  3. 调用返回,这时候判断哪个描述符还在哪个事件的描述符集合中,就表示哪个描述符就绪了对应事件,进而进行操作;

2.2、接口介绍

int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
参数:
nfds:几种事件的描述符集合最大的描述符+1;
readfds:可读事件描述符集合;  writefds:可写事件描述符集合;   exceptfds:异常事件描述符集合;   
timeout: struct timeval{uint32_t ntv_usec; uint32_t tv_sec}; 监控超时等待时间

返回值:成功返回就绪事件个数,出错返回-1;若没有描述符就绪则返回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);   //向set集合中添加fd描述符
void FD_ZERO(fd_set *set);         //初始化清空set集合

2.3、select模型的模拟实现类

#include "tcp_socket.hpp"                                                                                             
#include <vector>
#include <time.h>
#include <sys/select.h>
class Select{
  private:
    fd_set _rfds;//可读事件的描述符集合的备份,每次监控都是从这个集合复制拷贝一份出来监控(因为select存在对集合的修改)
    int _max_fd;
  public:
    //初始化成员变量
    Select()
      :_max_fd(-1)
    {
        FD_ZERO(&_rfds);
    }
    //将sock中的描述符fd,添加到rfds可读事件的描述符集合中
    bool Add(TcpSocket &sock)
    {
       int fd=sock.GetFd();
       FD_SET(fd,&_rfds);//将描述符添加至可读事件描述符集合中
       _max_fd=_max_fd>fd?_max_fd:fd;//重新设置最大的描述符
      return true;
    }
    //将描述符fd,从rfd集合中移除
    bool Del( TcpSocket& sock)
    {
       int fd=sock.GetFd();
       FD_CLR(fd,&_rfds);//从当前集合中移除最大的描述符,
      for(int i=_max_fd;i>=0;i--)
      {
          if(FD_ISSET(i,&_rfds))
          {
            _max_fd=i;
            break;
          }
      }
      return false;
    }
    //监控,返回就绪的描述符数组
    bool Wait(std::vector<TcpSocket> *array,int timeout=3000)
    {
       fd_set tmp=_rfds;//使用临时集合进行监控,因为select会修改集合中的描述符
       struct timeval tv;
       tv.tv_usec = (timeout%1000)*1000;
       tv.tv_sec = timeout/1000;
       int ret = select(_max_fd+1,&tmp,NULL,NULL,&tv);
       if(ret < 0)
       {
         perror("select error");
         return false;
       }
       else if(ret == 0)
       {
         printf("select timeout!\n");
         return false;
       }
       for(int i=0;i<=_max_fd;i++)
       {
         if(FD_ISSET(i,&tmp))
         {
          TcpSocket sock;
          sock.SetFd(i);
          array->push_back(sock);
         }
       }
       return true;
    }
};

2.4、select模型优缺点

优点
遵循posix标注,跨平台移植性较好(在其他平台下也阔以使用select实现多路转接);
缺点
1.能够监控的描述符数量有上限限制
select的监控集合,是一个数组,当做位图使用,因此能监控多少描述符,就决定有多少比特位,取决于这个宏 _FD_SETSIZE=1024;
2.select监控原理多次进行轮询遍历集合,会导致监控的描述符越多,效率就会越低(性能会随着描述符增多而降低);
3.select由于会修改集合,因此每次监控都需要重新添加描述符到集合中;
4.select返回的是一个就绪的描述符集合,而并不是直接给与就绪的描述符,因而需要用户遍历所有描述符查看哪个描述符还在集合中;

3、poll模型

poll模型,也是针对大量描述符进行监控,但是poll监控是为了每个描述符设置一个事件结构体;

3.1、操作流程

  1. 定义要监控的描述符事件结构体数组,向数组中添加需要监控的描述符以及事件信息;
    例:struct pollfd pfds[10]; pfds[0].fd=0; pfds[0].events=POLLIN;
  2. 调用监控接口开始监控,将需要监控的数据拷贝到内核中进行监控(监控原理也是多次轮询遍历);
    监控调用返回前,会将描述符实际就绪的事件,填充至revents成员中,若无就绪,则被置为0;
  3. 监控接口调用完毕后,遍历事件结构体数组,通过每个元素的revents成员,确定描述符就绪了什么事件,进行对应操作;

3.2、接口介绍

int poll(struct pollfd*fds , nfds_t nfds , int timeout);
参数:
fds:事件结构体数组的首地址;  nfds:数组中有效元素的个数;  timeout:监控超时时间(毫秒为单位);
返回值:
返回值为实际就绪的事件个数;  返回0时,表示监控超时;   返回值小于0时,则表示出错;:
struct pollfd{
	int fd;          这是要监控的描述符;
	short events;    针对这个描述符要监控的事件;    常用:POLLIN-可读;  POLLINT-可写(其都是些比特位)
	short revents;   监控调用返回后这个描述符实际就绪的事件;
};

3.3、接口的简单实用

 #include <stdio.h>
 #include <sys/poll.h>
 #include <unistd.h>
 #include <string.h>
 
 int main()
 {
 
   struct pollfd pfds[10];
   int poll_count=0;
   pfds[poll_count].fd=0;//表示监控的描述符是标准输入
   pfds[poll_count].events=POLLIN;//针对标准输入要监控的是可读事件
   poll_count++;
 
   while(1)
   {
     int ret=poll(pfds,poll_count,3000);
     if(ret<0)
     {
       perror("poll error");
       usleep(1000);
       continue;
     }
     else if(ret==0)
     {
       printf("poll timeout!\n");
      usleep(1000);
       continue;
     }
     for(int i=0;i<poll_count;i++)
     {
       if(pfds[i].revents & POLLIN)//pfds[i]就绪了可读事件
       {
         char buf[1024];
         read(i,buf,1023);
         printf("%d 描述符就绪,读取数据:%s\n",pfds[i].fd,buf);
       }
       else if(pfds[i].revents & POLLOUT)
       {
         //pfds[i]就绪了可写事件
       }
     }
 
   }
   return 0;
 }                                                                       

3.4、poll模型的优缺点

优点:
1.相对于select操作流程更为简单;
2.所能监控的描述符数量没有上限(描述符有多少对应结构体数组就可以定义多大);
缺点:
1.无法跨平台;
2.监控原理涉及多次轮询遍历,因此效率也会随着监控的描述符数量增多而降低;

4、epoll模型

epoll模型,针对大量描述符进行事件监控(被认为是linux2.6以后最好用的多路转接模型)

4.1、接口认识

创建句柄
int epoll_create(int size);  //在内核创建了struct eventpoll结构体,里面有两个重要成员---一棵红黑树&一个双向链表

参数:size ---- 所能监控的最大描述符,但在linux2.6.8之后被忽略,但是必须大于0,也就是没有了准确上限

返回值:成功返回epoll句柄 — 描述符;(也就是可通过该描述符找到该内核的结构体)

添加监控
int epol_ctl(int epfd, int cmd, int fd, struct epoll_event* ev);

参数:
epfd: epoll_create 返回的操作句柄;
cmd: 要进行的操作;

  • EPOLL_CTL_ADD(添加监控)
  • EPOLL_CTL_DEL (移除监控)
  • EPOLL_CTL_MOD (修改监控)

fd : 要操作的描述符;
ev: 要操作的描述符所对应的事件结构体;

返回值:成功返回0,失败返回-1;

其中参数中涉及的结构体
struct epoll_event{
	uint32_t events;//要监控的事件以及监控返回后保存实际就绪的事件
					//EPOLLIN - 可读事件   EPOLLOUT - 可写
	epoll_data_t data;//用户数据变量
};
typedef union epoll_data{
	void* ptr;
	int fd;
}epoll_data_t;
大致的思路就是描述符就绪了对应events事件,之后会返回一个联合体的data数据,进而完成响应操作;
开始监控
int epoll_wait(int epollfd, struct epoll_events * evs,int maxevents, int timeout);

参数:
epfd:epoll操作句柄
evs:事件结构体的首地址,用于存放就绪的事件信息;
maxevents:数组的大小或者元素个数(实则是设置一个上限,避免就绪事件过多数组空间不够而导致的错误);
timeout:监控的超时时间(毫秒为单位);
返回值:
成功返回就绪事件个数,出错返回-1,超时则返回0;

4.2、操作流程

1.首先通过epoll_create创建epoll句柄;
在这里插入图片描述
2. 向epoll添加监控----实则就是给内核的epoll结构体中的红黑树添加制定描述符的对应事件信息;
在这里插入图片描述
3.epoll_wait 开始监控;

  • epoll的监控默认是异步阻塞监控;

  • epoll开始监控,实则是告诉系统,可以开始监控了,监控过程由系统完成;

  • 系统在监控的时候为每一个描述符都定义了一个回调函数,回调函数的功能实则就是一旦描述符就绪了指定事件,则把描述符对应的事件结构添加到rdlist双向链表中;
    在这里插入图片描述

  • 此时进程只需要看rdllist双向链表是否为空,就可以确定是否有描述符就绪,而无需像select,poll那样轮询遍历链表;
    在这里插入图片描述

    • epoll_wait传入的数组在调用返回后,里面保存的都是就绪的描述符对应事件的结构体,返回值就是元素的个数;
    • 用户可针对事件结构体的evs[i].data.fd进行events事件的操作;

4.3、epoll类的模拟实现

 #include <sys/epoll.h>
 #include <time.h>
 #include <vector>
 #include <cstdlib>
 #include "tcp_socket.hpp"
 #define EPOLL_MAX 100
 class Epoll{
   private:
     int _epfd;//epoll操作句柄
   public:
     Epoll()
       :_epfd(-1)
     {
       _epfd=epoll_create(1);
       if(_epfd<0)
       {
         perror("epoll error");
         exit(0);
       }
     }
     //向epoll添加监控
     bool Add(TcpSocket sock)
     {
       struct epoll_event ev;
       ev.data.fd = sock.GetFd();
       ev.events = EPOLLIN;
         int ret = epoll_ctl( _epfd , EPOLL_CTL_ADD , sock.GetFd() , &ev);
         if(ret < 0){
           perror("epoll_ctl error");
           return false;
         }
         return true;
     }
     //解除对sock描述符的监控
     bool Del(TcpSocket &sock)
     {
        int  ret=epoll_ctl(_epfd,EPOLL_CTL_DEL, sock.GetFd(),NULL);
        if(ret<0)
        {
          perror("epoll_ctl error");
          return false;
        }
       return true;
     }
     bool Wait(std::vector<TcpSocket>* arry,int timeout=3000)
     {
       //int epoll_wait(int epollfd, struct epoll_events * evs,int maxevents, int timeout)
       struct epoll_event evs[EPOLL_MAX];
       int ret=epoll_wait(_epfd,evs, EPOLL_MAX, timeout);                                       
       if(ret<0)
       {
         perror("epoll_wait error");
         return false;
       }
       else if(ret==0)
       {
         printf("epoll timeout!\n");
         return false;
       }
       //epoll_wait返回值ret就是就绪事件的个数
       for(int i=0;i<ret;i++)
       {
         if(evs[i].events & EPOLLIN)//就绪事件的判断(可读)
         {
           TcpSocket sock;
           sock.SetFd(evs[i].data.fd);
           arry->push_back(sock);
         }
       }
       return true;
     }
    };

4.4、epoll的触发方式(面试会考哟~)

在添加介绍添加监控的epoll_ctl函数中的struct epoll_events事件结构体中的events中,存在一EPOLLET标志;
在这里插入图片描述
其表示设置epoll事件边缘触发方式,而epoll事件默认为水平触发;
而epoll的事件触发方式:

  • 水平触发(默认的触发方式)
    针对可读 : 接收缓冲区的达到低水位标记是就会触发事件,而低水位标记默认为1, 即缓冲区中有数据就会触发事件;
    针对可写:即发送缓冲区中有剩余空间就会触发事件;
  • 边缘触发(EPOLLET)
    针对可读:只有新数据到来才会触发事件,无论缓冲区中是否有数据;
    针对可写;发送缓冲区剩余空间大小从无到有才回触发事件;

边缘触发方式的思想是为了让程序员一次性把数据处理完毕,减少处理次数,以此提高效率(实际上提高并不明显);
而那么怎么才能一次性将数据处理完呢?
解决初思想:通常是循环读取数据,直至将缓冲区中的数据处理完;
那么等缓冲区中数据处理完毕后,recv会陷入阻塞,怎么解决呢?
因此边缘触发方式下,通常需要将描述符设置为非阻塞,让描述符的操作都成为非阻塞操作(若没有数据就报错返回);
而设置非阻塞,可通过fcntl接口实现~~;

int fcntl(int fd, int cmd, ... /* arg */ );     

1.获取描述符属性;

  • int flag=fctl( fd, F_GETFL, 0);
  1. 在原有属性的基础上新增非阻塞属性;
  • fcntl(fd,F_SETFD,flag | O_NONBLOCK);
    • O_NONBLOCK非阻塞属性,需要原有基础属性上进行修改;

边缘触发方式使用场景:
1. 想要尽量一次性处理所有请求,减少事件触发次数提高效率;
2. 避免水平触发模式下,因为半条数据没有处理而不断触发事件的情况;
缓冲区中数据不完整,因此想要等到新数据来了,再去取出完整数据进行处理;

4.5、epoll优缺点分析

  • 缺点:
    跨平移植性差;
  • 优点:
    1.所能监控的描述符数量没有上限制;
    2.监控原理是异步阻塞,让系统进行监控,系统为事件做了回调函数,将就绪事件添加到就绪链表,进程仅仅需用检验链表是否为空,就可判断是否有描述符就绪,而非轮询遍历操作,因此性能不会随着描述符增多而降低;(适用于超大量描述符的事件监控)
    3.直接返回就绪的事件信息,可以直接对就绪描述符进行指定事件的操作,不需要空遍历来判断哪个描述符就绪;

而select和poll相较epoll的优势,针对单个描述符的超时控制上,select和epoll更加简单轻便;

总结

以上就是博主关于多路转接模型以及实现的讲解;
希望能助铁子们,面试一臂之力~~~
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值