多路转接

  • 基本概念
    • ①阻塞与非阻塞
    • ②同步通信 vs 异步通信
    • ③非阻塞IO
  • 五种IO模型
    • ①阻塞IO:
    • ②非阻塞IO
    • ③信号驱动IO
    • ④IO多路转接:
    • ⑤异步IO
  • IO多路转接
    • ①select
    • ②poll
    • ③epoll
      • 1.epoll模型
      • 2.epoll的相关系统调用
      • 3.epoll的工作方式
      • 4.epoll工作方式的对比

基本概念

①阻塞与非阻塞

阻塞与非阻塞讨论的是在等待调用结果时的状态

阻塞调用是指在等待时,当前线程会被挂起. 调用线程只有在得到结果之后才会返回
非阻塞调用指在等待时,该线程可以执行其他任务,不被OS挂起

②同步通信 vs 异步通信

同步与异步讨论的是调用者是否会主动等待调用结果

同步:调用者发出调用时,没有得到结果不会返回,阻塞等待,调用者主动等待该调用结果
异步:与同步相反,发出调用后立即返回,调用内的工作由别人完成,自己并不参与,等待被调用者的通知,直接使用

③非阻塞IO

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

int fcntl(int fd, int cmd, … /* arg */ )
对于cmd参数:
复制一个现有的描述符(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)

通过fcntl,实现一个非阻塞的文件描述符

void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); //将当前的文件描述符的属性取出来if (fl < 0){ perror("fcntl");return; }fcntl(fd, F_SETFL, fl | O_NONBLOCK);//使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数
}

五种IO模型

①阻塞IO:

在内核将数据准备好之前, 系统调用会一直阻塞式等待(被OS挂起)
比如套接字:

②非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回
并且返回EWOULDBLOCK错误码,定期检测

非阻塞IO可能会反复检测该文件描述符数据是否就绪, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用

对于阻塞IO,OS需要唤醒阻塞的进程,由OS发起并执行
而非阻塞轮询是由用户自己发起检测,OS执行

③信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

④IO多路转接:

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

IO分两步,一个是等待,一个是拷贝,高效IO往往指等待时间少

IO多路转接负责同时等待多个文件描述符,当数据就绪时返回,再次recvfrom后就直接拷贝数据,提高了IO效率

⑤异步IO

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

异步IO只需要发出系统调用请求,然后等待OS递交信号,此时用户缓冲区已经拷贝完成,直接取用就行,该动作由OS完成,而信号驱动由用户完成拷贝数据到用户缓冲区

特点:没有参与等待,没有参与拷贝,不会等待OS的信号再去调用recvfrom

IO多路转接

①select

select的主要工作是负责等待数据就绪,并且通知上层
特点:只要底层缓冲区有数据(有空间),select就认为读事件(写事件)就绪

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:最大文件描述符加1(select遍历文件描述符数组)
fd_set:文件描述符的位图,输入输出型参数,输入代表需要OS检测的文件描述符,输出为就绪的文件描述符,只能最多同时监控1024个
readfds:读事件位图
writefds:写事件位图
exceptfds:异常位图
timeout:
如果为NULL,则一直阻塞式等待,
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回
返回值:
执行成功则返回所有就绪文件描述符的个数
0表示时间超过timeout
-1代表等待出错

作为输入输出型参数,select每次调用后,原来的参数数据就被覆盖了,所以需要通过数组保存原来的数据,每次都需要对传入的参数如readfds进行重新设置

连接事件到来,select也认为是读事件就绪

