由浅入深理解高级IO--select poll epoll

由浅入深理解高级IO–select poll epoll

目录

  • 五种IO模型
    • 阻塞IO
    • 非阻塞IO
    • 信号驱动IO
    • IO多路转接
    • 小结
  • 高级IO重要概念
    • 同步通信 vs 异步通信
    • 阻塞 vs 非阻塞
  • I/O多路转接之select
    • 初识select
    • socket就绪条件
    • select使用示例
    • select实现原理
  • I/O多路转接之poll
    • poll函数接口
    • poll的优点
    • poll的缺点
    • poll使用示例
  • I/O多路转接之epoll
    • epoll 有3个相关的系统调用
      • epoll_create
      • epoll_ctl
      • epoll_wait
    • epoll 的工作原理
      • epoll的使用三部曲
      • epoll的优点(和 select 的缺点对应)
    • epoll工作方式
      • 水平触发(LT
      • 边缘触发Edge Triggered工作模式
      • 对比LT和ET
    • ET模式要设置为非阻塞
    • epoll案例代码
    • 总结

五种IO模型

钓鱼例子理解五种IO模型

张三钓鱼:张三在河边钓鱼,一人一个鱼竿,张三一直盯着鱼竿看有没有鱼上钩,发现鱼上钩,立马将鱼钓上来。

李四钓鱼:李四在河边钓鱼,一人一个鱼竿,但李四不像张三一样,一直盯着鱼竿,在钓鱼期间,李四会吃零食,会去找朋友聊天,过一会来看鱼有没有上钩,鱼没上钩,就继续找朋友聊天或做其他事,当张三再次来检查时发现有鱼上钩,就把鱼钓上来。

王五钓鱼:王五钓鱼有些独特,在鱼竿上绑个铃铛,当有鱼上钩上,铃铛就会响,王五听到铃响后,就把鱼钓上来。当铃铛没有响,王五就在做其他事了。不会盯着鱼竿。

赵六钓鱼:赵六有很多鱼竿,加入有100个鱼竿,赵六就用100个鱼竿钓鱼,赵六就轮询检测哪根鱼竿上有鱼上钩,发现有鱼上钩就钓上来,然后继续去轮询检查其它鱼竿有没有鱼上钩,赵六的工作就是一直在遍历所有鱼竿检查有没有鱼上钩,有就钓上来,没有就一遍一遍的轮询检查。

田七钓鱼:田七是个老板,想吃鱼,但不想自己钓,就让司机小刘拿着鱼竿去钓鱼,钓到鱼之后送给田七。

从以上例子引出集中IO模型:张三—》阻塞IO;李四—》非阻塞IO;王五—》信号驱动IO;
赵六—》IO多路转接;田七—》异步IO

阻塞IO

在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。
阻塞IO是最常见的IO模型.。由浅入深理解高级IO--select poll epoll_第1张图片

非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
由浅入深理解高级IO--select poll epoll_第2张图片

信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
由浅入深理解高级IO--select poll epoll_第3张图片

IO多路转接

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件
描述符的就绪状态。
由浅入深理解高级IO--select poll epoll_第4张图片

异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
由浅入深理解高级IO--select poll epoll_第5张图片

小结

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝。而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少

高级IO重要概念

同步通信 vs 异步通信

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

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;

异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

I/O多路转接之select

初识select

系统提供select函数来实现多路复用输入/输出模型.
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select函数原型
由浅入深理解高级IO--select poll epoll_第6张图片
nfds:所要等的最大文件描述符值+1,不是描述符的个数+1,例如我们要等两个文件描述符1和7,nfds就设置;为8,文件描述符就是数组下标,只有知道最大的文件描述符值+1,就可以去遍历所有文件描述符;

readfds:既是输入型参数,又是输出型参数,输入时是用户想告诉OS,OS你应该帮我
关心哪些文件描述符上的读事件,输出时是OS想告诉用户,某些文件描述符的事件已经就绪。

exceptfds:OS哪个文件描述符报错了,你要告诉我

fd_set:是一个位图,以8个比特位为例。0000 0000 (位置编号0-7),比特位的位置代表是哪一个文件
描述符,比特位的内容代表的是是否需要关心特定文件描述符上的读事件。例 1011 1111表示的是:用户
告诉OS,你要帮我关心0,1,2,3,4,5,7号文件描述符。当0,和7号文件描述符就绪了,OS就会给用户
返回一张文件描述符1000 0001 ,

当select调用完成后,我们要去检测哪些文件描述符已经就绪了,然后去read(),read不会被阻塞,因为文件
描述符已经就绪了。当读取结束时,fd_set已经被改成了 1000 0001,下次再去select时,要重新设置
fd_set* readfds 为1011 1111.每次读取都要对select的位图进行设置,要关心哪些文件描述上的哪些事件

select:本质是"等"就绪事件通知方式。等的目的本质是在做条件检测,当底层的读或写事件就绪时会通知我们的上层,底层的读或写事件就绪了,你可以读或写了。

timeout:有3个值。一个是设置阻塞等,一是设置非阻塞等,一是设置一个时间间隔去等。nullptr:则表示select没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0为非阻塞。参数timeout取值:特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

select返回值:
大于0,表示有文件描述符就绪了
等于0,表示超时了,就是我们等待这段时间,没有文件描述符就绪
-1,错误

select就是一个系统调用,就足以见到是系统帮我们去做这样的工作的,让OS去帮我去等多个文件描述符,就要告诉OS两件事情:

1、OS你要帮我等哪些文件描述符。
2、OS你要帮我等哪些文件描述符上的哪些事件,其中隐含的是用户要告诉OS这样的一个信息。

以read为例fed_set是一张可以表示多个描述符的集合,该集合是一个位图,比特位的位置代表的是文件描述符的值,比特位的内容根据是输入还是输出,含义是不同的,输入时是用户告诉内核,OS你要帮我关心这些文件描述符上的读事件,当它返回时,比特位的位置代表的是文件描述符,比特位的内容代表的是哪些文件描述符上的读事件已经就绪了,意味着可以非阻塞读了

socket就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件

描述符, 并且返回值大于0;

socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;

监听的socket上有新的连接请求;

socket上有未处理的错误;

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记

SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;

socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;

socket使用非阻塞connect连接成功或失败之后;

socket上有未读取的错误;

select使用示例

Makefile文件

  1 Server:Server.cc
  2   g++ -o $@ $^ -std=c++11
  3 
  4 .PHONY:clean
  5 clean:
  6   rm -f Server                                                                                                      

SelectServer.hpp文件

1 #pragma once                                                                                                      
    2 #include "Sock.hpp"
    3 
    4 #define DFL_PORT 8080
    5 #define NUM (sizeof(fd_set)*8) //位图大小*8就时比特位大小
    6 #define DFL_FD -1
    7 
    8 class SelectServer 
    9 {
   10   private:
   11     int lsock;  //listen sock 
   12     int port;
   13     int fd_array[NUM];  //辅助数组,将已经打开的文件描述符保存起来
   14   public:
   15     SelectServer(int _p = DFL_PORT)
   16       :port(_p)
   17       {}
   18     void InitServer()
   19     {
   20       for(int i = 0;i < NUM;i++)
   21       {
   22         fd_array[i] = DFL_FD;  //全部初始化为 -1
   23       }
   24       lsock = Sock::Socket();
   25       Sock::Setsockopt(lsock);
   26       Sock::Bind(lsock,port);
   27       Sock::Listen(lsock);
   28 
   29       fd_array[0] = lsock;   //把0号下标设置为监听套接字
   30     }
   31     //新获取的文件描述符添加到fd_array里
   32     void AddFd2Array(int sock)
   33     {
   34       int i=0;
   35       for(;i < NUM;i++)
   36       {
   37         if(fd_array[i] == DFL_FD) //不关心的文件描述符
   38         {
   39           break;
   40         }                                                                                                         
   41       }  
   42       if(i >= NUM)  //数组满了,处理不了新来的文件描述符
   43       {
   44         cerr << "fd array is full ,close sock" <<endl;
   45         close(sock);
   46       }
   47       else 
   48       {
   49         fd_array[i] = sock;
   50         cout << "fd: " << sock << " add to select ..." << endl;        
   51       }
   52     }
   53     void DefFromArray(int index)
   54     {
   55       if(index >= 0 && index < NUM )
   56       {
   57         fd_array[index] = DFL_FD;
   58       }
   59     }
   60 
   61     //处理就绪文件描述符
   62     void HandlerEvents(fd_set *rfds)                                                                              
   63     {
   64       for(int i = 0;i < NUM ;i++)
   65       {
   66         if(fd_array[i] == DFL_FD)  //不是合法文件描述符,不需要关心的文件描述符,不需要检测是否在rfds里
   67         {
   68           continue;
   69         }
   70 
   71         //是合法文件描述符,意思是需要关心的文件描述符,检测是否就绪,rfds中的文件
   72         //放的描述符都是就绪的
   73         if(FD_ISSET(fd_array[i],rfds)) //FD_ISSET检测fd_array[i]是否在rfds中
   74         {
   75           //读就绪
   76           //读就绪有两种:1、链接就绪 2、数据就绪
   77           
   78           if(fd_array[i] == lsock)
   79           {
   80             // 1、链接事件就绪
   81             int sock = Sock::Accept(lsock); //不会被阻塞,因为已经就绪了
   82             if(sock >= 0)   
   83             {                                                                                                     
   84               //sock ok 
   85               cout << "get a new link ..." << endl;
   86               //拿到了文件描述符,将来要通过这个文件描述符进行数据IO
   87               //不可以直接读/写,因为我们不知道它的读或写是否就绪,只有OS知道,我们要把
   88               //文件描述符放入fd_array中
   89               AddFd2Array(sock);  //将有效文件描述符放到数组中
   90 
   91             }
   92           }
   93           else 
   94           {
   95             // 2、普通读数据就绪
   96             char buf[1024];
   97             //从fd_array[i]里读,读到buf中,期望读sizeof(buf)大小
   98             ssize_t s = recv(fd_array[i],buf,sizeof(buf),0);  //返回值为读取多少字节数
   99             if(s > 0 )
  100             {
  101               buf[s] = 0;
  102               cout << "client# " << buf << endl;
  103             }
  104             else if(s == 0) //说明对方将链接关闭
  105             {                                                                                                     
  106               cout << "client quit" <<endl;
  107               close(fd_array[i]); //我也将链接关闭
  108               DefFromArray(i);     //将文件描述符从数组里删除
  109             }
  110             else 
   111             {
  112               //..报错
  113             }
  114           }
  115         }
  116       }
  117     }
  118     void Start()
  119     {
  120       int maxfd = DFL_FD;
  121 
  122       for(;;)
  123       {
  124         //select
  125         fd_set rfds;                   //集合,读文件描述符集合
  126         FD_ZERO(&rfds);                //对rfds清空
  127         cout << "fd_array: ";                                                                                     
  128         for(int i =0;i < NUM ;i++)     //将要关心的文件描述符添加到rfds中。
  129         {
  130           if(fd_array[i] != DFL_FD)
  131           {
  132             cout << fd_array[i] << " ";   //输出数组中合法文件描述符
  133             FD_SET(fd_array[i],&rfds);  //将要关心的文件描述符设置进读文件描述符集合里
  134             if(maxfd < fd_array[i])
  135             {
  136               maxfd = fd_array[i];      //更新maxfd
  137             }
  138           }
  139         }
  140         cout << endl;
  141         cout << "begin select ..." << endl;
  142 
  143         //struct timeval timeout = {0,0};
  144         
  145         //select有3个返回值:>0,表示有多少个文件描述符就绪了,=0,表示等待超时了,<0等待失败
  146         //select第一个参数是等待的最大文件描述符值+1
  147         //最后一个参数timeout==0为非阻塞等待,==nullptr为阻塞等待,为其他值则是等待多少秒/微妙
  148         //随着时间的推移,获取新链接越来越多的时候,意味着rfds里的内容越来越多,意味着select等的文件
  149         //描述符越来越多了,就绪概率越来越大                                                                      
  150         switch(select(maxfd+1,&rfds,nullptr,nullptr,nullptr))
  151         {
  152           case 0:
  153             cout << "timeout ..." << endl ;
  154             break;
   155           case -1:
  156             cerr << "select error!" << endl;
  157             break;
  158           default:    //有文件描述符就绪,就绪的文件描述符在rfds里,rfds既是输入又是输出
  159                       //做输入时:用户告诉OS你要帮我关注哪些文件描述符,做输出时,OS告诉用户,你关心的
  160                       //哪些文件描述符就绪了
  161             HandlerEvents(&rfds);   //处理事件
  162             break;
  163         }
  164         
  165       }
  166     }
  167     ~SelectServer()
  168     {}                                                                                                            
  169 };

Server.cc文件

  1 #include "SelectServer.hpp"
  2 using namespace std;
  3 
  4 void Usage(string proc)
  5 {
  6   cout << "Usage:"<< proc << "port" <<endl;                                                                         
  7 }
  8 
  9 //Server port 
 10 
 11 int main(int argc,char* argv[])
 12 {
 13   if(argc !=2)
 14   {
 15     Usage(argv[0]);
 16     exit(1);
 17   }
 18   SelectServer* ssvr = new SelectServer(atoi(argv[1]));
 19   ssvr->InitServer();
 20   ssvr->Start();
 21   return 0;
 22 }

Sock.hpp文件

1 #pragma once                                                                                                        
  2 
  3 #include<iostream>
  4 #include<string>
  5 #include<sys/socket.h>
  6 #include<netinet/in.h>
  7 #include<sys/types.h>
  8 #include<unistd.h>
  9 #include<stdlib.h>
 10 #include<strings.h>
 11 #include<arpa/inet.h>
 12 #include<sys/select.h>
 13 
 14 using namespace std;
 15 class Sock
 16 {
 17   public:
 18     static int Socket()
 19     {
 20       int sock = socket(AF_INET,SOCK_STREAM,0);
 21       if(sock < 0)
 22       {
 23         cerr << "socket error" <<endl; 
 24         exit(2);
 25       }
 26       return sock;
 27     }
 28     static void Bind(int sock,int port)
 29     {
 30       struct sockaddr_in local;
 31       bzero(&local,sizeof(local));
 32 
 33       local.sin_family = AF_INET;
 34       local.sin_port = htons(port);
 35       local.sin_addr.s_addr = htonl(INADDR_ANY);
 36 
 37       if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
 38       { 
 39          cerr << "bind error" <<endl;
 40          exit(3);                                                                                                   
 41       }   
 42     }
 43     static void Listen(int sock)
 44     {
 45       if(listen(sock,5) < 0 )
 46       {
 47         cerr << "listen error" << endl;
 48         exit(4);
 49       }
 50     }
 51 
 52     static int Accept(int sock)
 53     {
 54       struct sockaddr_in peer;
 55       socklen_t len = sizeof(peer);
 56       int fd = accept(sock,(struct sockaddr*)&peer,&len);
 57       if(fd < 0)
 58       {
 59         cerr << "accept error" <<endl;
 60       }
 61       return fd;
 62     }                                                                                                               
 63 
 64    static void Setsockopt(int sock) //将端口设置为复用状态
 65    {
 66       //第一个参数是套接字,第二个参数是在哪一层,在套接字层,第三个参数是设置什么特性
 67       //后面两个参数是怎么设置
 68       int opt = 1;
 69       setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
 70    }
 71 };        

效果演示:
由浅入深理解高级IO--select poll epoll_第7张图片
select缺点

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

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

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

select能不能提高效率,什么叫做高级IO:

高级IO就是减少等待时间比重,read一次只能读取一个文件描述符,在单进程程序中,一次只能等一个文件描述符,select在单进程可以等待多个文件描述符。意味着任意时刻文件描述符就绪的概率变大了,服务器处于IO真实拷贝的情况时间变多,就是高性能服务器。

select实现原理

由浅入深理解高级IO--select poll epoll_第8张图片

I/O多路转接之poll

poll函数接口

由浅入深理解高级IO--select poll epoll_第9张图片

poll的优点

不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现

1、pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便。

2、poll并没有最大数量限制 (但是数量过大后性能也是会下降)。

poll的缺点

poll中监听的文件描述符数目增多时和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.

同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

poll使用示例

1 #include <iostream>                                                                                                 
  2 #include <poll.h>
  3 #include <unistd.h>
  4 
  5 using namespace std;
  6 int main()
  7 {
  8   //等标准输入
  9   
 10   struct pollfd rfds[1];      //等一个文件描述符,标准输入
 11   rfds[0].fd = 0;             //0就是标准输入
 12   rfds[0].events = POLLIN;    //关心POLLIN事件
 13   rfds[0].revents = 0;        //由系统填
 14   char buf[1024] = {0};
 15 
 16   cout << "poll begin..." <<endl ;
 17   while(true)
 18   {
 19     switch(poll(rfds,1,1000))  //返回值大于0,几个事件就绪
 20     {
 21       case 0:
 22         cout << "time out ..." << endl;
 23         break;
  24       case -1:
 25         cout << "poll error" << endl;
 26         break;
 27       default:
 28         cout << "events happen! " <<endl;
 29 
 30         if(rfds[0].fd == 0 && (rfds[0].revents & POLLIN))
 31         {
 32           ssize_t s = read(0,buf,sizeof(buf));
 33           buf[s] = 0;
 34           cout << "echo# " << buf <<endl;
 35         }
 36         break;
 37     }
 38   }
 39   return 0;
 40 }

poll总结:
poll需要用户 -》内核,内核-》用户的数据拷贝,数组越来越大时,拷贝的数据也就越来越大,效率越来越低,当数组量巨大时,poll轮询检测一次,周期变长,文件描述符越多,事件就绪的概率就越大,轮询是次数越来越多,所以文件描述符越多,poll效率越低下。

select和poll共同的缺点
不能等待海量文件描述符,select等待的文件描述符有上限,poll等待大量文件描述符是效率低下。

多路转接的情况更善于处理长链接的情况,因为大部分之间都在等。例如QQ就是长链接,
我们大部分时间都没有发消息。短链接为什么不行,如果有大量的短链接过来了,select,poll,epoll,不一定有正常通信高效,因为把文件描述符托管给select,poll,epoll是有成本的,如果刚放进去,就拿出来。不一定高效。

I/O多路转接之epoll

epoll 有3个相关的系统调用

epoll_create

由浅入深理解高级IO--select poll epoll_第10张图片

epoll_ctl

由浅入深理解高级IO--select poll epoll_第11张图片

epoll_wait

由浅入深理解高级IO--select poll epoll_第12张图片

epoll 的工作原理

由浅入深理解高级IO--select poll epoll_第13张图片
由浅入深理解高级IO--select poll epoll_第14张图片
由浅入深理解高级IO--select poll epoll_第15张图片
由浅入深理解高级IO--select poll epoll_第16张图片

epoll的使用三部曲

调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册
调用epoll_wait, 等待文件描述符就绪;

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

接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开

数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)

