c++高级篇(四) ——Linux下IO多路复用之epoll模型

IO多路复用 —— epoll

前言

在之前我们就已经介绍过了select和poll,在作为io多路复用的最后一个的epoll,我们来总结一下它们之间的区别:
a

select

实现原理

select 通过一个文件描述符集合(fd_set)来工作,该集合可以包含需要监控的文件描述符。调用者会指定一个超时时间,如果在这个时间内没有任何描述符准备好,则函数返回。select 可以同时监听读、写和异常三种类型的事件。

优点

select 函数调用和实现比较简单,同时它支持跨平台

缺点
  1. select是基于位图这一数据结构来存储与遍历文件描述符相关的信息,由于位图数据结构的限制,select最多能同时监听1024个文件描述符,如果超过1024个文件描述符,无法实现大规模的并发处理。

  2. 每次调用select,需要拷贝位图,而且select属于用户态,网络通信属于内核态,需要拷贝两次,会影响select的性能。

  3. select的每次监听都需要遍历一整个位图,随着需要监听的socket增加,性能会大大下降。

poll

实现原理

poll 通过一个链表来存储需要监控的文件描述符,当文件描述符就绪时,链表中的节点会被移动到就绪链表中,当链表为空时,poll会阻塞。poll与select类似,也是通过一个文件描述符集合来工作,但是poll所使用的数据结构是一个结构体数组,它的结构如下:

struct pollfd
{int fd; //存储的socketshort events; // socket触发的事件short revents; // 返回的事件
}
优点

poll 函数调用和实现简单,同时它支持跨平台,同时相对于select,poll没有了1024的限制,可以实现对更多文件描述符的监听。

缺点

poll监视的连接数没有1024的限制,但是随着socket的增多,poll的效率会降低,无法处理超大规模并发。

epoll

epoll的原理

epoll 全名是 eventpoll,是Linux内核2.5.44 版本之后才出现的一个事件通知机制。它属于IO多路复用技术的一种形式,IO多路复用技术指的是一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其进行读写操作。上面的select和poll都具有一个通病,那就是都有性能瓶颈,而epoll可以承受百万级别的连接,属于select和poll模式的升级版。

epoll的优点
  • select和poll监听都是线性检测的,而epoll是基于红黑树来管理文件描述符,对于事件的发生,它不像select和poll那样需要遍历整个文件描述符集合,而是通过回调函数来通知,所以epoll的效率更高.

  • select和poll在工作中需要对集合进行判断来看哪些文件描述符已经就绪,而epoll则不需要,epoll通过回调函数来通知,所以epoll的效率更高。

当我们需要监听大量的文件描述符时,epoll的效率会更高,所以epoll是当前最常用的IO多路复用技术。

epoll的操作函数

在Linux内核中,主要给我们提供了一下三个函数来操作epoll:

#include <sys/epoll.h>
//创建一个epoll实例,通过红黑树来管理文件描述符
int epoll_create(int size);
//管理红黑树中的文件描述符(添加,修改,删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待文件描述符就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_create

epoll_create函数用于创建一个epoll实例,参数size表示要监听的文件描述符的数量,但是这个参数在Linux 2.6.8之后已经没有意义了,因为epoll的红黑树可以动态扩展,所以大于0即可。

int epfd = epoll_create(0);

返回值:

  • 成功:返回epoll实例的文件描述符
  • 失败:返回-1,并设置errno
epoll_ctl

epoll_ctl函数用于管理红黑树中的文件描述符,参数epfd表示epoll实例的文件描述符,参数op表示要进行的操作,参数fd表示要监听的文件描述符,参数event表示要监听的事件。

在epoll中有以下和事件相关的数据结构

typedef union epoll_data {void        *ptr;int          fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可uint32_t     u32;uint64_t     u64;
} epoll_data_t;struct epoll_event {uint32_t     events;      /* Epoll events */epoll_data_t data;        /* User data variable */
};

