我是靠谱客的博主 舒心口红,最近开发中收集的这篇文章主要介绍高级IO--5种及select、poll、epoll的原理、执行以及优缺点,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

高级IO:5种典型的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就绪事件:可读事件/可写事件/异常事件

例如:基本的TCP服务器程序,一个执行流中,既有accept,也有recv/send;然而每种操作都有可能在不满足条件的时候阻塞,若在大量的描述符中对一个没有就绪的描述符进行操作(对没有新连接的监听套接字调用accept/对没有数据到来的新的通信套接字recv都会导致流程阻塞,其他描述符就算就绪,也无法操作)

多路转接IO的实现:select/poll/epoll

监控的好处:让进程可以只针对就绪了指定事件的描述符进行操作,提高效率性能,避免了因为对没有就绪的描述符操作导致的阻塞。

select模型:

操作流程:

1.程序员定义某个事件的描述符集合(可读事件的描述符集合/可写事件的描述符集合/异常事件的描述符集合),初始化清空集合,对哪个描述符关心什么事件,就把这个描述符添加到相应事件的描述符集合中

2.发起监控调用,将集合拷贝到内核中进行监控,监控的原理是轮询遍历判断

可读事件的就绪:接收缓冲区数据大小大于低水位标记(量化标准--通常默认1个字节)

可写事件的就绪:发送缓冲区中剩余空间的大小大于低水位标记(量化标准--通常默认1个字节)

异常事件的就绪:描述符是否产生了某个异常

3.监控的调用返回,表示监控出错/有描述符就绪/监控等待超时

并且调用返回时,将事件监控的描述符集合中的未就绪描述符从集合中移除--(集合中仅仅保留就绪的描述符)

4.程序员轮询判断哪个描述符仍然还在哪个集合中,就确定这个描述符是否就绪了某个事件,然后进行对应事件的操作即可

select并不会直接返回给用户就绪的描述符直接操作,而是返回了就绪的描述符集合,因此需要程序员进行判断

代码操作:

1.定义集合--struct fd_set--成员只有一个数组--当作二进制位图使用--添加描述符就是将描述符的值对应的比特位置1

因此select能够监控的描述符数量,取决于二进制位图的比特位多少--而比特位多少取决于宏-FD_SETSIZE,默认等于1024

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

参数:

nfds          是需要监视的最大的文件描述符值+1,减少遍历次数;
rdset,wrset,exset       可读/可写/异常事件的描述符集合;
参数timeout为结构struct timeval{tv_sec;tv_usec;},用来设置select()的等待时间,决定select阻塞/非阻塞/限制超时的阻塞

若timeout为NULL,表示阻塞监控,知道有描述符就绪,或者监控出错才会返回;

若timeout中的成员数据为0,则表示非阻塞,监控的时候若没有描述符就绪,则立即超时返回

若timeout中成员数据不为0,则在指定时间内,没有就绪则超时返回

返回值:返回值大于0表示就绪的描述符个数;

            返回值=0表示没有描述符就绪,超时返回,

            返回值小于0表示监控出错

3.调用返回,返回给程序员,就绪的描述符集合,程序员遍历判断哪个描述符还在哪个集合中,就是就绪了哪个事件

int  FD_ISSET(int fd, fd_set *set);   // 用来测试描述set中相关fd 的位是否为真,fd是否在集合中

因为select返回时会修改集合,因此每次监控时候都要重新添加描述符

4.若对描述符不想监控了,则从集合中移除描述符

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