简易的select服务器:

  1 #pragma once                                                                                                               2 #include"sock.hpp"3 #define DFL_FD -14 #define BACK_LOG 55 #define NUM 10246 namespace ns_select7 {8   class SelectServer9   {10     private:11       int listen_sock;12       unsigned short port;13     public:14     SelectServer(unsigned short _port):port(_port)15     {16         17     }18     void InitSelectServer(){19       listen_sock=tzc::Sock::Socket();20       tzc::Sock::Bind(listen_sock,port);21       tzc::Sock::Listen(listen_sock,BACK_LOG);22     }23     void Run()24     {25       fd_set rfds;26       int fd_array[NUM]={0};27       ClearArray(fd_array,NUM,DFL_FD);//将数组初始化为-128       fd_array[0]=listen_sock;//放入监听套接字29       for(;;)30       {31         //重新设置时间与max_fd32         struct timeval timeout={5,0};33         int max_fd=DFL_FD;34         FD_ZERO(&rfds);35         //添加进rfds36         for(auto i=0;i<NUM;i++)37         {38           if(fd_array[i]==DFL_FD)39           {40             continue;41           }42           FD_SET(fd_array[i],&rfds);43           if(max_fd<fd_array[i])                                                                                           44           {45             max_fd=fd_array[i];46           }47         }48         switch(select(max_fd+1,&rfds,nullptr,nullptr,&timeout))49         {50           case 0:51             std::cout<<"timeout"<<std::endl;52             break;53           case -1:54             std::cerr<<"select error"<<std::endl;55             break;56           default:57             //正常事件处理58         HandlerEvent(rfds,fd_array,NUM);59             break;60 61         }62       }   63     }64     void HandlerEvent(const fd_set &rfds,int fd_array[],int num)65     {66       for(auto i=0;i<num;i++)67       {                                                                                                                    68         if(fd_array[i]==DFL_FD)69         {70           continue;71         }72         //有效fd73         if(fd_array[i]==listen_sock&&FD_ISSET(fd_array[i],&rfds))74         {75           //连接事件到来76           struct sockaddr_in peer;77           socklen_t len=sizeof(peer);78           int sock=accept(fd_array[i],(struct sockaddr*)&peer,&len);79           if(sock<0)80           {81             std::cerr<<"accept error"<<std::endl;82             continue;83           }84           uint16_t peer_port=htons(peer.sin_port);85           std::string peer_ip=inet_ntoa(peer.sin_addr);86           std::cout<<peer_ip<<": "<<peer_port<<std::endl;87           //将文件描述符添加到fd_array数组中88           if(!AddFdToArray(fd_array,num,sock))89           {90             close(sock);91             std::cout<<"select server full,close fd "<<sock<<std::endl;                                                    92           }93         }94         else 95         {96           if(FD_ISSET(fd_array[i],&rfds))97           {98             //读事件就绪99             char buffer[1024];
100             //粘包等问题
101             //定制协议
102             //对每个文件描述符定义缓冲区
103             ssize_t s=recv(fd_array[i],buffer,sizeof(buffer)-1,0);
104             if(s>0)
105             {106               buffer[s]=0;
107               std::cout<<"echo# "<<buffer<<std::endl;
108             }
109             else if(s==0)
110             {111               std::cout<<"client quit"<<std::endl;
112               close(fd_array[i]);
113               fd_array[i]=DFL_FD;
114             }
115             else
116             {117               std::cerr<<"recv error"<<std::endl;
118               close(fd_array[i]);
119               fd_array[i]=DFL_FD;
120             }
121           }
122           else
123           {124
125           }
126         }
127       }
128     }
129 private:
130     void ClearArray(int fd_array[],int num,int default_fd)
131     {132       for(auto i=0;i<num;i++)
133       {134         fd_array[i]=default_fd;
135       }
136     }
137     bool AddFdToArray(int fd_array[],int num,int sock)
138     {139       for(int i=0;i<num;i++)
140       {141         if(fd_array[i]==DFL_FD)
142         {143           fd_array[i]=sock;
144           return true;
145         }
146       }
147       //使用完空间了
148       return false;
149     }
150   };
151
152 };

sock.h

  1 #include<iostream>                                                                                                         2 #include<unistd.h>3 #include<cstring>4 #include<sys/socket.h>5 #include<sys/types.h>6 #include<arpa/inet.h>7 #include<netinet/in.h>8 namespace tzc9 {10   class Sock11   {12     public:13       static int Socket()14       {15         int sock=socket(AF_INET,SOCK_STREAM,0);16         if(sock<0)17         {18           std::cerr<<"socket error"<<std::endl;19           exit(1);20         }21         int opt=1;22         setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));23 24           return sock;25       }26       static bool Bind(int sock,unsigned short port)27       {28         struct sockaddr_in local;29         memset(&local,0,sizeof(local));30         local.sin_family=AF_INET;31         local.sin_port=htons(port);32         local.sin_addr.s_addr=INADDR_ANY;33         if(bind(sock,(struct sockaddr*)& local,sizeof(local))<0)34         {35           std::cerr<<"bind error"<<std::endl;36           exit(2);37         }38         return true;39       }40       static bool Listen(int sock,int backlog)41       {42         if(listen(sock,backlog)<0)43         {                                                                                                                  44           std::cout<<"listen error"<<std::endl;45           exit(3);46         }47         return true;48       }49   };50 51 };