而在epool_ctl函数中主要要注意下面几个参数;

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
  • epfd:epoll实例的文件描述符
  • op:要进行的操作
    • EPOLL_CTL_ADD:添加文件描述符
    • EPOLL_CTL_MOD:修改文件描述符
    • EPOLL_CTL_DEL:删除文件描述符
  • fd:要监听的文件描述符
  • event:epoll事件,用来修饰fd,指定检测什么事件
    • events:事件类型
      • EPOLLIN:读事件
      • EPOLLOUT: 写事件
      • EPOLLERR: 错误事件
    • data:用户数据,通常情况下使用fd即可
epoll_wait

epoll_wait函数用于等待事件的发生,参数epfd表示epoll实例的文件描述符,参数events表示要监听的事件,参数maxevents表示最多监听多少个事件,参数timeout表示等待时间。

int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
  • epfd:epoll实例的文件描述符
  • events:要监听的事件
  • maxevents:最多监听多少个事件
  • timeout:等待时间
    • 0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
    • 大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
    • -1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞

epoll的使用

这里为实现了一个简单的基于epoll实现的服务端与客户端通讯,大家可以自己测试一下:


//server.cpp
#include <iostream>
#include <unistd.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <string.h>int main(int argc,char* argv[])
{if(argc!=3){std::cout<<"命令行参数数量不对"<<std::endl;std::cout<<"./demo port timeout"<<std::endl;exit(-1);}// 创建套接字int lfd=socket(AF_INET,SOCK_STREAM,0);if(lfd<0){perror("create socket");return -1;}//绑定struct sockaddr_in server_addr;memset(&server_addr,0,sizeof(server_addr));server_addr.sin_port=htons(atoi(argv[1]));server_addr.sin_family=AF_INET;server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//设置端口复用int opt=1;setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//绑定端口int ret=bind(lfd,(struct sockaddr*)&server_addr,sizeof(server_addr));if(ret<0){perror("bind");exit(-1);}//监听ret=listen(lfd,10);if(ret<0){perror("listen");}//创建epoll实例int epfd=epoll_create(100);if(epfd<0){perror("epfd");exit(-1);}struct epoll_event ev;ev.data.fd=lfd;ev.events=EPOLLIN;ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);if(ret<0){perror("epoll_ctl");}struct epoll_event evs[1024];int size=(sizeof(evs)/sizeof(struct epoll_event));while(1){int num=epoll_wait(epfd,evs,size,atoi(argv[2]));for(int i=0;i<num;i++){int fd=evs[i].data.fd;if(fd==lfd)  //如果是监听的socket{int cfd=accept(fd,0,0);  //接收客户端的连接ev.events=EPOLLIN;ev.data.fd=cfd;int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);if(ret<0){perror("epoll_ctl");exit(-1);}}else  //不是监听的说明要接收客户端的消息{char buffer[1024];memset(buffer,0,sizeof(buffer));int len=recv(fd,buffer,sizeof(buffer)-1,0);std::cout<<len<<std::endl;if(len==0)  //客户端已经断开连接{int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);if(ret<0){perror("epoll_ctl_del");exit(-1);}close(fd);}else if(len>0){std::cout<<333<<std::endl;std::cout<<"client:"<<buffer<<std::endl;char* recebuf="ok";send(fd,recebuf,sizeof(recebuf),0);}else{perror("recv");exit(-1);}}}}return 0;
}
//client.cpp
#include <iostream>
#include <unistd.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>int main(int argc, char* argv[])
{if (argc != 3) {std::cout << "命令行参数数量不对" << std::endl;std::cout << "./client server_ip port" << std::endl;return -1;}// 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("socket");return -1;}// 设置服务器地址信息struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(atoi(argv[2]));if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {perror("inet_pton");close(sockfd);return -1;}// 连接到服务器if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("connect");close(sockfd);return -1;}// 发送数据给服务器const char* sendbuf = "Hello from client!";ssize_t send_len = send(sockfd, sendbuf, strlen(sendbuf), 0);if (send_len < 0) {perror("send");close(sockfd);return -1;}std::cout << "Sent: " << sendbuf << std::endl;// 接收服务器的数据char buffer[1024];memset(buffer, 0, sizeof(buffer));ssize_t recv_len = recv(sockfd, buffer, sizeof(buffer)-1, 0);if (recv_len > 0) {std::cout << "Received from server: " << buffer << std::endl;} else if (recv_len == 0) {std::cout << "Server closed the connection." << std::endl;} else {perror("recv");}// 关闭套接字close(sockfd);return 0;
}
# makefile
all: client serverserver: server.cppg++ -g -o server server.cppclient: client.cppg++ -g -o client client.cpp