使用select对标准输入进行监控:

 1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <fcntl.h>
  5 #include <time.h>
  6 #include <sys/select.h>
  7 
  8 int main(){
  9 
 10   //对标准输入进行监控--标准输入描述符-0
 11   //1.定义指定事件的描述符集合
 12 
 13   fd_set rfds;
 14   while(1){
 15     printf("开始监控n");
 16     //select(maxfd+1,可读事件集合,可写事件集合,异常事件集合,超时时间)
 17     //开始监控,超时/有就绪则调用返回,返回的时候将集合中未就绪的描述符移除
 18     //超时:在tv指定的时间内都一直没有描述符就绪
 19     //有就绪:有描述符就绪的指定的事件
 20     struct timeval tv;//因为select会重置集合和事件结构,因此每次需要重新设置
 21     tv.tv_sec=3;                                                               
 22     tv.tv_usec=0;
 23     FD_ZERO(&rfds);//初始化清空集合
 24     FD_SET(0,&rfds);//将0号描述符添加到集合中
 25     int ret=select(0+1,&rfds,NULL,NULL,&tv);
 26     if(ret<0){
 27       perror("select errorn");
 28       return -1;
 29 
 30     }else if(ret==0){
 31       printf("wait timeoutn");
 32       continue;
 33     }
 34     if(FD_ISSET(0,&rfds)){//判断描述符是否在集合中,判断是否就绪了事件
 35       printf("准备从标准输入读取数据...n");
 36       char buf[1024]={0};
 37       int ret=read(0,buf,1023);
 38       if(ret<0){
 39         perror("read error");
 40         FD_CLR(0,&rfds);//从集合中移除描述符
 41         return -1;
 42       }
 43       printf("read buf:[%s]n",buf);
 44     }
 45 
 46 
 47   }
 48   return 0;
 49 }                 

 select.hpp

  1 #include <cstdio>                                                            
    2 #include <vector>
    3 #include <sys/select.h>
    4 #include <time.h>
    5 #include "tcpsocket.hpp"
    6 
    7 #define MAX_TIMEOUT 3000
    8 //通过类的实例化对象来实现select的简单操作
    9 class Select
   10 {
   11   public:
   12     Select():_maxfd(-1){
   13       FD_ZERO(&_rfds);
   14     }
   15     bool Add(TcpSocket &sock){//添加描述符的操作
   16       //获取到套接字描述符
   17       int fd=sock.GetFd();
   18       //添加到事件的描述符集合中
   19       FD_SET(fd,&_rfds);
   20       //判断重新确定当前集合中的最大描述符
   21       _maxfd=_maxfd>fd?_maxfd:fd;
   22       return true;
   23     }
   24 
   25     bool Del(TcpSocket &sock){//移除描述符的监控
   26       //获取到套接字描述符
   27       int fd=sock.GetFd();
   28       //移除到事件的描述符集合中
   29       FD_CLR(fd,&_rfds);
   30       //判断重新确定当前集合中的最大描述符
   31       if(fd!=_maxfd) return true;
   32       //假设集合中以前是8,8移除之后,从7开始判断,还在集合中的第一个就是最大      
              的
   33       for(int i=_maxfd-1;i>=0;i--){
   34         if(FD_ISSET(i,&_rfds)){
   35           _maxfd=i;
   36           break;
   37         }
   38       }
   39       return true;
   40     }
   41     //开始监控,转接向外部返回就绪的TcpSocket
   42     bool Wait(std::vector<TcpSocket> *list,int timeout=MAX_TIMEOUT){
   43       //select开始监控,定义超时时间,添加描述符到集合中
   44       struct timeval tv;
   45       tv.tv_sec=timeout/1000;
   46       tv.tv_usec=(timeout%1000)*1000;
   47       fd_set tmp_set=_rfds;//每次使用临时的集合进行监控
   48       int ret=select(_maxfd+1,&tmp_set,NULL,NULL,&tv);
   49       if(ret<0){
   50         perror("select error");
   51         return false;
   52       }else if(ret==0){
   53         list->clear();
   54         printf("wait timeoutn");
   55         return true;
   56       }
   57         //判断哪些描述符就绪了,组织TcpSocket对象,添加到list中
   58       //从0~maxfd逐个进行判断哪个数字在集合中哪个数据就是就绪的描述符的值
   59       for(int i=0;i<=_maxfd;i++){
   60         if(!FD_ISSET(i,&tmp_set)){
   61           continue;
   62         }
   63         TcpSocket sock;
   64         sock.SetFd(i);
   65         list->push_back(sock);
   66 
   67         return true;
   68       }
   69     }
   70   private:
   71     fd_set _rfds;//可读事件的描述符集合
   72     int _maxfd;//保存集合每次集合操作后的最大描述符
   73 };
   74                        
