IO多路转接--select--poll--epoll

目录

一,五种IO模型

1,阻塞IO

2,非阻塞IO

3, 信号驱动IO

 4,IO多路转接

 5,异步IO

二,同步通信和异步通信

三,IO模型的设置

1,阻塞IO

2,非阻塞IO

3,I/O多路转接之select

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

2,函数原型

3,select特点

4,select服务端举例

5,select缺点

4,poll多路转接

5,epoll多路转接

        1,int epoll_create(int size)

        2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

        3.epoll_wait

6.epoll工作原理

epoll的优点(和 select 的缺点对应)

7,epoll代码示例


一,五种IO模型

1,阻塞IO

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

        

2,非阻塞IO

        在内核将数据未准备好之前,系统调用仍然会返回,并且返回EWOULDBLOCK错误码,非阻塞IO一般需要程序员以循环的方式反复读写文件描述符,这个过程称为轮询,这对CPU是极大的浪费,一般特定的场景使用。

3, 信号驱动IO

        在内核将数据准备好以后,通过SIGIO信号通知应用程序进行IO操作

 4,IO多路转接

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

 5,异步IO

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

小结:

        任何IO过程中都包含两个步骤,第一是等待,第二是拷贝,而在实际应用的过程中,等待时间远远高于拷贝的过程。让IO更高效,最核心的办法就是让等待的时间尽可能的长。

二,同步通信和异步通信

同步和异步关心的是消息通信机制

1,同步,在系统调用时,在没有的得到结果之前,该调用不返回,直到调用返回结果。

2,异步,调用在发出以后,这个调用就直接返回了,没有返回结果,调用者通过信号,状态,来通知调用者。

//此同步非互斥和同步里面的同步

1,进程/线程同步也是进程/线程之间直接的制约关系

2,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候。

三,IO模型的设置

1,阻塞IO

        一个文件描述符,默认都是阻塞IO。

2,非阻塞IO

        通过fcntl设置文件描述符,函数原型如下

#include <unistd.h>
#include <fcntl.h>
 
int fcntl(int fd, int cmd, ... /* arg */ );

传入的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).

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

 实现函数setnoblock()

void setnoblock(int fd)
{
    int f1=fctl(fd,F_GETFL);
    if(f1<0)
    {
        cerr<<"fcntl"<<endl;
        return ;
    }
    fcntl(fd,f_SETFL,f1|O_NONBLOCK);

}

a,使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).

b,然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数

  1 #include<iostream>
  2 #include<unistd.h>
  3 #include<fcntl.h>
  4 
  5 void setnoblock(int fd)
  6 {
  7   int f1=fcntl(fd,F_GETFD);
  8   if(f1<0)
  9   {
 10     std::cerr<<"fcntl"<<std::endl;
 11     return ;
 12   }
 13   fcntl(fd,F_SETFD,f1|O_NONBLOCK);
 14 
 15 }
 16 int main()
 17 {
 18   setnoblock(0);
 19   while (1) {
 20       char buf[1024] = {0};
 21       ssize_t read_size = read(0, buf, sizeof(buf) - 1);
 22     if (read_size < 0) {
 23       std::cout<<"read"<<std::endl;
 24        sleep(1);
 25        continue;
 26         }
 27     std::cout<<buf<<std::endl;                                                                                                      
 28  }
 29   return 0;
 30 }

3,I/O多路转接之select

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

        select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;

        程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

         select时间复杂度---》O(n)

2,函数原型

       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

        1,nfds,代表最大文件描述符+1。

        2,rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;

        3,参数timeout为结构timeval,用来设置select()的等待时间

                a,参数timeout取值

                NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

                b,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的全部位

3,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的第一个参数

4,select服务端举例

