IO复用select、poll、epoll

tech2022-10-12  98

一、IO 一次IO读操作需要两个步骤 1)等待数据 2)将数据从内核buffer拷贝到用户进程buffer

同步阻塞IO: 用户进程进行read,会发出中断,如果内核数据没有准备好,会一直等待kernel准备数据,等待kernel复制数据到进程内存中

同步IO非阻塞: 进程进行数据读取,会发出中断请求,如果kernel数据没有准备好,则立即返回

同步IO多路复用: 是阻塞的,可以监听多个套接字

异步IO 进程发出中断请求,立即返回,kernel准备好数据并拷贝到进程中,进程才会处理

二、SELECT: 程序会在select()阻塞,直到超时或者一个文件描述符发生了改变**(其中如果timeout设置为nullptr,则会一直阻塞)**,返回为发生变化的文件描述符的个数。FD_SET实际上是一个bitmap,每一bit表示对应的文件描述符,将要进行监听的套接字对应的那个bit设为1(比如FD_SET总共8bit,要监听sock=0的套接字则会把第一个bit设为1,1000000)。发生变化后,会把没有发生变化文件描述符对应的bit清零。

SELECT使用方法: 用vector来存储要进行监控的文件描述符,**(因为select会清空没有发生变化的文件描述符)**首先将监听套接字和其他客户端套接字加入监听队列中,用select进行监听,先判断vector中的套接字是否变化,再判断监听套接字是否变化

#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> #include<vector> vector<int> client;//建立的处理套接字保存在vector中 int main(){ //建立套接字 int sockfd=socket(AF_INET,SOCK_STREAM,0); //建立ipv4通用地址结构 struct sockaddr_in server_addr,client_addr; memset(&server_addr,0,sizeof(server_addr)); client_addr.sin_family=AF_INET; client_addr.sin_addr.s_addr=htonl(INADDR_ANY); client_addr.sin_port=htons(8888); memset(&client_addr,0,sizeof(client_addr)); //监听套接字和通用地址结构绑定 bind(sockfd,(struct sockaddr*)(&server_addr),sizeof(struct sockaddr_in)); //监听端口 listen(sockfd,5); //套接字集合 while(true){ fd_set rdf; FD_ZERO(rdf); FD_SET(sockfd,rdf);//监听套接字 int maxSock=sockfd; for(int i=0;i<client.size();i++){ FD_SET(client[i],rdf); maxSock=max(maxSock,client[i]); } int ret=select(Max+1,&rdf,nullptr,nullptr,nullptr); if(ret<1){ cout<<"套接字没有发生变化"<<endl; exit(1); } for(int i=0;i<client.size();i++){ if(FD_ISSET(client[i],rdf)){ char buf[1024]; read(client[i],buf,1024); } } if(FD_ISSET(sockfd,rdf)){ //监听套接字发生变化,需要接收数据 int newsock=accept(sockfd,(struct sockaddr_in*)(&client_addr),sizeof(client_addr)); client.push_back(newsock); } } for(int i=0;i<client.size();i++){ close(client[i]); } return 0; }

当进程调用select,会把要监听的FD_SET拷贝到kernel中,kernel会遍历该集合,判断需要监听的文件描述符是否发生了改变,改变的不变,没有改变的清零,并返回;或者超时了函数返回0.进程得到返回结果,会重新遍历FD_SET判断具体哪一个文件描述符发生了改变。每次调用select都需要重新把需要监听的文件描述符加入FD_SET中 进程分为running和waiting状态,CPU会调度running的进程,waiting的进程不占用CPU资源。 一个进程创建了SOCKET,就会有一个文件句柄指向改对象,SOCKET中包含了缓冲区,等待列表。当一个进程调用RECV()就会进入waiting,并且添加到等待队列中。

三、poll: 原理和select一样,只不过把进行监听的文件描述符集合(FD_SET)改成了pollfds数组,每个元素是pollfd结构体,其中结构为