main.cpp
 1 #include "select.hpp"
  2 #include <iostream>                                                            
  3 //select实现的一个并发服务器
  4 int main(int argc,char *argv[]){
  5 
  6   if(argc!=3){
  7     printf("usage: ./main ip portn");
  8     return -1;
  9   }
 10   std::string srv_ip=argv[1];
 11   uint16_t srv_port=std::stoi(argv[2]);
 12   TcpSocket lst_sock;
 13   CHECK_RET(lst_sock.Socket());//创建套接字
 14   CHECK_RET(lst_sock.Bind(srv_ip,srv_port));//绑定地址信息
 15   CHECK_RET(lst_sock.Listen());//开始监听
 16 
 17   //多路转接IO
 18   Select s;
 19   s.Add(lst_sock);
 20 
 21   while(1){
 22     std::vector<TcpSocket>list;
 23     bool ret=s.Wait(&list);
 24     if(ret==false){
 25       return -1;
 26     }
 27     for(auto sock :list){
 28       //遍历就绪TcpSocket进行操作,获取新连接/接收数据
 29       if(sock.GetFd()==lst_sock.GetFd()){
 30         TcpSocket new_sock;
 31         bool ret=lst_sock.Accept(&new_sock);//获取新套接字
 32         if(ret==false){
 33           continue;
 34         }
 35         s.Add(new_sock);
 36       }else {
 37         std::string buf;
 38         ret=sock.Recv(&buf);//
 39         if(ret==false){
 40           sock.Close();
 41           continue;
 42         }
 43         printf("client say:%sn",buf.c_str());
 44         buf.clear();
 45         std::cout<<"server say: ";
 46         std::cin>>buf;
 47         ret=sock.Send(buf);
 48         if(ret==false){
 49           sock.Close();
 50           continue;
 51         }
 52       }
 53     }
 54   }
 55 
 56   lst_sock.Close();//关闭套接字
 57   return 0;
 58 }                                                                              

Select的优缺点分析:主要看应用场景

缺点:

1.select对描述符进行监控有最大数量上限,上限取决于宏--_FD_SETSIZE,默认大小1024.

2.在内核中进行监控,通过轮询遍历判断实现,性能会随着描述符的增多而下降

3.只能返回就绪的集合,需要进程进行轮询遍历判断才能得知哪个描述符就绪了哪个事件

4.每次监控都需要重新添加描述符到集合中,每次监控都需要将集合重新拷贝到内核中

优点:

遵循posix(可移植操作系统接口)标准,跨平台移植性比较好

poll模型:

操作流程:

1.定义监控的描述符事件结构体数组,将需要监控的描述符以及事件标识信息,添加到数组的各个节点中

2.发起调用开始监控,将描述符事件结构体数组,拷贝到内核中进行轮询遍历判断,若有就绪/等待超时则调用返回,并且在每个描述符对应的事件结构体中,标识当前就绪的事件

3.进程轮询遍历数组,判断数组中的每个节点中的就绪事件是哪个事件,决定是否就绪了以及如何对描述符进行操作

接口认识

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll监控采用事件结构体的形式

struct pollfd(int fd--要监控的描述符;short events--要监控的事件 POLLIN/POLLOUT; short revents--调用返回时填充的就绪事件;)

arry_fds:事件结构体数组,填充要监控的描述符以及事件信息

nfds:数组中的有效节点个数(数组可能很大,但需要监控的节点只有前nfds个)

timeout:监控的超时等待时间--单位:毫秒

返回值:返回值大于0表示就绪的描述符事件个数;返回值等于0表示等待超时;小于0表示监控出错

优缺点分析:

优点:

1.使用事件结构体进行监控,简化了select中三种事件集合的操作流程

2.监控的描述符数量,不做最大数量限制

