多路io- epoll

4-1简介

​ 它是linux中内核实现io多路/转接复用的一个实现。(epoll不可跨平台,只能用于Linux)io多路转接是指在同一个操作里,同时监听多个输入输出源,在其中一个或多个输入输出源可用时范慧慧这个源,然后对其进行操作。

​ epoll采用红黑树来管理待检测的集合,而poll和select都是以线性的方式进行管理。随着集合的增大,select和poll的检测速度会逐渐变慢,而epoll凭着红黑树的性质使得自己不会慢很多,所以使用epoll效率高。

​ select和poll在工作过程中会对内核/用户空间数据进行拷贝。而在epoll中,内核和用户区使用的是共享内存,省去了没有必要的拷贝。

​ epoll会直接返回那些文件描述符是已经准备就绪的,而select和poll则需要我们自己想办法判断。

​ epoll没有最大文件描述符的限制,仅受系统可以打开多少文件描述符的限制。

​ 当多路复用的文件数量庞大,io操作频繁时,可使用epoll。

4-2 epoll的相关函数

头文件:#include <sys/epoll>

epoll_create函数:

函数原型:int epoll_create(int size);

函数功能:创建一个红黑树模型实例,用于管理待检测的文件描述符集合。

函数参数

size:这个参数在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值即可。

函数返回值

成功:返回一个有效的文件描述符,通过它就可以访问创建的epoll实例了。

失败:返回-1

epoll_ctl函数:

函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数功能:用于管理红黑树上的结点,可以进行添加,删除,修改等操作。

函数参数

epfd:这个是epoll_create返回的文件描述符,通过它找到对应的epoll实例。

op:这是一个枚举值,控制通过该函数执行什么操作

​ EPOLL_CTL_ADD:往epoll模型中添加新的节点。

​ EPOLL_CTL_MOD:修改epoll模型中已经存在的节点。

​ EPOLL_CTL_DEL:删除epoll模型中的指定的节点。

fd:填要添加/修改/删除的文件描述符。

event:epoll事件,用来修饰第三个参数对应的文件描述符,指定检测这个文件描述符的某些事情。

​ EPOLLIN:读事件,接收数据,如果检测到读缓冲区有动静,代表该文件描述符就绪。

​ EPOLLOUT:写事件,发送数据,如果检测到写缓冲区有动静,代表该文件描述符就绪。

​ EPOLLERR:异常事件。

data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。

函数返回值

成功:返回0

失败:返回-1

结构体struct epoll_event的成员

​ uin32_t events:监听的事件类型

​ epoll_data_t data它是一个epoll_data共用体,代表用户数据

共用体epoll_data:

​ void *ptr:存储一个指针

​ int fd:存储一个文件描述符

​ uint32_t u32:存储一个32位无符号整数

​ uint64_t u64:存储一个42位无符号整数

epoll_wait函数:

函数原型:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

函数功能:检测创建的epoll实例中有没有就绪的文件描述符。

函数参数

epfd:填epoll_create函数的返回值,通过它找到对应的epoll实例。

events:传出参数,一个结构体数组地址,里面存储了已就绪的文件描述符信息。

maxevents:修饰函数的第二个参数,表示结构体数组的容量或者说是元素个数。

timeout:代表检测的epoll实例中没用已经就绪的文件描述符,则该函数的阻塞时长为多少,单位毫秒。

  0:函数不阻塞,无论epoll实例中有没有就绪的文件描述符,函数调用后都直接返回。

  大于0:如果epoll实例中没有已就绪的文件描述符,则阻塞对应毫秒数再返回。

   -1:函数一直阻塞,直至epoll实例中有已就绪的文件描述符之后才解除阻塞。

函数返回值

成功

​ 大于0:则代表检测到的已就绪的文件描述符总个数。

​ 等于0:则代表函数没有检测到满足条件的文件描述符,然后阻塞被解除了。

失败:返回-1

4-3 在服务端使用epoll实现io多路转接的步骤:

第一步:使用socket函数创建一个用于监听的套接字描述符lfd。

第二步:使用bind函数将用于监听的描述符lfd与服务端本地地址进行绑定。

第三步:将lfd设置为监听态。

第四步:创建一个struct epoll_event 变量ev,用来每次为epoll实例送入要检测的描述符。创建一个相同类型的结构体数组evs,用于存储epoll_wait函数每次调用后获得的结果。

第五步:创建一个epoll实例,并且将用于监听的文件描述符lfd通过epoll_ctl函数加入到epoll实例中,然后让epoll检测lfd的读缓冲区。

