select epoll,高级IO--5种及select、poll、epoll的原理、执行以及优缺点

 2023-09-25 阅读 18 评论 0

摘要:高级IO:5种典型的IO模型/多路转接模型---三种实现 四种IO模型:阻塞IO/非阻塞IO/信号驱动IO/异步IO select epoll?IO的过程:发起IO调用,等待IO条件就绪,然后将数据拷贝到缓冲区进行处理---等待/拷贝 阻塞IO:为了完成IO,发起调用,若当前不具备IO条件,则一直等待 类似于钓鱼,没有

高级IO:5种典型的IO模型/多路转接模型---三种实现

四种IO模型:阻塞IO/非阻塞IO/信号驱动IO/异步IO

select epoll?IO的过程:发起IO调用,等待IO条件就绪,然后将数据拷贝到缓冲区进行处理---等待/拷贝

阻塞IO:为了完成IO,发起调用,若当前不具备IO条件,则一直等待

类似于钓鱼,没有鱼上钩就一直等待.

poll的区别、

 一个IO完毕后才能进行下一个,对于资源没有充分利用,大部分时间都在等待.

非阻塞IO:完了完成IO,发起调用,若当前不具备IO条件,则立即返回(通常干其他事情需要循环操作重新发起IO)

poll用法,类似于钓鱼,鱼竿抛下去,有鱼则上钩,没鱼也不想一直等待,去干点其他事情.

 流程相对于阻塞操作来说复杂一点,对资源利用更加充分,IO操作不够实时.

poll。信号驱动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   //对标准输入进行监控--标准输入描述符-011   //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 error\n");28       return -1;29 30     }else if(ret==0){31       printf("wait timeout\n");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 30008 //通过类的实例化对象来实现select的简单操作9 class Select10 {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     //开始监控,转接向外部返回就绪的TcpSocket42     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 timeout\n");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.cpp1 #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 port\n");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   //多路转接IO18   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:%s\n",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 30008 class Epoll9 {10   public:11     Epoll():_epofd(-1){12       //1.创建epoll13       _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){//移除epoll39       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 timeout\n");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.跨平台移植性差。

多路转接模型

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

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

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

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

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/94842.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息