3.不需要每次重新定义事件节点

缺点:

1.跨平台移植性差

2.每次监控依然需要向内核中拷贝监控数据

3.在内核中监控依然采用轮询遍历,性能会随着描述符增多而下降

epoll模型:Linux下最好用的性能最高的多路转接模型

操作流程:

1.发起调用在内核中创建epoll句柄epollevent结构体(这个结构体中包含很多信息,红黑树+双向链表)

2.发起调用对内核中的epollevent结构添加/删除/修改所监控的描述符监控信息

3.发起调用开始监控,在内核中采用异步阻塞操作实现监控,等待超时/有描述符就绪了事件则调用返回,返回给用户就绪描述的事件结构信息

4.进程直接对就绪的事件结构体中的描述符成员进行操作即可。

接口信息:

int epoll_create(int size); --创建epoll句柄

size:在linux2.6.2之后被忽略,只要大于0即可。

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

epfd:epoll_create返回的操作句柄

cmd:针对fd描述符的监控信息要进行的操作--添加/删除/修改  

EPOLL_CTL_ADD(注册新的fd到epfd)/EPOLL_CTL_DEL(删除一个fd)/EPOLL_CTL_MOD(修改已经注册的fd监听事件)

fd:要监控操作的描述符

ev:fd描述符对应的事件结构体信息

struct epoll_event{

uint32_t events;//对fd描述符要监控的事件-EPOLLIN/EPOLLOUT

unoin{int  fd; void *ptr;}data;//要填充的描述符信息

}

一旦epoll开始监控,描述符就绪了进程关心的事件,则就会给用户返回所添加的对应事件结构体信息,通过事件结构体信息中包含的描述符进行操作---因此第三个单数fd与结构体中的fd描述符通常是同一个描述符。

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

epfd:epoll操作句柄

evs: struct epoll_event结构体数组的首地址,用于接收就绪描述符对应的事件结构体信息

max_event:本次监控想要获取的就绪事件最大数量,不大于evs数组的节点个数,防止访问越界

timeout:等待超时时间,单位:毫秒

返回值:返回值大于0表示就绪的描述符事件个数;返回值等于0表示等待超时;小于0表示监控出错

epoll的监控原理:异步阻塞操作

监控由系统完成,用户添加监控的描述符以及对应事件结构体会被添加到内核的wvwntpoll的结构体中的红黑树中。

一旦发起调用开始监控,则操作系统为每个描述符事件做了一个回调函数,功能是当描述符就绪了关心的事件,则将描述符对应的事件结构体添加到双向链表中

进程自身,只是每隔一段时间,判断双向链表是否为NULL,决定是否有就绪

 

  1 #include <cstdio>                                                            
    2 #include<vector>
    3 #include<sys/epoll.h>
    4 #include <iostream>
    5 #include <stdlib.h>
    6 
    7 #define MAX_TIMEOUT 3000
    8 class Epoll
    9 {
   10   public:
   11     Epoll():_epofd(-1){
   12       //1.创建epoll
   13       _epfd=epoll_create(1);
   14       if(_epfd<0){
   15         perror("epoll create error");
   16         exit(-1);
   17 
   18       }
   19     }
   20     bool Add(TcpSocket &sock){
   21       //2.添加描述符监控事件信息
   22       //(1).获取描述符
   23       int fd=sock.GetFd();
   24       //(2).定义描述符对应的事件结构体
   25         //EPOLLIN-可读事件   EPOLLOUT-可写事件
   26         struct epoll_event ev;
   27       ev.events=EPOLLIN;
   28       ev.data.fd=fd;
   29       //(3).添加到内核中
   30       //epoll_ctrl(epoll句柄,操作类型,监控描述符,描述符事件结构)
   31       int ret=epoll_ctrl(_epfd,EPOLL_CTL_ADD,fd,&ev);
   32       if(ret<0){
   33         perror("epoll ctl add error");
   34         return false;
   35       }
   36       return true;
   37     }
   38     bool Del(TcpSocketk &sock){//移除epoll
   39       int fd=sock.GetFd();//获取描述符
   40       int ret=epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,NULL);//移除
   41       if(ret<0){
   42         perror("epoll ctl del error");
   43         return false;
   44       }
   45       return true;
   46     }
   47     //开始监控
   48     bool Wait(std::vector<TcpSocket> *list,int timeout=MAX_TIMEOUT){
   49       //(1)开始监控
   50       struct epoll_event evs[10];
   51       int nfds=epoll_wait(_epfd,evs,10,timeout);
   52       if(nfds<0){
   53         perror("epoll wait error");
   54         return false;
   55       }else if(nfds==0){
   56         printf("epoll wait timeoutn");
   57         list->clear();
   58         return true;
   59       }
   60       //(2)监控调用返回后,为每一个就绪的描述符组织Tcpsocket对象
   61       for(int i=0;i<nfds;i++){
   62         if(evs[i].events&EPOLLIN){//可读事件的操作
   63         TcpSocket sock;
   64         sock.SetFd(evs[i].data.fd);
   65         //(3)将Tcpsocket对象添加到list中,进行返回
   66         list->push_back(sock);
   67         }//else if()其他事件的判断操作
   68       }
   69       return true;
   70     }
   71   private:
   72     int _epfd;
   73 
   74 };                              

 epoll中就绪事件的触发方式