第六步:进入无限循环,在循环中使用epoll_wait函数不断检测epoll实例中有没有描述符就绪,如果有文件描述符就绪了,那么epoll_wait函数则会返回就绪的文件描述符个数,同时会将就绪的文件描述符都存放到刚才我们创建的evs数组当中去,然后我们就可以对这个数组进行遍历,来挨个处理里面已经准备就绪的文件描述符。因为一开始我们将监听描述符lfd放入了epoll实例中了,所以epoll_wait修改的evs数组中,可能既监听到请求的lfd也包含其他与客户端建立连接的文件描述符。所以现在分两种情况:

n 如果是lfd也就是监听描述符就绪了,那么代表监听到了新的客户端连接请求。当我们在在遍历evs数组时,如果发现某个就绪的文件描述符是用于监听的lfd,则使用lfd通过accept函数与客户端建立连接,然后将accept返回的已经建立连接的文件描述符通过epoll_ctl函数添加到epoll实例中去,同时我们可以设置让epoll监听它那些缓冲区。

n 如果是普通的与客户端进行连接的文件描述符就绪了,那么代表有客户的向服务端发送数据了,那么我们可以使用这个文件描述符与客户端进行通信了,记得在通常的过程中要判断客户端是否已经关闭链接或者其他的一些什么行为,如果客户端断开了链接,那么我们可以通过epoll_ctl函数将这个文件描述符从epoll实例中删除掉。

第七步:重复第六步,直至我们服务端不需要接收请求了,可以使用close函数关闭监听文件描述符lfd以及epoll创建的实例,就此结束。

4-4 使用epoll实现io多路转接的服务端代码实例(epoll反应堆):

点击查看代码
#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/epoll.h>

#define MAX_NUM 128 //待检测的描述符的最大数量

int main()

{

  struct sockaddr_in serv;//用于记录服务端本地地址

  struct epoll_event ev; //用于每次往epoll实例中送入数据

  struct epoll_event evs[MAX_NUM]; //用于记录每次epoll_wait函数检测就绪的描述符

  int epfd; //用于记录epoll实例

  //创建套接字

  int lfd = socket(AF_INET, SOCK_STREAM, 0);

  if (lfd == -1) {

​     printf("创建套接字失败!\n");

​     exit(0);

  }

  //绑定

  serv.sin_family = AF_INET;

  serv.sin_port = htons(10066);

  serv.sin_addr.s_addr = htonl(INADDR_ANY);

  int rent = bind(lfd,(struct sockaddr *)&serv,sizeof(serv));

  if (rent == -1) {

​     printf("绑定套接字失败!\n");

​     exit(0);

  }

  //监听

  rent = listen(lfd,MAX_NUM);

  if (rent == -1) {

​     printf("监听失败!\n");

​     exit(0);

  }

  //创建epoll实例

  epfd = epoll_create(100);

  if (epfd == -1) {

​     printf("创建epfd实例失败!\n");

​     exit(0);

  }

  //将监听文件描述符添加到实例epfd中

  ev.events = EPOLLIN;//设置检测读缓存

  ev.data.fd = lfd;

  rent = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

  if (rent == -1) {

​     printf("监听文件描述符加入epoll实例失败!\n");

​     exit(0);

  }

  //持续检测

  int size = sizeof(evs) / sizeof(struct epoll_event);

  while (1)

  {

​     //调用该函数检测epfd实例中是否有文件描述符已经就绪,没有则一直阻塞(可改)

​     int num = epoll_wait(epfd,evs,size,-1);

​     

​     //循环遍历evs数组,并且处理就绪的文件描述符

​     for (int i = 0; i < num; i++) {

​       //取出文件描述符

​       int log_fd = evs[i].data.fd;

​       //判断它是不是监听文件描述符,如果是则代表有客户端连接请求

​       if (log_fd == lfd) {

​         //建立连接

​         int cfd = accept(lfd, NULL, NULL);

​         //将新得到的与客户端建立连接的文件描述符添加到epoll实例中

​         ev.events = EPOLLIN;//这里检测读缓存,可以根据实际情况更改

​         ev.data.fd = cfd;

​         rent = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

​         if (rent == -1) {

​           printf("监听文件描述符加入epoll实例失败!\n");

​           exit(0);

​         }

​       }

​       else {

​         //如果不是监听描述符那就是与客户端通信的描述符

​         //你可以在这里与客户端进行通信,并且处理来自各项事物

​         //这里以简单的收到信息然后回复客户端为例子

​         char data[100];//用于存储来自客户端的信息

​         char serv_data[100] = "服务端已经收到你的数据!";

​         memset(data, 0, sizeof(data));

​         int len = recv(log_fd, data, sizeof(data),0);

​         if (len == 0) {

​           printf("客户端已经关闭连接!\n");

​           //将该文件描述符从epoll实例中去除

​           epoll_ctl(epfd,EPOLL_CTL_DEL,log_fd,NULL);

​           //关闭该描述符

​           close(log_fd);

​         }

​         else if (len > 0) {

​           //代表接收到了数据

​           printf("来自客户端:%s\n",data);

​           if (send(log_fd,serv_data,sizeof(serv_data),0) == -1) {

​             printf("服务器回应客户端失败!\n");

​           }

​         }

​         else {

​           perror("recv");

​           exit(0);

​         }

​       }

​     }

  }

  close(epfd);//关闭epoll实例

  close(lfd); //关闭监听文件描述符

  return 0;

}