#include"sock.hpp"
#define NUM 1024
#define DFL_FD -1
namespace so_sever
{
  class Select_Sever
  {
    private:
      int listen_sock;
      unsigned short port;
    public:
      Select_Sever(unsigned short _port):port(_port)
      {}
      //先初始化fd,起任务,填充select变量
      void InitSelectSever()
      {
       listen_sock= so_Sock::so_sock::Socket();
       so_Sock::so_sock::Bind(listen_sock,port);
       so_Sock::so_sock::Listen(listen_sock);
      }
      void Run()
      {
        fd_set rfds;
        int fd_array[NUM]={0};
        //让数组中所有数据都变成-1,然后填充对应的监听套接字
        clearArrar(fd_array,NUM,DFL_FD);
        fd_array[0]=listen_sock;
        for( ; ;)
        {
          //select时间也需要设置,输入输出型参数
          struct timeval timeout={5,0};

          //对所有的合法fd重新设置
          int maxfd=DFL_FD;
          FD_ZERO(&rfds);//对select中读描述符进行重新设置
          //对文件描述符数组进行判断
          for(int i=0;i<NUM;i++)
          {
            if(fd_array[i]==DFL_FD)
            {
              continue;
            }
           //合法的文件描述符 
           FD_SET(fd_array[i],&rfds);
           if(fd_array[i]>maxfd)
           {
             maxfd=fd_array[i];
           }
          }
           //select 阻塞等待
           
           switch (select(maxfd+1,&rfds,nullptr,nullptr,/*&timeout*/ nullptr))
           {
             case 0:
               std::cout<<"timeout....... "<<timeout.tv_sec<<std::endl;
              break;
             case -1:
               std::cout<<"select error "<<std::endl;
              break;
             default:
             // std::cout<<"select wait success"<<std::endl;
              Hander(rfds,fd_array,NUM);
              break;
           }//end switch 
          }//end for
        }
      void Hander(const fd_set &rfds,int fd_array[],int num)
      {
        //读取套接字
        //如何判断套接字已经等待成功 在fd数组里&&rfds里面这个已经存在
        for(int i=0;i<num;i++)
        {
          if(fd_array[i]==DFL_FD)
          {
            continue;
          }
          //说明这个文件描述符已存在
          if(FD_ISSET(fd_array[i],&rfds) && fd_array[i]==listen_sock)
          {
              //说明等待成功
              //接受套接字等待成功,读事件还没有就绪
              struct sockaddr_in peer;
              socklen_t len=sizeof(peer);
              //这里会不会阻塞,不会,已经有套接字加入到数组里面,
              int sock=accept(fd_array[i],(struct sockaddr*)&peer,&len);
              if(sock<0)
              {
                std::cout<<"accept error"<<std::endl;
                continue;
              }
              //端口转换
              uint16_t peer_port=htons(peer.sin_port);
              //ip转换
              std::string peer_ip=inet_ntoa(peer.sin_addr);

              std::cout<<"get a new link "<<" port "<<peer_port<<" ip "<<peer_ip<<std::endl;
              //走到这里 能否读取数据??  不能 recv是IO,select只是等
              //要将文件描述符添加到fd——fd_array  
              if(!AddFdTorray(fd_array,num,sock))
              {
                //说明没添加成功
                close(sock);
                std::cout << "select server is full, close fd : " << sock << std::endl;
              }
          }//end if 
          else 
          {
            //说明可以读取数据了
            if(FD_ISSET(fd_array[i],&rfds))
            {
              //是一个合法的fd,并且可以读取了
          //是一个合法的fd,并且已经就绪了,是读数据事件就绪
          //实现读写,会阻塞吗??绝对不会
            char buffer[1024];
          //能确定你读完了请求吗???
          //如果我一条链接给你发了多个请求数据,但是每个都只有10字节, 粘包?
          //如果没有读到一个完整的报文,数据可能丢失
          //这里我们怎么保证自己能拿到完整的数据呢??
          //1. 定制协议
              //2. 还要给每一个sock定义对应的缓冲区
              //ssize_t s=read(fd_array[i],buffer,sizeof(buffer)-1);
              ssize_t s=recv(fd_array[i],buffer,sizeof(buffer)-1, 0);
              if(s>0)
              {
                buffer[s]=0;
                std::cout<<buffer<<std::endl;
              }
              else if(s == 0)
              {
                std::cout<<"  client close "<<std::endl;
                //对端关闭
                close(fd_array[i]);
                fd_array[i]=DFL_FD;//清除文件描述符
              }
              else 
              {
                  std::cout<<" recv error "<<std::endl;
                  close(fd_array[i]);
                  fd_array[i]=DFL_FD;
              }
            }
            else 
            {
              //todo
            }
          }//end if

         }//end for
      }
      ~Select_Sever()
      {}
    private:
      void clearArrar(int fd_array[],int num,int default_fd)
      {
        for(int i=0;i<num;i++)
        {
          fd_array[i]=default_fd;
        }
      }
      bool AddFdTorray(int fd_array[],int num,int sock)
      {
        for(int i=0;i<num;i++)
        {
          if(fd_array[i]==DFL_FD)
          {
            fd_array[i]=sock;
            return true;
          }
        }
        return false;
      }

  };
}