事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,

epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.

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

epoll工作方式

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

假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait…

水平触发(LT

epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写

边缘触发Edge Triggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
当epoll检测到socket上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
epoll_wait 不会再返回了.
也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.

对比LT和ET

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.另一方面, ET 的代码复杂程度更高了.

ET模式要设置为非阻塞

LT会不会没读完造成数据丢失

在ET模式下没读完,会造成数据丢失。怎么保证把数据全读完呢,假如对方给我发来了4096个字节,可我并不知道对方给我发多少字节,期望缓冲区的大小是1024个字节,也就是我一次拷贝了1024字节,底层还有3074个字节,怎么把数据读完呢?不保证3074字节丢失。要循环读取就行,每次读取1024字节,当有一次小于1024字节后,就知道把数据读完了。

为什么ET一定要工作在非阻塞方式下:因为ET不读完就有可能数据丢失,就会倒逼程序员一直读,而要一直读,必须采用循环读,循环读最担心最后一次读,最后一次读有可能 缓存区已经没有任何数据可以读,这是就会读阻塞,如果在单进程里,整个进程都可能因为缓冲区里没数据而一直阻塞着,所以ET接口必须设置为非阻塞状态

epoll案例代码

Makefile文件
由浅入深理解高级IO--select poll epoll_第17张图片
epollServer.hpp

 1 #include "Sock.hpp"                                                                                               
    2 
    3 #define SIZE 64
    4 
    5 //每一个文件描述符都对应一个桶
    6 class bucket
    7 {
    8   public:
    9     char buffer[20];  //buffer用来存储对方给我发的数据,超过20个比特:就回显给用户
   10     int pos;
   11     int fd;
   12 
   13     bucket(int sock)
   14       :fd(sock)
   15       ,pos(0)
   16     {
   17       memset(buffer,0,sizeof(buffer));
   18     }
   19     ~bucket()
   20     {}
   21 };
   22 
   23 class EpollServer
   24 {
   25   private:
   26     int lsock;
   27     int port;
   28     int epfd;
   29   public:
   30     EpollServer(int _p = 8080)
   31       :port(_p)
   32   {}
   33     
   34     //将关心文件描述符添加到epoll模型中
   35     void AddEvent2Epoll(int sock,uint32_t event)
   36     {
   37       struct epoll_event ev;
   38       ev.events = event;
   39       if(sock  == lsock ) //监听套接字
   40       {                                                                                                           
   41         ev.data.ptr = nullptr;
   42       }
   43       else 
   44       {
   45         ev.data.ptr = new bucket(sock);
   47       //用户告诉内核,你要帮我关心哪些文件描述符的哪些事件
   48       epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev);
   49 
   50     }
   51     void DelEventFromEpoll(int sock)
   52     {
   53       close(sock);
   54       epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
   55     }
   56     void InitServer()
   57     { 
   58       //创建套接字
   59       lsock = Sock::Socket();
   60       Sock::Setsockopt(lsock); //接口复用
   61       Sock::Bind(lsock,port);
   62       Sock::Listen(lsock);
   63                                                                                                                   
   64       epfd = epoll_create(256);  //创建epoll模型,epfd就是文件描述符
   65       if(epfd < 0)                //创建模型失败
   66       {
   67         cerr << "epoll_create error" << endl;
   68         exit(5);
    69       }
   70       cout << "Listen sock: " << lsock <<endl;
   71       cout << "epoll fd: " << epfd << endl;
   72     }
   73     void HandlerEvents(struct epoll_event revs[],int num)
   74     {
   75       for(int i=0;i < num ;i++)
   76       {
   77         uint32_t ev = revs[i].events;  //ev是就绪事件
   78         if(ev & EPOLLIN)     //读就绪
   79         {
   80           //读分两类:
   81           //1、是监听套接字文件描述符就绪
   82           //2、普通文件描述符就绪
   83           if(revs[i].data.ptr != nullptr)
   84           {
   85             //普通文件描述符
   86             bucket* bp = (bucket*)revs[i].data.ptr;                                                               
   87             //从bp->fd里读,读到bp->buffer里,期望读sizeof(bp->buffer)大小,已经读bp个大小了
   88             //要减去bp的大小
   89             ssize_t s = recv(bp->fd,bp->buffer+bp->pos,sizeof(bp->buffer)-bp->pos,0);
   90             if(s > 0) //读成功
   91             {
    92               bp->pos += s;   //更新bp
   93               cout << "client " << bp->buffer <<endl;  //打印读到的内容
W> 94               if(bp->pos >= sizeof(bp->buffer))  //说明读够buffer大小的内容
   95               {
   96                 //将对应的文件描述符读事件改为写事件
   97                 struct epoll_event temp; 
   98                 temp.events  = EPOLLOUT;  //改为输出
   99                 temp.data.ptr = bp;
  100                 epoll_ctl(epfd,EPOLL_CTL_MOD,bp->fd,&temp); //EPOLL_CTL_MOD修改,修改fd,改为temp事件
  101               }
  102             }
  103             else if(s == 0) //对方把文件描述符关了,我们要把节点从红黑树中拿掉
  104             {
  105               DelEventFromEpoll(bp->fd);
  106               delete bp;
  107             }
  108             else 
  109             {                                                                                                     
  110               //TODU
  111             }
  112           }
  113           else 
  114           {
  115             //listen sock 
  116             int sock = Sock::Accept(lsock);
  117             if(sock > 0) //获取成功
  118             {
  119               //将文件描述符添加到红黑树里(epoll中)
  120               AddEvent2Epoll(sock,EPOLLIN);  //添加为读事件
  121             }
  122           }
  123         }
  124           else if(ev & EPOLLOUT) 
  125           {
  126             //write 将buffer里的数据写回去
  127             bucket* bp = (bucket*)revs[i].data.ptr;
  128             send(bp->fd,bp->buffer,sizeof(bp->buffer),0);
  129             DelEventFromEpoll(bp->fd);
  130             delete bp;
  131           }                                                                                                       
  132           else 
  133           {
  134             // other events 
  135             
  136           }
  
  137         }
  138     }
  139     void Start()
  140     {
  141       //把一个事件添加到epoll模型
  142       AddEvent2Epoll(lsock,EPOLLIN);   //listen套接字,只关心读事件
  143       int timeout = 1000;
  144       //定义一个缓冲区,用来存放已经就绪的事件
  145       struct epoll_event revs[SIZE];
  146       for(;;)
  147       {
  148         //epoll_wait:OS告诉用户哪些文件描述符已经就绪了,所有就绪的文件描述符都会写在revs中
  149         //revs中有SIZE个文件描述符
  150         //num为就绪的文件描述符个数
  151         int num = epoll_wait(epfd,revs,SIZE,timeout);
  152         switch(num)
  153         {
  154           case 0:                                                                                                 
  155             cout << "time out ..." <<endl;  
  156             break;
  157           case -1:
  158             cerr << "epoll_wait error" <<endl;
  159           default:  //有事件就绪了
   160             HandlerEvents(revs,num);  //revs中有num的事件就绪
  161             break;
  162         }
  163 
  164       }
  165     }
  166 
  167     ~EpollServer()
  168     {
  169       close(lsock);
  170       close(epfd);
  171     }
  172 };                                          

Sock.hpp

 1 #pragma once
  2 
  3 #include<iostream>
  4 #include<string>
  5 #include<sys/socket.h>
  6 #include<netinet/in.h>
  7 #include<sys/types.h>
  8 #include<unistd.h>
  9 #include<stdlib.h>
 10 #include<strings.h>
 11 #include<arpa/inet.h>
 12 #include<sys/select.h>
 13 #include<sys/epoll.h>
 14 #include<cstring>                                                                                                   
 15 using namespace std;
 16 class Sock
 17 {
 18   public:
 19     static int Socket()
 20     {
 21       int sock = socket(AF_INET,SOCK_STREAM,0);
 22       if(sock < 0)
 23       {
 24         cerr << "socket error" <<endl; 
 25         exit(2);
 26       }
 27       return sock;
 28     }
 29     static void Bind(int sock,int port)
 30     {
 31       struct sockaddr_in local;
 32       bzero(&local,sizeof(local));
 33 
 34       local.sin_family = AF_INET;
 35       local.sin_port = htons(port);
 36       local.sin_addr.s_addr = htonl(INADDR_ANY);
 37 
 38       if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
 39       { 
 40          cerr << "bind error" <<endl;                                                                               
 41          exit(3);
 42       }   
 43     }
 44     static void Listen(int sock)
 45     {
 46       if(listen(sock,5) < 0 )
 47       {
 48         cerr << "listen error" << endl;
 49         exit(4);                                                                                                    
 50       }
 51     }
 52 
 53     static int Accept(int sock)
 54     {
 55       struct sockaddr_in peer;
 56       socklen_t len = sizeof(peer);
 57       int fd = accept(sock,(struct sockaddr*)&peer,&len);
 58       if(fd < 0)
 59       {
 60         cerr << "accept error" <<endl;
 61       }
 62       return fd;
 63     }
 64     static void Setsockopt(int sock)
 65     {
 66       int opt = 1;
 67       setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt,sizeof(opt));
 68     }
 69 };   

main.cc

  1 #include "epollServer.hpp"
  2 
  3 
  4 int main()
  5 {
  6   EpollServer* es = new EpollServer(8081);                                                                          
  7   es->InitServer();
  8   es->Start();
  9   return 0;
 10 }


总结

select总结:
先将文件描述符默认保存在一个fd.array数组里,一开始先将listen套接字保存在数组fd_array中,接下来使用select之前要对文件描述符的输入输出参数重新设定,可是重新设定时,要知道把那些文件描述符设置进rfds中,此时需要用for循环遍历数组的方式添加进来,第一次fd_array中只有一个合格的文件描述符lsock.

select中,关心的10个文件描述符右有6个就绪了,我怎么知道哪6个文件描述符就绪了,需要遍历数组去找。

epoll中,所有就绪的文件描述符及其事件,当revsa返回时,会以0为下标开始,依次填入已经就绪的就绪队列中,epoll_wait的返回值代表着有几个文件描述符就绪。

你可能感兴趣的:(Linux,多路转接,高级IO,select,poll,epoll)