4.5 epoll的水平触发模式LT

​ 当文件描述符上有事件发送时,epoll_wait函数会立刻返回,并且返回所以处于就绪状态的文件描述符。但如果我们对已就绪的文件描述符进行了数据处理后仍有一些文件描述符上剩下了没有处理完的数据,那么下次调用epoll_wait函数时任然会返回该文件描述符。

​ 水平模式适用于阻塞式io或阻塞式io,并且epoll默认的是水平触发模式。

4.6 epoll的边沿触发模式ET

​ 在边沿触发模式下,如果文件描述符就绪了则epoll_wait函数会返回就绪的文件描述符,但是如果我们在对文件描述符处理时没有处理完,文件描述符上依然剩余着某些数据或者缓冲区未满,那么下一次epoll_wait检测时遇到这个没有处理完的描述符,将不再通知。直到有新的数据到来。

​ 综上所述:epoll在边沿模式下epoll_wait检测到文件描述符有新事件时才会通知,如果不是新事件,则不通知,当通知的次数减少后,效率才能提高。

4.6.1水平模式切换为边沿模式的方法

第一步:在为epoll实例添加文件描述符时,我们会用到一个结构体struct epoll_event,其中中有一个events 成员,将它按照你的实际需求设置为以下内容即可:

​ EPOLLIN | EPOLLET:设置边沿触发模式,并且监听可读事件

​ EPOLLOUT | EPOLLET:设置边沿触发模式,并且监听可写事件

第二步:对于读缓冲区,因为边沿模式只会提醒一次,如果我们操作完某个文件描述符后发现文件描述符 的读缓冲区还有剩余数据,但是接下来epoll_wait就不会提醒整个文件描述符了。所以为了避免数据的丢失和 处理不完整,我们就需要一次性读完整个缓冲区的内容,假设读缓冲区中的内容特别多,我们一次性读不完我 们该怎么办?(写缓冲区不需要担心)

第一种方法:准备一块特大的内存。假设读缓冲区有1MB的数据,那么我们准备2MB来接收这个数据, 像这种通过提前准备一块大的内存的方法来接收数据的方法存在一些弊端:1.系统能够分配的内存是有上限的。 2.内存的大小没有一个统一的设定标准,太多了浪费,太小了不够使用。

第二种方法:通过循环的方式来接收数据,假设1024kb的数据,我们只有100kb的内存用于接收数据, 那么我们可以循环11次来接收这些数据。但是这存在一个大大的弊端,因为套接字是默认阻塞的。当调用read 和recv函数时,如果缓冲区的数据被读完后,当前进程和线程就阻塞了,这样我们就无法进行其他操作了。在 边沿模式下,我们必须将套接字设置为非阻塞。以下是设置非阻塞的方法:

第一步:int flags = fcntl(cfd,F_GETFL); cfd是用于通信的套接字,也是要设置为非阻塞的套接字。

第二步:flags |= O_NONBLOCK; 我们通过按位或操作符将 O_NONBLOCK 标志位设置到 flags 变量中,表示将套接字设置为非阻塞模式。

第三步:fcntl(cfd, F_SETGL, flags); 我们使用 F_SETFL 参数将新的标志设置回套接字。

注意:调用fcntl函数需要头文件#include <fcntl.h>。当缓冲区数据被读完了,调用的read()/recv()函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1