5,select缺点

        每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.

        每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

        select支持的文件描述符数量太小

4,poll多路转接

        poll==>时间复杂度O(n),

        poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

        poll与select函数基本一致,只是把事件集合封装了一下,使得提前处理参数,后续也不用每次轮询时查找。

#include"sock.hpp"
#include<poll.h>

namespace Poll_etta
{
  class poll_sever
  {
    private:
      int listen_sock;
      unsigned short port;
    public:
      poll_sever(int _port):port(_port)
      {}
      //初始化
      void InitSever()
      {
         listen_sock=so_Sock::so_sock::Socket();
         so_Sock::so_sock::Bind(listen_sock,port);
         so_Sock::so_sock::Listen(listen_sock);
      }
      //RUN 任务
      void Run()
      {
        struct pollfd rfds[64];
        //初始化参数
        for(int i=0;i<64;i++)
        {
          rfds[i].fd=-1;
          rfds[i].events=0;   //我所关心的事件
          rfds[i].revents=0;  //操作系统对我关心的事件 做出回应
        }
        //填充我所关心的事件
        rfds[0].fd=listen_sock;
        rfds[0].events=POLLIN;//关心读事件
        rfds[0].revents=0;//内核填充
        for(; ;)
        {
          switch(poll(rfds,64,-1))
          {
            case 0:
              std::cout<<"time out"<<std::endl;
              break;
            case -1:
              std::cerr<<"poll error"<<std::endl;
              break;
            default:
            //处理逻辑,有事件到来
              for(int i=0;i<64;i++)
              {
                if(rfds[i].fd==-1)
                {
                  continue;
                }
                if(rfds[i].revents&POLLIN)
                {
                  //能accept吗  不能  要填充就绪事件
                  if(listen_sock==rfds[i].fd)
                  {
                    std::cout<<" get a new link"<<std::endl;
                  }
                  else 
                  {
                    //recv数据
                  }
                }
              }
              break;

          }
        }

      }
      ~poll_sever()
      {}
  };
}

5,epoll多路转接

        手册上说:为处理大批量句柄而作改进的POLL,

        其主要有三个函数接口。

        1,int epoll_create(int size)

                创建出一个epoll的句柄,size一般被忽略。

                调用完成以后,必须close掉。

        2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

                epoll的事件注册函数.

                       它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注                           册要监听的事件类型.

                      第一个参数是epoll_create()的返回值(epoll的句柄).

                      第二个参数表示动作,用三个宏来表示.

                      第三个参数是需要监听的fd.

                      第四个参数是告诉内核需要监听什么事.

               第二个参数的取值:

                        EPOLL_CTL_ADD :注册新的fd到epfd中;

                        EPOLL_CTL_MOD :修改已经注册的fd的监听事件;

                        EPOLL_CTL_DEL :从epfd中删除一个fd;

                struct epoll_event结构如下:

events可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);

EPOLLOUT : 表示对应的文件描述符可以写;

EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); EPOLLERR : 表示对应的文件描述符发生错误;

EPOLLHUP : 表示对应的文件描述符被挂断;

EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.

EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里. 

        3.epoll_wait

                int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

        收集在epoll监控的事件中已经发送的事件.

        参数events是分配好的epoll_event结构体数组. epoll将会把发生的事件赋值到events数组中           (events不可以是空指针,内核只负责把数据复制到这个

        events数组中,不会去帮助我们在用户态中分配内存). maxevents告之内核这个events有多           大,这个 maxevents的值不能大于创建epoll_create()时的size.

        参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).

       如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小        于0表示函数失败

6.epoll工作原理

       当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有         两个成员与epoll的使用方式密切相关。

        每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象添            加进来的事件

        这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来            (红黑树的插入时间效率是lgn,其中n为树的高度).

        而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的            事件发生时会调用这个回调方法.

        这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.

        在epoll中,对于每一个事件,都会建立一个epitem结构体

struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
} 

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.

如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1)

总结一下, epoll的使用过程就是三部曲:

        调用epoll_create创建一个epoll句柄;

        调用epoll_ctl, 将要监控的文件描述符进行注册;

        调用epoll_wait, 等待文件描述符就绪

epoll的优点(和 select 的缺点对应)

1,接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开 数据拷贝轻量: 只在合适的时候调用 2,EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)

3,事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.

4,没有数量限制: 文件描述符数目无上限

7,epoll代码示例

        

#include"sock.hpp"
#include<sys/epoll.h>

#define MAX_NUM 64
namespace  ns_epoll
{
  class EpollSever
  {
    private:
        int epfd; //epoll_ctl 的第一个参数,表示当前有几个文件描述符存在epoll里面
        int listen_sock;
        uint16_t port;
    public:
        //构造
        EpollSever(uint16_t _port):port(_port)
        {}
        //初始化套接字
        void InitSever()
        {
          listen_sock=so_Sock::so_sock::Socket();
          so_Sock::so_sock::Bind(listen_sock,port);
          so_Sock::so_sock::Listen(listen_sock);
          //监听完毕,打印出来kankan
          std::cout<<"deBug test "<<" Listen sock "<<listen_sock<<std::endl;
          //初始化epoll函数的第一个参数
          if((epfd=epoll_create(256))<0)
          {
            //创建失败
            std::cout<<"epoll create fail"<<std::endl;
            exit(4);
          }
        }
        void AddEvent(int sock,uint16_t event)
        {
          struct epoll_event ev;
          ev.events=0;//初始化
          ev.events|=event;
          ev.data.fd=sock;

          //添加等待队列失败,继续等待
          if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0)
          {
            std::cout<<" epoll_ctl add fail "<<" sock "<<sock<<std::endl;
          }
        }
        void DeleteEvent(int sock)
        {
          if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0)
          {
            std::cout<<" delete events fail "<<std::endl;
          }
        }
        //事件跑起来
        void Run()
        {
          //走到这里,至少有一个套接字,把套接字加到等待队列中,让epoll_wait “等”个文件描述符就绪
          AddEvent(listen_sock,EPOLLIN);
          int timeout=-1;
          struct epoll_event revs[MAX_NUM];//定义最大等待数
          //循环等待
          for(;;)
          {
            //返回值代表有几个事件准备好
            int num=epoll_wait(epfd,revs,MAX_NUM,timeout);
            if(num>0)
            {
                //说明等待成功,但是那个文件描述符就绪 不知道 遍历
                for(int i=0;i<MAX_NUM;i++)
                {
                   int sock = revs[i].data.fd;
                   if(revs[i].events & EPOLLIN)//读事件就绪
                   {
                     if(sock==listen_sock)
                     {
                       //连接事件就绪
                       struct sockaddr_in peer;
                       socklen_t len=sizeof(peer);
                       int sk=accept(sock,(struct sockaddr*)&peer,&len);
                       if(sk<0)
                       {
                         std::cout<<"accept fail "<<std::endl;
                         continue;
                       }
                       //得到一个新连接
                       std::cout<<"get a new link "<<std::endl;
                       AddEvent(sk,EPOLLIN);
                     }
                   //彻底就绪,直接读取文件
                   else 
                   {
                      char buffer[1024];
                      ssize_t  s=recv(sock,buffer,sizeof(buffer)-1,0);
                      if(s>0)
                      {
                        buffer[s]=0;
                        std::cout<<buffer<<std::endl;
                      }
                      else 
                      {
                        std::cout<<"Client close "<<std::endl;
                        close(sock);
                        //移除这个套接字在epfd中
                        DeleteEvent(sock);
                      }
                   }
                }
                else if(revs[i].events&EPOLLOUT)
                {
                  // todo 
                }
                else 
                {
                  //do some thing 
                }
              }
            }
            else if(num==0)
            {
              std::cout<<" time out"<<std::endl;
            }
            else 
            {
              std::cout<<" epoll error "<<std::endl;
            }
          }

        }
       ~EpollSever()
       {
         if(listen_sock>=0)close(listen_sock);
         if(epfd>=0)close(epfd);
       }

  };
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想找后端开发的小杜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值