epoll的工作模式

epoll有两种工作模式:LT(Level Triggered,水平触发)和ET(Edge Triggered,边缘触发)。

LT模式

LT又叫水平模式,是epoll的默认工作模式。在这种模式下,内核会不断地通知你文件描述符是否就绪,即使你已经读取了数据。也就是说,即使你读取了数据,文件描述符仍然被认为是就绪的,内核会继续通知你。这种模式适用于需要不断地检查文件描述符是否就绪的场景。

水平模式主要有以下特点:

  • 这是epoll的默认工作模式。
  • 在LT模式下,当一个文件描述符准备好进行读写操作时,epoll会通知应用程序。
  • 如果应用程序没有立即处理该事件,或者在处理过程中没有完全读取或写入所有数据,那么只要文件描述符仍然处于就绪状态,epoll将继续通知应用程序。

其实对于大多数应用来说,LT模式足够使用,并且它的行为与传统的poll和select相似

ET模式

ET又叫边缘模式,是epoll的高级模式。在这种模式下,内核只会通知你文件描述符从非就绪状态变为就绪状态一次,即使你读取了数据,文件描述符仍然被认为是非就绪的,内核不会继续通知你。这种模式适用于需要高效处理大量并发连接的场景。

边缘模式主要有以下特点:

  • ET模式是一种低延迟、高性能的工作模式。
  • 在ET模式下,epoll只会在状态发生变化时通知应用程序一次。例如,如果一个文件描述符从非就绪变为就绪,epoll将通知应用程序;但是,如果应用程序未能在第一次通知后立即处理完所有可用的数据,那么即使该文件描述符仍然是就绪状态,epoll也不会再次发送通知,直到该文件描述符的状态再次发生变化(即从就绪变回非就绪,再由非就绪变成就绪)。

因此,在ET模式下,应用程序必须确保每次收到通知时都尽可能多地读取或写入数据,以避免错过后续的通知。这通常意味着在循环中尽可能多地尝试读写操作,直到遇到EAGAIN或EWOULDBLOCK错误为止,这表明当前没有更多可读写的就绪数据。

PS:

  1. LT模式会不断通知应用程序,即使应用程序已经开始读取数据,这样可以保证应用程序能够及时处理数据,但是 频繁的通知会带来性能的损耗,而ET模式只会在状态发生变化时通知应用程序一次,因此应用程序需要确保每次收到通知时都尽可能多地读取或写入数据,以避免错过后续的通知。所以ET模式要求应用程序在每次收到通知时都尽可能多地读取或写入数据,否则可能会错过后续的通知。因此,ET模式通常需要更复杂的编程逻辑,并且对应用程序的设计和实现有更高的要求。

2.我们在使用ET模式要设置EPOLLET标志如下:

if(fd==lfd)  //如果是监听的socket
{int cfd=accept(fd,0,0);  //接收客户端的连接ev.events=EPOLLIN|EPOLLET;  //设置边沿触发ev.data.fd=cfd;int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);if(ret<0){perror("epoll_ctl");exit(-1);}
}

3.LT模式下支持阻塞和非阻塞,而ET模式下只支持非阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/888692.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【kettle】mysql数据抽取至kafka/消费kafka数据存入mysql