select缺点:
1.select能够同时等待的文件描述符是有上限的(1024)
2.select需要和OS交互数据,涉及较多数据拷贝,当select面临的链接很多时,会因为拷贝数据而降低效率
3.select每次调用,都必须从第三方数组重新添加fd,影响程序运行效率,比较麻烦,
4.select的nfds参数,决定了操作系统检测遍历的范围,当大量连接到来时,OS需要遍历的数据会越来越多
5.select成功返回后,每次都需要遍历第三方数组判断哪些文件描述符事件就绪
select优点:
1.select可以同时等待多个fd,而且只负责等待,不会拷贝数据到用户缓冲区

多路转接适用场景:大量的连接到来,但是只有少量是活跃的
而一般场景:如果大量连接到来都很活跃,直接阻塞式recvfrom读取就足够了

②poll

poll解决了select的两个问题
1.解决了select检测文件上限的问题
2.将用户传给OS的需要检测的文件描述符与OS传给用户的就绪文件描述符的两个行为进行分离
poll的缺点:
1.和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
2.每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
3.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降

poll函数接口

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:需要检测的文件描述符
nfds:结构体数组长度
timeout:轮询方式,与select一致
返回值
小于0, 表示出错;
等于0, 表示poll函数等待超时;
大于0, 表示poll由于监听的文件描述符就绪而返回.
struct pollfd {
int fd; //file descriptor
short events; // 用户 requested events
short revents; // OS returned events
};
events和revents的取值:
POLLIN:数据可读
POLLOUT:数据可写

简易poll

  1 #include"sock.hpp"                                                                                                         2 #include<poll.h>3 class PollServer4 {5   private:6     int listen_sock;7     int port;8   public:9     PollServer(int _port):port(_port)10     {}11     void InitServer()12     {13       listen_sock=tzc::Sock::Socket();14       tzc::Sock::Bind(listen_sock,port);15       tzc::Sock::Listen(listen_sock,5);16     }17     void Run()18     {19       struct pollfd rfds[64];20       for(int i=0;i<64;i++)21       {22         rfds[i].fd=-1;23         rfds[i].events=0;24         rfds[i].revents=0;25       }26       rfds[0].fd=listen_sock;27       rfds[0].events|=POLLIN;28       rfds[0].revents=0;29       for(;;)30       {31         switch(poll(rfds,64,-1))32         {33           case 0:34             std::cout<<"timeout"<<std::endl;35             break;36           case -1:37             std::cerr<<"poll error"<<std::endl;38           default:39             for(int i=0;i<64;i++)40             {41               if(rfds[i].fd==-1)42               {43                 continue;                                                                                                  44               }45               if(rfds[i].revents&POLLIN)46               {47                 if(rfds[i].fd==listen_sock)48                 {49                   //连接到来50                   std::cout<<"get a new link"<<std::endl;51                 }52                 else 53                 {54                   //读事件就绪55                   56                 }57               }58             }59         }60       }61     };62 };

③epoll

1.epoll模型


epoll的使用过程就是三部曲:
1.调用epoll_create创建一个epoll句柄
2.调用epoll_ctl, 将要监控的文件描述符进行注册
3.调用epoll_wait, 等待文件描述符就绪

2.epoll的相关系统调用

int epoll_create(int size)
size:128or256,该参数已被废弃
返回值:
返回一个epoll的文件描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
第一个参数是epoll_create()的返回值(epoll的句柄)
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD :注册新的fd到epfd中
EPOLL_CTL_MOD :修改已经注册的fd的监听事件
EPOLL_CTL_DEL :从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是告诉内核需要监听什么事

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
epoll将会把发生的事件拷贝到events数组中
maxevents告知内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size