可读事件:接收数据的缓冲区数据大小大于水位标记,就会触发可读事件

可写事件:发送缓冲区剩余空间大小大于低水位标记,就会触发可写事件

低水位标记:基准衡量值,默认1个字节

边缘触发方式:EPOLLET

可读事件:只有新数据到来的时候,才会触发一次事件

可写事件:发送缓冲区中剩余空间从无到有的时候才会触发一次事件

边缘触发因为触发方式的不同,因此要求进程中事件触发进行数据接收的时候,要求最好能够一次将所有数据全部读取(因为剩余数据不会触发第二次事件,只有新数据到来才会触发)

然而循环读取能够保证读完缓冲区中的所有数据,但是在没有数据的时候会造成阻塞,因此边缘触发方式中,描述符的操作都采用非阻塞操作--非阻塞描述符操作在没有数据/超时情况下会报错返回:EAGAIN or EWOULDBLOCK

如何将描述符设置为非阻塞(描述符的所有操作都为非阻塞)

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

fd:指定的描述符

cmd:F——GETFL/F_SETFL---获取/设置一个描述符的属性信息--O_NONBLOCK-非阻塞属性

arg:要设置的属性信息/获取的属性信息  F_GETFL使用的时候,arg被忽略,默认设置即可

边缘触发:为了防止一些事件不断触发(接收数据后(按条取指定长度)),缓冲区留有半条,就会不断触发事件,这种情况要不然上层操作,将半条数据读取出来,外部维护,要不然就使用边缘触发,等待新数据到来数据完整在触发事件。

epoll的优缺点分析:

1.没有描述符监控数量的上限

2.监控信息只需要向内核添加一次

3.监控使用异步阻塞操作完成,性能不会随着描述符的增多而下降

4.直接向用户返回就绪的事件信息(包含描述符在内),进程直接可以针对描述符以及事件进行操作,不需要判断有没有就绪了

1.跨平台移植性差。

多路转接模型

使用场景:只要对描述符有(可读/可写/异常)事件监控的需求就都可以使用多路转接模型

适用场景:适用于对大量描述符进行监控,但是同一时间只有少量描述符活跃的场景

理解多路转接模型的并发(用户之间的)/多进程多线程的并发(基于操作系统时间片的均衡并发)不同之处:

多路转接模型可以和线程池搭配适用

最后

以上就是舒心口红为你收集整理的高级IO--5种及select、poll、epoll的原理、执行以及优缺点的全部内容,希望文章能够帮你解决高级IO--5种及select、poll、epoll的原理、执行以及优缺点所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(41)

评论列表共有 0 条评论

立即
投稿
返回
顶部