多路io复用epoll [补档-2023-07-20]的更多相关文章

  1. Redis03——Redis之单线程+多路IO复用技术

    Redis 是单线程+多路IO复用技术 多路复用:使用一个线程来检查多个文件描述符的就绪状态 如果有一个文件描述符就绪,则返回 否则阻塞直到超时 得到就绪状态后进行真正的操作可以在同一个线程里执行,也 ...

  2. Linux C++ 网络编程学习系列(5)——多路IO之epoll边沿触发

    多路IO之epoll边沿触发+非阻塞 源码地址:https://github.com/whuwzp/linuxc/tree/master/epoll_ET_LT_NOBLOCK_example 源码说 ...

  3. Linux C++ 网络编程学习系列(6)——多路IO之epoll高级用法

    poll实现多路IO 源码地址:https://github.com/whuwzp/linuxc/tree/master/epoll_libevent 源码说明: server.cpp: 监听127. ...

  4. Linux C++ 网络编程学习系列(4)——多路IO之epoll基础

    epoll实现多路IO 源码地址:https://github.com/whuwzp/linuxc/tree/master/epoll 源码说明: server.cpp: 监听127.1:6666,功 ...

  5. 多路IO复用模型--select, poll, epoll

    select 1.select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数 2.解决1024以下客户端时使用se ...

  6. 3高并发server:多路IO之epoll

     1 epoll epoll是Linux下多路复用IO接口select/poll的增强版本号,它能显著提高程序在大量并.发连接中仅仅有少量活跃的情况下的系统CPU利用率,由于它会复用文件描写叙述符 ...

  7. IO复用——epoll系列系统调用

    1.内核事件表 epoll是Linux特有的I/O复用函数.epoll把用户关心的文件描述上的事件放在内核里的一个事件表中,并用一个额外的文件描述符来标识该内核事件表.这个额外文件描述符使用函数epo ...

  8. Linux企业级项目实践之网络爬虫(27)——多路IO复用

    与多线程和多进程相比,I/O多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程. 主要应用: (1)客户程序需要同时处理交互式的输入和服务器之间的网络连接 (2) ...

  9. 协程与多路io复用epool关系

    linux上其实底层都基于libevent.so模块实现的,所以本质一样 gevent更关注于io和其它 epool只是遇到io就切换,而gevent其它等待也切换

  10. python3.x 多路IO复用补充asyncio

    asyncio模块是python之父写的模块,按说应该是靠谱的,python3.6版本定义为稳定版本. 说明书:https://docs.python.org/3/library/asyncio.ht ...

随机推荐

  1. AC(AtCoder) Library 文档翻译

    AC(AtCoder) Library Document下载使用 如何安装 首先在 Github 上找到 ac-library 仓库.下载最新版本 解压 zip 文件后将 atcoder ,放置GCC ...

  2. Educational Codeforces Round 104 (Rated for Div. 2) A-E 个人题解

    比赛链接 1487A. Arena n 个 Hero,分别有 \(a_i\) 的初始等级.每次两个 Hero 战斗时:等级相同无影响,否则等级高的英雄等级+1.直到某个英雄等级到了 \(100^{50 ...

  3. 图解 Promise 实现原理(二)—— Promise 链式调用

    本文首发于 vivo互联网技术 微信公众号 链接: https://mp.weixin.qq.com/s/Xz2bGaLxVL4xw1M2hb2nJQ作者:Morrain 很多同学在学习 Promis ...

  4. node-sass安装失败问题

    在node 中安装sass依赖总会出现各种各样的问题,第一次遇见这样的问题 Cached binary found at C:\Users\ltzhouhuan\AppData\Roaming\npm ...

  5. 项目启动一直 DruidDataSource inited,启动很慢

    解决方案: 由于在调试过程中,代码中设置了断点,在启动的时候,服务就一直卡住了,当关闭断点,重新启动就ok了.

  6. P5733 【深基6.例1】自动修正

    1.题目介绍 2. 题解 2.1 字符串大小写转换 思路 str[i] -= 'a' -'A'; 注意这里转换方式,即减去偏移量(ASCII码表中,'a'在'A'前面,如果记不得偏移量,就直接写'a' ...

  7. Python Code_05位运算

    coding:utf-8 author : 写bug的盼盼 development time : 2021/8/28 7:16 print(4&8)#非1即0 print(4|2)#同0即0, ...

  8. [转帖]CentOS 8已经停止维护,怎么检查CentOS的版本

    https://rumenz.com/rumenbiji/centos-check-version.html CentOS 8 在2021年12月31日停止更新并停止维护(EOL). CentOS 7 ...

  9. [转帖]谈谈对K8S CNI、CRI和CSI插件的理解

    K8S的设计初衷就是支持可插拔架构,解决PaaS平台不好用.不能用.需要定制化等问题,K8S集成了插件.附加组件.服务和接口来扩展平台的核心功能.附加组件被定义为与环境的其他部分无缝集成的组件,提供类 ...

  10. 关于sar的学习

    关于sar的学习 背景 公司一套基于某冷门Python架构的系统前几天出现异常卡顿. 当时安装的时候必须使用ubuntu系统. 所以当时默认安装的ubuntu1804, 本来想尝试使用一下sar查看卡 ...