struct epoll_event结构如下:

  1 #pragma once2 #include"sock.hpp"3 #include<sys/epoll.h>                                                                                                      4 #define back_log 5   5 #define MAX_NUM 646 namespace  ns_epoll7 {                  8   const int size=256;9   class EpollServer  10   {                11     private:12       int listen_sock;13       int epfd;       14       uint16_t port;15     public:         16       EpollServer(int _port):port(_port)17       {}                                18       void InitEpollServer()19       {                     20         listen_sock=tzc::Sock::Socket();21         tzc::Sock::Bind(listen_sock,port);22         tzc::Sock::Listen(listen_sock,back_log);23         if((epfd=epoll_create(size))<0)         24         {      25           std::cerr<<"epoll_create error\n"<<std::endl;26           exit(4);27         }28       }29       void AddEvent(int sock,uint32_t event)30       {31         struct epoll_event ev;32         ev.events=0;33         ev.events|=event;34         ev.data.fd=sock;35         if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0)36         {37           std::cerr<<"epoll_ctl error,fd:"<<sock<<std::endl;38         }39       }40       void DelEvent(int sock)41       {42         if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0)43         {                                                                                                                  44           std::cerr<<"epoll_ctl error"<<std::endl;45         }46       }47       void Run()48       {49         AddEvent(listen_sock,EPOLLIN);50         int timeout=1000;51         struct epoll_event revs[MAX_NUM];52         for(;;)53         {54           //返回值num表明就绪事件个数,OS会依次放入revs中55           int num=epoll_wait(epfd,revs,MAX_NUM,timeout);56           if(num>0)57           {58             for(int i=0;i<num;i++)59             {60               int sock=revs[i].data.fd;61               if(revs[i].events&EPOLLIN)62               {63                 if(sock==listen_sock)64                 {65                   struct sockaddr_in peer;66                   socklen_t len=sizeof(peer);67                   int sk=accept(listen_sock,(struct sockaddr*)&peer,&len);                                                 68                   if(sk<0)69                   {70                     std::cout<<"accept error"<<std::endl;71                     continue;72                   }73                   std::cout<<"get a new link: "<<inet_ntoa(peer.sin_addr)<<": "<<ntohs(peer.sin_port)<<std::endl;74                   AddEvent(sk,EPOLLIN);75                 }76                 else 77                 {78                   //读事件就绪79                   char buffer[1024];80                   ssize_t s=recv(sock,buffer,sizeof(buffer)-1,0);81                   if(s>0)82                   {83                     buffer[s]=0;84                     std::cout<<buffer<<std::endl;85                   }86                   else 87                   {88                     std::cout<<"client close"<<std::endl;89                     close(sock);90                     DelEvent(sock);91                   }                                                                                                        92                 }93               }94               else if(revs[i].events&EPOLLOUT)95               {96                   //写事件97               }98             }99           }
100           else if(num==0)
101           {102             std::cout<<"time out"<<std::endl;
103           }
104           else
105           {106             std::cout<<"epoll error"<<std::endl;
107           }
108         }
109       }
110       ~EpollServer()
111       {112         if(listen_sock>=0)
113         {114           close(listen_sock);
115         }
116         if(epfd>=0)
117         {118           close(epfd);
119         }
120       }
121
122   };
123 };    

epoll优点:
1.接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
2.数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝 内核中,该红黑树结点仍在内存,下次操作不需要重新添加(而select/poll都是每次循环都要进行拷贝)
3.事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
4.没有数量限制: 文件描述符数目无上限

3.epoll的工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET),默认是LT的

LT VS ET

LT与ET的差别在于就绪事件通知机制:
LT:只要底层有数据就会一直通知上层读取数据
ET:当底层的数据从无到有,从有到多变化时才会通知上层一次

对于ET:
只有在底层数据变化时才会通知,但是如果此次并没有读完缓冲区数据,如果此后该缓冲区也再也无数据变化,将会导致该剩余数据一直没能被应用层读取,所以ET应该保证一次就把缓冲区的数据全部读取完

通过不断循环调用recv,判断recv的返回值来判定是否读取完毕,读取时会出现如下两种情况:
情况1:如果返回的小于期望读取的字节数,那么说明已经读取完毕
情况2:如果最后一次读取的刚刚将缓冲区读完,返回值刚好等于缓存区大小,但此时读取将被判定为还未读取完毕,会再次读取,进程将被阻塞,服务器将被挂起

所以对于情况2,就要求ET模式必须为非阻塞轮询模式的读取,当缓冲区无数据时返回值小于0,退出循环

可见ET模式下recv,write都必须是非阻塞的,而LT可以不需要,因为即使此次没读完还会通知,下次还能读取
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET

4.epoll工作方式的对比

1.LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是一次处理完数据
2.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的
3.同时ET 的代码复杂程度也更高

