zoukankan      html  css  js  c++  java
  • Linux 网络编程>epoll<LT/ET模式整理(~相逢何必曾相识~)

    今天自己整理一下epoll,网上有很多经典的介绍,看了很多~收藏了很多~还是整理一下做个积累,

    自己的东西好找~

    1. epoll 模型简介

    epoll 是Linux I/O 多路复用接口 select/poll 的加强版,首字母e(enhacement)中文翻译就 加强/提高

    顾名思义,很强epoll模型会显著提高程序在大量并发连接中只有少量活跃CPU系统的CPU利用率,

    它把用户关心的文件描述符上的事件放在内核的一个事件表中,无需像select和poll那样每次调用都

    重复传入文件描述符集,在获取事件的时候,无需遍历整个被监听的文件描述符集,而是遍历那些

    被内核IO事件异步唤醒而加入ready队列的描述符集合。所以epoll是Linux大规模高并发网络程序的

    首选模型。

    2. epoll 模型API

     epoll 通过使用一组函数来完成任务。

    2.1 epoll_create / epoll_create1 

    创建一个epoll句柄(epoll特有),用来唯一标识内核中这个事件表,这个特有的epoll文件描述符将是

    其他所有epoll系统调用的第一个参数(epollfd),以指定要访问的内核事件表。

    #include <sys/epoll.h>
    
    int epoll_create(int size);
    int epoll_create1(int flags);

    我们注意到,上面有两个create函数,它们的作用相同,后面一个create1是Linux内核2.6.27版本中新

    增加的函数关于两个函数了解:http://man7.org/linux/man-pages/man2/epoll_create.2.html

    size: 提示内核要监听的文件描述符个数,于内存大小有关。目前已被废弃

    2.2 epoll_ctl,该函数用来操作epoll的内核事件表

    #include <sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    // 成功返回0,出错返回-1

    参数说明:

      epfd: 就是函数epoll_create创建的epoll句柄,唯一

      op: 是指定的操作类型有三种

        EPOLL_CTL_ADD : 向epfd注册fd上的event

        EPOLL_CTL_MOD: 修改fd已注册的event

        EPOLL_CTL_DEL:  从epfd上删除fd的event

      fd: 是操作的文件描述符

      event: 指定内核要监听的事件,它是struct epoll_event结构类型的指针

    struct epoll_event {
       uint32_t    events; /* Epoll events */ 
      epoll_data_t data;  /* User data variable */ };

      events  成员描述事件类型,将以下宏定义通过位或方式组合:

       EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

       POLLOUT:表示对应的文件描述符可以写

       EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

       EPOLLERR:表示对应的文件描述符发生错误

       EPOLLHUP:表示对应的文件描述符被挂断;

       EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的

       EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再

               次把这个socket加入到EPOLL队列里

      data 用户数据变量

        用于存储用户数据,是epoll_data_t 结构类型,定义如下,epoll_data_t是一个联合体,fd指定事件从属

        目标文件描述符,ptr可以用来指定fd相关的用户数据,但两者不能同时使用。

    typedef union epoll_data {
      void     *ptr;
      int      fd;
      uint32_t   u32;
      uint64_t   u64;
    }epoll_data_t;

    2.3 epoll_wait,该函数用来等待监听文件描述符上有事件发生

    1 #include <sys/epoll.h>
    2 
    3 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    // 成功返回就绪的文件描述符个数,若出错返回 -1,超时返回0

    参数说明:

      epfd:           就是函数epoll_crate创建的句柄,唯一

      events:        是一个传入传出参数,是一个epoll_event结构指针,用来从内核得到事件集合

      maxevents: 告知内核events的大小,但不能大于epoll_create()时创建的size

      timeout:       是超时事件,-1为阻塞,0为立即返回,非阻塞,大于0是指定的微妙。

    3. LT和ET触发模式(正文,[临摹] ~千呼万唤始出来~)

    3.1 概念

    LT(Level_triggered 水平触发): 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知

    处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),       那么下次调用

    epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,

    它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,  而它们每次都会返回

    这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

    ET(Edge_triggered 边沿触发): 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知

    处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),                  那么下次调用

    epoll_wait()时,它不会通知你  也就是它只会通知你一次,直到该文件描述符上出现第二次可读写

    事件才会通知你 这种模式比水平触发效率高  系统不会充斥大量你不关心的就绪文件描述符!!!

    阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直

    阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。  当你去写一个阻塞的文件描述符时

    如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞  直到有空间可写。以上的

    读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据, 还包括接收连

    接accept(),发起连接connect()等操作...

    非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回, 返回成

    功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理

    它不会像阻塞IO那样,卡在那里不动!!!

    3.2 几种IO模型的触发方式

    select(), poll()模型都是水平触发模式,信号驱动IO是边沿触发模式,      epoll()模型即支持水平触发

    也之处边沿触发,默认水平触发(LT)

    3.3 Demo

    约定

    这里讨论epoll的LT和ET,以及阻塞IO和非阻塞IO对它们的影响,        对应监听的socket文件描述符

    用sockfd,对于accept() 返回的文件描述符(即要读写的文件描述符)用connfd表示。

    场景

      1) 水平触发的非阻塞sockfd

      2) 边沿触发的非阻塞sockfd

      3) 水平触发的阻塞connfd

      4) 水平触发的非阻塞connfd

      5) 边沿触发的阻塞connfd

      6) 边沿触发的非阻塞connfd

    以上没有验证阻塞的sockfd,因为epoll_wait()返回必定是已就绪的连接,设不设置阻塞accept()都会立

    即返回。例外:UNP里面有个例子,在BSD上,使用select()模型。设置阻塞的监听sockfd时, 当客户

    端发起连接请求,由于服务器繁忙没有来得及accept(),此时客户端自己又断开,           当服务器到达

    accept()时,会出现阻塞。本机测试epoll()模型没有出现这种情况,我们就暂且忽略这种情况!!!

    代码:文件名 epoll_lt_et.c

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <string.h>
      4 #include <errno.h>
      5 #include <unistd.h>
      6 #include <fcntl.h>
      7 #include <arpa/inet.h>
      8 #include <netinet/in.h>
      9 #include <sys/socket.h>
     10 #include <sys/epoll.h>
     11 
     12 // 最大缓冲区大小
     13 #define MAX_BUFFER_SIZE     5
     14 
     15 // epoll最大监听数
     16 #define MAX_EPOLL_EVENTS    20   
     17 
     18 // LT 模式
     19 #define EPOLL_LT            0
     20 
     21 // ET 模式
     22 #define EPOLL_ET            1
     23 
     24 // 文件描述符设置为阻塞
     25 #define FD_BLOCK            0
     26 
     27 // 文件描述符设置为非阻塞
     28 #define FD_NONBLOCK         1
     29 
     30 
     31 // 设置fd 为非阻塞
     32 int 
     33 set_nonblock(int fd) 
     34 {   
     35     /* 获取文件的flags */
     36     int flags = fcntl(fd, F_GETFL);
     37     /* 设置文件的flags 为非阻塞*/
     38     fcntl(fd, F_SETFL, flags | O_NONBLOCK);
     39     return flags; 
    40
    } 41 42 // 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) 43 void 44 addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type) 45 { 46 struct epoll_event ep_event; 47 ep_event.data.fd = fd; 48 ep_event.events = EPOLLIN; 49 50 /* 如果是ET模式,设置EPOLLET */ 51 if(epoll_type == EPOLL_ET) 52 ep_event.events |= EPOLLET; 53 54 /* 设置非阻塞 */ 55 if(block_type == FD_NONBLOCK) 56 set_nonblock(fd); 57 58 /* 注册epoll_fd(epoll_create生成的epoll专用描述符) 文件描述符上的事件 59 EPOLL_CTL_ADD -> 注册 60 EPOLL_CTL_MOD -> 修改 61 EPOLL_CTL_DEL -> 删除 62 */ 63 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event); 64 } 65 66 // LT 处理流程 67 void 68 epoll_lt(int sockfd) 69 { 70 char buff[MAX_BUFFER_SIZE]; 71 int ret; 72 73 memset(buff, 0, MAX_BUFFER_SIZE); 74 printf("======> 水平触发模式开始recv ...\n"); 75 ret = recv(sockfd, buff, MAX_BUFFER_SIZE, 0); 76 printf("ret = [%d]\n", ret); 77 if(ret > 0) 78 printf("收到消息:%s, 共%d字节\n", buff, ret); 79 else { 80 if(ret == 0) 81 printf("客户端主动关闭.\n"); 82 close(sockfd); 83 } 84 85 printf("======> 水平触发处理结束 \n"); 86 } 87 88 // 带循环的ET处理流程 89 void 90 epoll_et_loop(int sockfd) 91 { 92 char buff[MAX_BUFFER_SIZE]; 93 int ret; 94 95 printf("======> 带循环的ET开始recv数据...\n"); 96 while(1) { 97 memset(buff, 0, MAX_BUFFER_SIZE); 98 ret = recv(sockfd, buff, MAX_BUFFER_SIZE, 0); 99 if(ret == -1) { 100 if(errno == EAGAIN || errno == EWOULDBLOCK) { 101 printf("循环读完所有数据.\n"); 102 break; 103 } 104 close(sockfd); 105 break; 106 } else if(ret == 0) { 107 printf("客户端主动关闭请求.\n"); 108 close(sockfd); 109 break; 110 } else 111 printf("收到消息:%s, 共%d个字节.\n", buff, ret); 112 } 113 114 printf("======> 带循环ET处理结束\n"); 115 } 116 117 // 不带循环的ET处理流程,比epoll_et_loop少了while循环 118 void 119 epoll_et_nonloop(int sockfd) 120 { 121 char buff[MAX_BUFFER_SIZE]; 122 int ret; 123 124 printf("======> 不带循环的ET开始recv数据...\n"); 125 memset(buff, 0, MAX_BUFFER_SIZE); 126 ret = recv(sockfd, buff, MAX_BUFFER_SIZE, 0); 127 if(ret > 0) 128 printf("收到消息:%s, 共%d个字节.\n", buff, ret); 129 else { 130 if(ret == 0) 131 printf("客户端主动关闭.\n"); 132 close(sockfd); 133 } 134 135 printf("======> 不带循环ET处理结束\n"); 136 } 137 138 // 处理epoll的返回结果 139 void 140 epoll_process(int epollfd, struct epoll_event *events, int number, 141 int sockfd, int epoll_type, int block_type) 142 { 143 144 struct sockaddr_in client_addr; 145 socklen_t client_addrlen; 146 int newfd, connfd; 147 int i; 148 149 for(i = 0; i < number; i++) { 150 newfd = events[i].data.fd; 151 if(newfd == sockfd) { 152 printf("======> 新一轮accept() <======\n"); 153 printf("accept()开始...\n"); 154 155 /* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */ 156 sleep(3); 157 client_addrlen = sizeof(client_addr); 158 connfd = accept(sockfd,(struct sockaddr*)&client_addr,&client_addrlen); 159 printf("connfd = [%d]\n", connfd); 160 161 /* 注册已连接的socket到epoll,并设置LT还是ET,是阻塞还是非阻塞 */ 162 addfd_to_epoll(epollfd, connfd, epoll_type, block_type); 163 printf("accept()结束!!!\n"); 164 165 }else if(events[i].events & EPOLLIN) { 166 167 /* 可读事件处理流程 */ 168 if(epoll_type == EPOLL_LT) { 169 printf("======> 水平触发开始...\n"); 170 epoll_lt(newfd); 171 172 }else if(epoll_type == EPOLL_ET) { 173 174 printf("======> 边沿触发开始...\n"); 175 /* 带循环的ET模式 */ 176 epoll_et_loop(newfd); 177 178 /* 不带循环的ET模式*/ 179 //epoll_et_nonloop(newfd); 180 } 181 }else 182 printf("其他事件发生...\n"); 183 } 184 185 } 186 187 // 出错处理函数 188 void 189 err_exit(char *msg) 190 { 191 perror(msg); 192 exit(1); 193 } 194 195 // 创建socket 196 int 197 creat_socket(const char *ip, const int port_number) 198 { 199 //struct sockaddr_in server_addr = {AF_INET}; 200 struct sockaddr_in server_addr; 201 int sockfd, reuse = 1; 202 203 memset(&server_addr, 0, sizeof(server_addr)); 204 server_addr.sin_family = AF_INET; 205 server_addr.sin_port = htons(port_number); 206 if(inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1) 207 err_exit("inet_pton() error."); 208 209 /* 创建socket */ 210 if((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) 211 err_exit("socket() error."); 212 213 /* 设置复用socket地址 */ 214 if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) 215 err_exit("setsockopt() error."); 216 217 /* 绑定 */ 218 if(bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) 219 err_exit("bind() error."); 220 221 /* 监听 */ 222 if(listen(sockfd, 5) == -1) 223 err_exit("listen() error."); 224 225 return sockfd; 226 } 227 228 229 int main(int argc, char *argv[]) 230 { 231 if(argc < 3) { 232 fprintf(stderr,"Usage: %s ip_address port_number\n", argv[0]); 233 exit(1); 234 } 235 236 int sockfd, epollfd, number; 237 238 sockfd = creat_socket(argv[1], atoi(argv[2])); 239 struct epoll_event events[MAX_EPOLL_EVENTS]; 240 241 /* 242 linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能 243 并去掉了无用的size参数 244 */ 245 if((epollfd = epoll_create1(0)) == -1) // 注意这里epoll_create1是数字1,不是字母'l' 246 err_exit("epoll_create1() error."); 247 248 /* 249 * 以下设置针对监听的sockfd, 当epoll_wait返回时,必定有事件发生, 250 * 所以这里我们忽略罕见的情况外设置阻塞IO没意义,我们设置为非阻塞IO 251 **/ 252 253 /* sockfd -> 非阻塞LT模式 */ 254 addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK); 255 256 /* sockfd -> 非阻塞的ET模式 */ 257 //addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK); 258 259 while(1) { 260 261 number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1); 262 if(number == -1) 263 err_exit("epoll_wait() error."); 264 else { 265 /* 以下的LT,ET 以及是否阻塞都是针对accept()函数返回的文件描述符,即connfd */ 266 267 /* connfd: 阻塞式的LT模式 */ 268 epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK); 269 270 /* connfd: 非阻塞式的LT模式 */ 271 //epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK); 272 273 /* connfd: 阻塞式的ET模式 */ 274 //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK); 275 276 /* connfd: 非阻塞式的ET模式 */ 277 //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK); 278 } 279 280 } 281 282 close(sockfd); 283 return 0; 284 }

     写一个简单的Makefile 编译运行

    epoll_lt_et : epoll_lt_et.c
        gcc -W -Wall -o $@ $^
    clean:
        rm -fr epoll_lt_et

    4. 验证(~犹抱琵琶半遮面~)

    代码如上,还需要运行才能看到现象,(伍佰曰:来来来,干了这杯,还有三杯~)

    1. 验证水平触发非阻塞sockfd, 关键代码在 254行,编译运行

     这是我们运行后再另一个终端快速用5个客户端连接到服务器,可以看到5个终端连接都处理完成了返回

     新的connfd为5, 6, 7,8,9

    迅速连接5个客户端

    上面测试完后,我们批量kill 掉那5个客户端,继续后续测试

    2. 边沿触发非阻塞sockfd, 注释上面 254 行,打开 257 行,同样编译运行,快速在另一个终端里面创建

     5个客户连接,或者10个,这里创建了5个连接

    我们看服务器反应,5个客户端值处理了3个,说明在高并发时,会出现客户端连接不上的问题

    3. 水平触发阻塞connfd, 我们先把sockfd改回到水平触发,注释 257,打开254, 重点代码在 268, 编译

     运行用一个客户端连接,发送123456789,

    再看服务器的反应,可以看到水平触发了2次,因为我们代码缓冲区的大小为5个字节,处理代码一次接

    不完水平触发一直触发,直到数据全部读取完毕

    4. 水平触发非阻塞connfd.注释掉268,打开 271,同样上面测试,看结果,

     客户端发送12345678

    服务器

    5. 边沿触发阻塞connfd, 放开第 274,注释其他,先测试不带循环的的ET模式(即不循环读取数据,

     跟水平触发读取一样),在epoll处理函数中注释176,打开179编译运行,开启一个客户端发送123456789,

     服务器端,可以看到边沿触只触发了一次,只读取了5个字节

    继续在刚才的客户端上发送一个字符a,告诉epoll_wait(),有新的可读事件发生了,

    服务器端,服务器又触发了一次新的边沿触发,并继续读取上次没有读完的6789加一个回车符,

    这时我们在客户端继续发送一个字符b,

    服务器端,这个时候就会读取上次没读完的a加上上次的回车符,2个字节,还剩3个字节的缓冲区

    就可以读到本次的b和本次的回车符共4个字节

    此时我们就可看到,阻塞的边沿触发,如果不一次性读取一个事件上的数据,就会干扰下一个事件。

    下面我们验证一次性读取数据,即带循环的阻塞connfd,ET模式,和上面的读取方式不一样了,一次

    性读完注释 179, 放开 176,编译运行,客户端发送 123456789, 

    服务器端,可以看到一次全部把数据收到了,

    但是仔细看,程序没有输出"======> 带循环的ET处理结束<======", 是因为代码一直卡在

    了第 98recv函数上,因为是阻塞IO,如果没有数据可读,它会一直等在哪里,   直到有

    数据可读,如果这个时候用另外一个客户端去连接 服务器不会受理这个新的客户请求连接.

    6. 边沿触发的非阻塞connfd, 不带循环的ET,注释代码 176,274,打开179,277, 

    客户端先发送 1234567890,然后服务器端还是数据读取不完,第二次发一个a,通知epoll_wait()

    新的可读事件发生了,这次会将上次剩下的67890,5个字节读出,第三次在发一个b, 这们看

    到读出了5个字节,  是第一次的1234567890后的回车符+第二次的a和第二次的回车符+b+第

    三次的回车符共5个字节

    服务器端:

    这里我们开始正规边沿触发非阻塞connfd, 带循环的ET,注释代码 179, 打开 176,277

    编译运行,用一个客户端连接,发送1234567890,

    服务器端,可以看到数据全部一次读取完毕,处理函数也退出了,因为非阻塞IO如果没有数据可

    读时,  会立即返回,并设置error,我们这里是根据 EAGAIN和EWOULDBLOCK来判断数据全部

    读取完毕了,可以退出循环了

    这时我们用另一个客户端去连接,服务器依然可以正常接收请求;

    5. 总结(~此时无声胜有声~)

    1. 对于监听的sockfd,最好使用水平触发模式,边沿触发模式会导致高并发情况下,有的客户端会连接不上,

     如果非要使用边沿触发,网上有的方案是用while来循环accept( ).

    2. 对于读写的connfd水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置阻塞。

    3.对于读写的connfd边沿触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。

    6. 鸣谢 

    重点鸣谢以下二位,十分感谢,本文的参考模板,很适合我~ 
    
    感谢:https://blog.csdn.net/liu0808/article/details/52980413
    
    感谢:https://blog.csdn.net/men_wen/article/details/53456491

    后记:

      生活往往是

       也仅仅是

      我们现在经历的这一刻

  • 相关阅读:
    搭建ARL资产安全灯塔
    免杀技术发展史
    米酷CMS 7.0.4代码审计
    腾讯安全实习 应用运维安全面试
    Docker部署CTF综合性靶场,定时刷新环境
    西湖论剑2020MISC-Yusa_yyds
    (转)马云炮轰银行监管的解读
    ATT&CK 实战
    Docker环境复现利用Redis未授权访问漏洞 >> 批量扫描检测利用
    修改CH340芯片信息
  • 原文地址:https://www.cnblogs.com/zhaoosheLBJ/p/9268532.html
Copyright © 2011-2022 走看看