struct pollfd{ int fd;//要监听的文件描述符 short events;//关注的事件,读POLLIN,写POLLOUT short revents;//关注的事情发生了改变就返回1 }

kernel会把revents设成1,因此只需要判断revents&POLLIN就可知道有没有发生变化。 1)因为只是把revents清零,因此不需要每次调用poll()的时候重新把需要监听的事件加入pollfds[]中 2)因为传入的是pollfds[],因此可以超过1024.

和select一样都需要进行用户态到内核态的切换,都需要对返回结果进行遍历,判断哪一个文件描述符发生了改变。

四、epoll: 1)生成epoll专用的文件描述符:int epoll_create(int size);//size表示epoll能够关注的最大描述符数 2)控制某个epoll文件描述符事件,注册、修改、删除int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event); epfd是epoll_create()生成的epoll专用描述符 op进行的操作:EPOLL_CTL_ADD;EPOLL_CTL_MOD;EPOLL_CTL_DEL fd:关联的文件描述符

struct epoll_event{ uint32_t events;//EPOLLIN、EPOLLOUT、EPOLLERR epoll_data_t data;//一个union用于描述关联套接字的属性 };

3)int epoll_wait(int epfd, struct epoll_events* events, int maxevents, int timeout); events 用于回传处理事件的数组,是struct epoll_events

#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> #include <sys/epoll.h> #include<vector> int main(){ //建立套接字sock、bind、listen int fd=epoll_create(3000); //回传数组 struct epoll_events all[3000]; struct epoll_events ev; ev.events=EPOLLIN | EPOLLET; ev.data.fd=sock; epoll_ctl(fd,EPOLL_CTL_ADD,sock,&ev); while(true){ //返回的结果在all数组 int ret=epoll_wait(fd,all,3000,-1); //遍历返回数组 for(int i=0;i<ret;i++){ //有新连接 if(all[i].data.fd==sock){ int csock=accept(); struct epoll_events newClient; newClient.events=EPOLLIN | EPOLLET; newClient.data.fd=csock; epoll_ctl(fd,EPOLL_CTL_ADD,csock,newClient); } //已经连接的客户端有数据发送 else{ if(!all[i].events & EPOLLIN){ continue; } else{ int len=read(); if(len==0){ epoll_ctl(fd,EPOLL_CTL_DEL,all[i].data.fd,nullptr); close(all[i].data.fd); } send(); } } } } }

工作模式: 1)水平触发模式(默认):判断缓冲区,只要fd对应的缓冲区有数据,epoll_wait就返回,返回次数与发送数据次数没有关系(返回次数>发送次数); 2)边沿触发模式(ET):客户端给服务端发送数据,发送一次就epoll_wait就返回一次,只要将struct epoll_event.events=EPOLLIN | EPOLLET即可 3)边沿非阻塞模式

工作原理: 调用epoll_create(),会创建一个eventpoll结构体,

struct eventpoll {   ...   /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,   也就是这个epoll监控的事件*/   struct rb_root rbr;   /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/   struct list_head rdllist;   ... };

调用epoll_wait(),如果rdlist中有数据,则会返回,没有的话,就sleep,timeout时间到后就返回

三种模式比较: 都是通过某种结构向内核传递需要监听的文件描述符。 select传入的是fd_set,没有与读写等事件绑定,因此需要传入三个fd_set,返回的是修改后的fd_set,因此如果需要重新监听的话,就需要重新往fd_set里面添加文件描述符。 poll传入的是polld结构数组,里面包含了{fd,event,revent},每次修改的是revent,每次调用poll不需要重置polld。这两者都无法确定哪些套接字发生了变化,因此需要O(n)来搜寻发生了变化的套接字。 epoll建立了一个红黑树来管理注册的事件,,红黑树的每个节点包括了{event,data},epoll_wait会将返回的事件放在rdlist中,并通过一个数组返回。

select和poll仅支持LT,epoll支持ET。select和poll需要轮询,将发生变化的套接字返回给进程,epoll采用的是回调,一旦发生了变化,就添加到rdlist中。

最新回复(0)