目录 一、mysql数据抽取至kafka1、表输入2、json output3、kafka producer4、启动转换&#xff0c;查看是否可以消费 二、消费kafka数据存入mysql1、Kafka consumer2、Get records from stream3、字段选择4、JSON input5、表输出 一、mysql数据抽取至kafka 1、表输入 点击新建…

docker-compose部署skywalking 8.1.0

一、下载镜像 #注意 skywalking-oap-server和skywalking java agent版本强关联&#xff0c;版本需要保持一致性 docker pull elasticsearch:7.9.0 docker pull apache/skywalking-oap-server:8.1.0-es7 docker pull apache/skywalking-ui:8.1.0二、部署文件docker-compose.yam…

用Python开发一个经典贪吃蛇小游戏

Python 是开发小游戏的绝佳工具,借助第三方库,如 pygame,我们可以快速开发一个经典的贪吃蛇游戏。本篇将介绍如何用 Python 实现一个完整的贪吃蛇小游戏。 一、游戏设计 1.1 游戏规则 玩家通过方向键控制贪吃蛇移动。贪吃蛇吃到食物后会变长,同时得分增加。如果贪吃蛇撞到…

在 MacOS 上为 LM Studio 更换镜像源

在 MacOS 之中使用 LM Studio 部署本地 LLM时&#xff0c;用户可能会遇到无法下载模型的问题。 一般的解决方法是在 huggingface.co 或者国内的镜像站 hf-mirror.com 的项目介绍卡页面下载模型后拖入 LM Studio 的模型文件夹。这样无法利用 LM Studio 本身的搜索功能。 本文将…

vue中.sync修饰符的用法

一、什么是.sync修饰符 在Vue.js中&#xff0c;.sync 修饰符用于创建一个双向绑定的 prop。它使子组件能够更新父组件的 prop 值&#xff0c;实现父子组件之间的双向数据同步。具体来说&#xff0c;.sync 修饰符主要有以下几个功能&#xff1a; 简化双向绑定&#xff1a; 使用…

【附源码】基于环信鸿蒙IM SDK实现一个聊天Demo

项目背景 本项目基于环信IM 鸿蒙SDK 打造的鸿蒙IM Demo&#xff0c;完全适配HarmonyOS NEXT系统&#xff0c;实现了发送消息&#xff0c;添加好友等基础功能。代码开源&#xff0c;功能简洁&#xff0c;如果您有类似开发需求可以参考。 源码地址&#xff1a;https://github.c…

SHELL----正则表达式

一、文本搜索工具——grep grep -参数 条件 文件名 其中参数有以下&#xff1a; -i 忽略大小写 -c 统计匹配的行数 -v 取反&#xff0c;不显示匹配的行 -w 匹配单词 -E 等价于 egrep &#xff0c;即启用扩展正则表达式 -n 显示行号 -rl 将指定目录内的文件打…

Can‘t find variable: token(token is not defined)

文章目录 例子 1&#xff1a;使用 var例子 2&#xff1a;使用 let 或 const例子 3&#xff1a;异步操作你的代码中的情况 Cant find variable: tokentoken is not defined源代码 // index.jsPage({data: {products:[],cardLayout: grid, // 默认卡片布局为网格模式isGrid: tr…

Kafka-创建topic源码

一、命令创建topic kafka-topics --create --topic quickstart-events --bootstrap-server cdh1:9092 --partitions 2 --replication-factor 2 二、kafka-topics脚本 exec $(dirname $0)/kafka-run-class.sh org.apache.kafka.tools.TopicCommand "$" 脚本中指定了…

【AI系统】GhostNet 系列

GhostNet 系列 本文主要会介绍 GhostNet 系列网络&#xff0c;在本文中会给大家带来卷积结构的改进方面的轻量化&#xff0c;以及与注意力(self-attention)模块的进行结合&#xff0c;部署更高效&#xff0c;更适合移动计算的 GhostNetV2。让读者更清楚的区别 V2 与 V1 之间的…