LinuxI/O多路转接相关推荐

  1. epoll(eventpoll)是干嘛的?IO多路转接技术(相较select、poll的优点)

    首先我们要知道epoll是用来干什么的(定义):epoll是一种IO多路转接技术,在LINUX网络编程中,经常用来做事件触发,即当有特定事件到来时,能够检测到,而不必阻塞进行监听,基于事件驱动的IO多 ...

  2. Linux下I/O多路转接之select --fd_set

    fd_set 你终于还是来了,能看到这个标题进来的,我想,你一定是和我遇到了一样的问题,一样的疑惑,接下来几个小时,我一定竭尽全力,写出我想说的,希望也正是你所需要的: 关于Linux下I/O多路转接 ...

  3. IO多路转接之epoll

    IO多路转接之epoll 文章目录 IO多路转接之epoll 一.epool 二.基于epoll实现服务器(LT) 三.**基于epoll实现服务器(LT)** 一.epool 是为处理大批量句柄而作 ...

  4. IO多路转接之poll

    IO多路转接之poll 文章目录 IO多路转接之poll 一.Poll 一.Poll 1.函数原型: int poll(struct pollfd *fds, nfds_t nfds, int tim ...

  5. I/O多路转接之select

    I/O多路转接之select 文章目录 I/O多路转接之select 一.五种IO模型 二.I/O多路转接之select原理 一.五种IO模型 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待 ...

  6. IO多路转接模型-----epoll

    epoll: Linux下性能最高的多路转接模型 epoll 有3个相关的系统调用. epoll_create 功能:创建epoll,在内核中创建eventpoll结构体,size决定了epoll最多 ...

  7. IO多路转接模型----(select的模型,select的优缺点,poll的模型,poll的优缺点)

    IO多路转接模型:select/poll/epoll 对大量描述符进行事件监控(可读/可写/异常) select模型 用户定义描述符的事件监控集合 fd_set(这是一个位图,用于存储要监控的描述符) ...

  8. 高级IO--1 ---(五种典型IO,阻塞IO,非阻塞IO,信号驱动IO,异步IO, IO多路转接)

    高级IO: 五种典型IO: 阻塞IO/非阻塞IO/信号驱动IO/异步IO/IO多路转接 IO多路转接模型:select/poll/epoll 五种典型IO 阻塞IO IO操作的流程:等待IO操作条件具 ...

  9. 循环服务器,并发服务器模型以及I/O多路转接模型

    https://blog.csdn.net/xinianbuxiu/article/details/53455784 一.基于TCP/IP协议的基本循环服务器 tcp_server.c #includ ...

  10. 多路转接select1

    高级IO 通常情况下所有的 IO 都可以分为两步来完成, 第一步等待, 第二步数据搬迁, 为了提高 IO 效率通常所运用的方法就是减少等待的时间 举个钓鱼的例子 现在有五个人张三, 李四, 王五, 赵 ...

最新文章

  1. 漫画:百度从Google学来的面试题,想进大厂必备!
  2. 力的作用与形态的分类
  3. angular项目如何分层
  4. 神策数据曹犟将出任导师:宝洁黑客马拉松聚焦工业大数据
  5. 剑指 offer 链表倒数的第k个数
  6. 【Servlet】Servlet显示时间和IP等信息
  7. JAVA的cell设置行间距_iOS设置tableView的cell之间的间距
  8. python学习笔记——多线程编程
  9. jxl导入Excel
  10. JavaScript 资源列表
  11. 利用curl去hack他人博客
  12. 解决Fedora中root无法登录问题
  13. c语言float类型小数部分占几个字节,c语言float类型小数点后位数
  14. 在Debian上安装FlashPlayer10
  15. python十折交叉验证
  16. 全息投影技术及其实现(附素材下载)
  17. 大数据技术之大数据概论
  18. 梆梆加固的病毒分析-破解篇
  19. Windows系统安装绿色版MySQL8
  20. 时序预测之三_傅立叶和小波变换

热门文章

  1. day01 错误1
  2. mysql 分区原理_mysql 原理~ 分区表
  3. javascript——构造函数和原型对象
  4. ps新手零基础知识入门教程学习_图文
  5. C#序列化和反序列化到底是什么意思
  6. catia 摆正点云的方法_基于CATIA的逆向工程点云定位方法
  7. 链家网东莞二手房数据分析
  8. C语言二进制与十进制之间的转换
  9. iOS小技能:导航控制器(控制器、view的多种创建方式、控制器的生命周期)
  10. VGA驱动实现并下板EGO1