传奇996_51——脱下装备,附加属性设为0

奶奶的lua怎么都修改不了&#xff0c;可以调用txt的 ; LINKPICKUPITEM ; ChangeitemaddvaLue -1 5 0 ; GETITEMADDVALUE 3 5 M10 ; SENDUPGRADEITEM ; SENDMSG 9 你的衣服附加了<$STR(M10)>点防御属性. 或者lua callscriptex(actor,“LINKPICKUPITEM”) callscriptex(…

YOLOv8改进,YOLOv8引入CARAFE轻量级通用上采样算子,助力模型涨点

摘要 CARAFE模块的设计目的是在不增加计算复杂度的情况下,提升特征图的质量,特别是在视频超分辨率任务中,提升图像质量和细节。CARAFE结合了上下文感知机制和聚合特征的能力,通过动态的上下文注意力机制来提升细节恢复的效果。 理论介绍 传统的卷积操作通常依赖于局部区域…

大型制造企业IT蓝图、信息化系统技术架构规划与实施路线方案

关注 获取ppt​​​​​​全文&#xff0c;请关注作者

HTTP 长连接(HTTP Persistent Connection)简介

HTTP长连接怎么看&#xff1f; HTTP 长连接&#xff08;HTTP Persistent Connection&#xff09;简介 HTTP 长连接&#xff08;Persistent Connection&#xff09;是 HTTP/1.1 的一个重要特性&#xff0c;它允许在一个 TCP 连接上发送多个 HTTP 请求和响应&#xff0c;而无需为…

MySQL悲观锁和乐观锁

MySQL悲观锁和乐观锁 在数据库中&#xff0c;锁是用来管理并发控制的一种机制&#xff0c;确保数据的一致性和完整性。MySQL中的悲观锁和乐观锁是两种不同的并发控制策略&#xff0c;它们在处理并发事务时采用不同的方法。 悲观锁&#xff08;Pessimistic Locking&#xff09…

控制模组进入飞行模式

控制模组进入飞行模式 控制模组进入飞行模式 控制模组进入飞行模式 控制模组进入飞行模式 #!/bin/bash ## 5G模组采用USB3.0与上位机连接&#xff0c;usb接口在上位机上虚拟出多个port,其中一个可用于发送AT命令&#xff0c;控制模组 ## 本脚本控制模组进入飞行模式## flyin …

001集—— 创建一个WPF项目 ——WPF应用程序入门 C#

本例为一个WPF应用&#xff08;.NET FrameWork&#xff09;。 首先创建一个项目 双击xaml文件 双击xaml文件进入如下界面&#xff0c;开始编写代码。 效果如下&#xff1a; 付代码&#xff1a; <Window x:Class"WpfDemoFW.MainWindow"xmlns"http://schema…

微信小程序配置less并使用

1.在VScode中下载Less插件 2.在微信小程序中依次点击如下按钮 选择 从已解压的扩展文件夹安装… 3.选中刚在vscode中下载安装的插件文件 如果没有修改过插件的安装目录&#xff0c;一般是在c盘下C:\用户\用户名.vscode\extensions\mrcrowl.easy-less-2.0.2 我的路径是&#xf…

AUTOSAR AP 汽车API知识点总结(Automotive API )R24-11

汽车API知识点总结 一、背景与目标 背景:智能互联汽车正逐步依赖远程诊断、软件更新等功能以确保行驶安全,并且用户已习惯于通过智能设备中的应用程序控制连接设备。虽然AUTOSAR标准支持车辆软件的可更新性,但尚未提供将AUTOSAR应用产生的数据和功能安全可靠地暴露给非AUTO…

Vue网页屏保

Vue网页屏保 在vue项目中&#xff0c;如果项目长时间未操作需要弹出屏幕保护程序&#xff0c;以下为网页屏保效果&#xff0c;看板内容为连接的资源。 屏保组件 <template><div v-if"isActive" class"screensaver" click"disableScreens…