【C++高并发服务器WebServer】-15:poll、epoll详解及实现

在这里插入图片描述

本文目录

  • 一、poll
  • 二、epoll
    • 2.1 相对poll和select的优点
    • 2.2 epoll的api
    • 2.3 epoll的demo实现
    • 2.5 epoll的工作模式

一、poll

poll是对select的一个改进,我们先来看看select的缺点。

在这里插入图片描述
我们来看看poll的实现。

struct pollfd {int fd; /* 委托内核检测的文件描述符 */short events; /* 委托内核检测文件描述符的什么事件 */short revents; /* 文件描述符实际发生的事件 */
};struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明如下。
fds:是struct pollfd结构体数组,这是一个需要检测的文件描述符集合。

当内核检测到有变动之后,有需要修改的,会直接修改revents,不需要修改events了,相对select来说,就不需要每次重置fds集合。

除此之外,相对于select来说,并没有1024的限制。
nfds是第一个参数数组中最后一个有效元素的下标+1。

timeout,注意这个是int类型的,当为0时代表不阻塞,当为-1时表示阻塞,当检测到需要检测的文件描述符发生了变化,解除阻塞。>0表示阻塞的时长。

poll函数返回值为-1时表示失败,>0会返回n,表示检测到集合中有n个描述符发生了变化。

在这里插入图片描述

poll的服务端实现代码如下。

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 监听listen(lfd, 8);// 初始化检测的文件描述符数组struct pollfd fds[1024];for(int i = 0; i < 1024; i++) {fds[i].fd = -1;fds[i].events = POLLIN;}fds[0].fd = lfd;int nfds = 0;while(1) {// 调用poll系统函数,让内核帮检测哪些文件描述符有数据int ret = poll(fds, nfds + 1, -1);if(ret == -1) {perror("poll");exit(-1);} else if(ret == 0) {continue;} else if(ret > 0) {// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变if(fds[0].revents & POLLIN) {// 表示有新的客户端连接进来了struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);// 将新的文件描述符加入到集合中for(int i = 1; i < 1024; i++) {if(fds[i].fd == -1) {fds[i].fd = cfd;fds[i].events = POLLIN;break;}}// 更新最大的文件描述符的索引nfds = nfds > cfd ? nfds : cfd;}for(int i = 1; i <= nfds; i++) {if(fds[i].revents & POLLIN) {// 说明这个文件描述符对应的客户端发来了数据char buf[1024] = {0};int len = read(fds[i].fd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");close(fds[i].fd);fds[i].fd = -1;} else if(len > 0) {printf("read buf = %s\n", buf);write(fds[i].fd, buf, strlen(buf) + 1);}}}}}close(lfd);return 0;
}

对应的客户端代码我们继续沿用之前的即可。

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}int num = 0;while(1) {char sendBuf[1024] = {0};sprintf(sendBuf, "send data %d", num++);write(fd, sendBuf, strlen(sendBuf) + 1);// 接收int len = read(fd, sendBuf, sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", sendBuf);} else {printf("服务器已经断开连接...\n");break;}// sleep(1);usleep(1000);}close(fd);return 0;
}

二、epoll

首先调用epoll_create实现一个epoll的实例,这个epoll实例是在内核区,是结构体类型,可以理解成一块数据。返回值是一个文件描述符,那我们就可以通过这个文件描述符来操作这块内核当中的epoll数据(通过epoll提供的一些api来进行操作)。

eventpoll中有两个最关键的数据,就是rbrrdlist,也就是红黑树双向就绪链表

rbr记录需要检测的文件描述符。(之前需要把一些表从用户态拷贝到内核态,现在是直接在内核态,效率高了很多。另外现在是红黑树,之前是链表,红黑树的遍历效率也高很多。)

rdlist是检测文件描述符当中哪些是有数据发生改变的。

在函数epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev)中最后一个参数evstruct epoll_event类型,需要设置eventsev.data.fd

调用epoll_wait之后内核就会去检测rbr里面的文件描述符是否有对应的数据改变。如果有改变的(就绪的),就会把文件描述符的信息放到rdlist中,然后会把这个list拷贝到用户区,这样用户区直接遍历这几个fd,就可以进行对应的读写操作了。

在这里插入图片描述

2.1 相对poll和select的优点

时间复杂度方面,每次调用 select 或 poll 时,内核需要遍历所有被监控的文件描述符,检查它们的状态。select 和 poll 的时间复杂度是 O(n)。当文件描述符数量很大时(例如成千上万个),这种线性扫描的效率会非常低。

epoll 的时间复杂度是 O(1),epoll 使用红黑树和双向链表来管理文件描述符。当文件描述符的状态发生变化时,内核会将其加入到就绪链表中,用户程序只需要检查就绪链表即可,而不需要遍历所有文件描述符。

文件描述符数量限制方面,默认情况下,select 只能监控最多 1024 个文件描述符(由 FD_SETSIZE`定义)。如果需要监控更多的文件描述符,需要修改内核参数并重新编译程序。poll 使用数组来存储文件描述符,理论上可以监控任意数量的文件描述符。但当文件描述符数量很大时,遍历整个数组的效率会非常低。

epoll 可以轻松支持数万个甚至更多的文件描述符。它使用红黑树来存储文件描述符,查找和插入的效率很高。

用户态和内核态的数据拷贝方面,每次调用 select 或 poll 时,都需要将文件描述符集合从用户态拷贝到内核态:当文件描述符数量很大时,这种拷贝操作会带来较大的开销。

对于epoll,文件描述符只需要通过 epoll_ctl 添加到内核事件表中一次,后续不需要重复拷贝。当文件描述符状态变化时,内核会直接将事件放入就绪链表中,用户程序通过 epoll_wait 获取就绪事件。

事件触发模式方面,select 和 poll 只支持水平触发(Level-Triggered,LT)模式:如果文件描述符的状态满足条件(例如有数据可读),select 和 poll 会一直通知用户程序,直到状态发生变化。

在这里插入图片描述

epoll 支持水平触发(LT)和边缘触发(Edge-Triggered,ET)模式:
水平触发(LT):与 select 和 poll 的行为相同,只要文件描述符的状态满足条件,就会一直通知用户程序。边缘触发(ET):只有当文件描述符的状态发生变化时,才会通知用户程序。这种模式可以减少重复通知的次数,提高效率。但是需要用户程序一次性处理完所有数据,否则可能会丢失数据。

内核实现机制方面,select 和 poll 是基于轮询的机制:每次调用时,内核需要遍历所有文件描述符,检查它们的状态。这种机制在大规模并发场景下效率较低。

epoll 是基于事件回调的机制:内核会为每个文件描述符注册回调函数,当文件描述符的状态发生变化时,内核会调用回调函数将其加入到就绪链表中。这种机制避免了不必要的遍历,效率更高。

2.2 epoll的api

头文件如下。

#include <sys/epoll.h>

/创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。【从linux内核2.6.8开始,size这个参数已经被忽略了,但是必须大于0。】

int epoll_create(int size);
- 参数:size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:-1 : 失败> 0 : 文件描述符,操作epoll实例的

对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息

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_event是检测事件的结构体,定义如下。

struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:- EPOLLIN - EPOLLOUT - EPOLLERR- EPOLLET (设置边沿触发)

在其中,又有一个联合体epoll_data_t,定义如下。通过联合体,用户可以选择存储不同类型的数据,如指针、文件描述符、32 位或 64 位整数。

ptr是一个指向任意类型的指针。用户可以将与事件相关的任意数据存储在这个指针中,例如指向某个结构体的指针。这种方式非常灵活,可以存储用户自定义的数据结构。
fd,这是 epoll 最常用的用途之一,直接存储与事件相关的文件描述符。
u32是一个 32 位的无符号整数。用户可以存储一些简单的整数值作为用户数据。u64同理。

联合体 epoll_data_t 的设计允许用户根据需要选择存储不同类型的数据。联合体的特性是所有成员共享同一块内存,因此在任何时刻,联合体中只有一个成员是有效的。用户可以根据实际需求选择存储哪种类型的数据。

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

检测函数如下。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, inttimeout);- 参数:- epfd : epoll实例对应的文件描述符- events : 传出参数,保存了发送了变化的文件描述符的信息- maxevents : 第二个参数结构体数组的大小- timeout : 阻塞时间- 0 : 不阻塞- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
-		 > 0 : 阻塞的时长(毫秒)- 返回值:- 成功,返回发送变化的文件描述符的个数 > 0- 失败 -1

这里有个问题是,在使用 epoll 时,epoll_ctl 函数确实已经将文件描述符(fd)注册到了 epoll 实例中,但 epoll_event 结构体中的 data.fd 仍然需要存储文件描述符的原因主要有以下几点:

首先epoll_ctl是用于将文件描述符注册到 epoll 实例中,并设置相关的事件类型(如 EPOLLIN、EPOLLOUT 等)。它的作用是告诉 epoll 哪些文件描述符需要被监控,以及监控哪些类型的事件。

epoll_event 中 用于在 epoll_wait 调用时返回检测到的事件。
它的作用是告诉用户哪些文件描述符发生了事件,以及发生了哪些类型的事件。epoll_event 中的 data.fd 是为了方便用户在 epoll_wait 返回后,能够直接获取到发生事件的文件描述符。

epoll_wait 返回时,它会返回一个 epoll_event 数组,每个 epoll_event 表示一个发生事件的文件描述符及其事件类型。通过在 epoll_event 中存储 fd,用户可以直接从 epoll_event 中获取到发生事件的文件描述符,而无需额外查找。

2.3 epoll的demo实现

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 监听listen(lfd, 8);// 调用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);struct epoll_event epevs[1024];while(1) {int ret = epoll_wait(epfd, epevs, 1024, -1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n", ret);for(int i = 0; i < ret; i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据达到,有客户端连接struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);epev.events = EPOLLIN | EPOLLOUT; //监听的事件比较多,所以每一种事件在下方都需要进行对应的处理。epev.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);} else {if(epevs[i].events & EPOLLOUT) {continue;}   // 有数据到达,需要通信char buf[1024] = {0};int len = read(curfd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);} else if(len > 0) {printf("read buf = %s\n", buf);write(curfd, buf, strlen(buf) + 1);}}}}close(lfd);close(epfd);return 0;
}

2.5 epoll的工作模式

epoll 有两种工作模式:LT(水平触发)模式和 ET(边沿触发)模式。在 LT 模式中,当内核检测到文件描述符(fd)的读缓冲区中有数据时,会通知用户。如果用户没有读取数据,数据会一直保留在缓冲区中,epoll 会持续通知用户。即使用户只读取了一部分数据,epoll 也会继续通知,直到缓冲区的数据被完全读走。LT 模式同时支持阻塞(block)和非阻塞(non-block)的 socket,它是一种缺省的工作方式,内核会持续告知用户文件描述符是否就绪,并允许用户对这个就绪的 fd 进行 I/O 操作。如果用户不进行任何操作,内核会继续发送通知。

相比之下,ET 模式是一种高速工作方式,仅支持非阻塞 socket。在这种模式下,内核仅在文件描述符从未就绪变为就绪时通过 epoll 通知用户一次。一旦通知,内核会假定用户知道文件描述符已经就绪,并且不会再为该文件描述符发送更多的就绪通知,除非用户执行了某些操作导致文件描述符不再处于就绪状态。在 ET 模式中,如果用户不对 fd 执行 I/O 操作,从而使得它再次变为未就绪状态,内核不会再次发送通知。这种模式显著减少了 epoll 事件被重复触发的次数,因此比 LT 模式更高效。在 ET 模式下工作时,必须使用非阻塞套接字,以避免由于单个文件句柄的阻塞读/写操作导致处理多个文件描述符的任务饿死。

需要特别注意的是,ET模式中,如果用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不会再通知了。

如果使用了ET模式,那么在监听到有客户端连接之后,对cfd的属性需要设置非阻塞。

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

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

相关文章

深度分析:网站快速收录与网站内容多样性的关系

本文转自&#xff1a;百万收录网 原文链接&#xff1a;https://www.baiwanshoulu.com/87.html 网站快速收录与网站内容多样性之间存在着密切的关系。以下是对这一关系的深度分析&#xff1a; 一、网站内容多样性对快速收录的影响 提升搜索引擎抓取效率&#xff1a; 多样化的…

接入 deepseek 实现AI智能问诊

1. 准备工作 注册 DeepSeek 账号 前往 DeepSeek 官网 注册账号并获取 API Key。 创建 UniApp 项目 使用 HBuilderX 创建一个新的 UniApp 项目&#xff08;选择 Vue3 或 Vue2 模板&#xff09;。 安装依赖 如果需要在 UniApp 中使用 HTTP 请求&#xff0c;推荐使用 uni.requ…

PLSQL: 存储过程,用户自定义函数[oracle]

注意: raise notice是高斯的输出语句; DBMS_OUT_PUT.PUT_LINE是oracle的输出语句 存储过程 Stored Procedure 存储过程可以封装数据访问逻辑&#xff0c;使得应用程序可以通过调用存储过程来执行这些逻辑&#xff0c;而不是直接执行SQL语句。这有助于提高代码的可重用性、可…

从零开始玩转Docker:轻松开启容器化之旅

一、什么是 Docker Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。简单来说&#xff0c;Docker 就像是一个超级 “快递箱”&#xff0c…

deepseek+kimi自动生成ppt

打开deepseek官网&#xff0c;输入详细的需求&#xff0c;让他生成个ppt 接着deepseek开始思考生成了 接着复制生成了的内容 打开kimi粘贴刚才deepseek生成的内容 可以一键生成啦&#xff0c;下载编辑使用吧

关于预训练后训练、LLM和视频大模型相关学习记录

Pre-training与Post-training 预训练主要是“模仿”&#xff0c;而后训练则是“创造” 预训练对数据质量的要求不高&#xff0c;而后训练对数据质量的要求较高 预训练&#xff1a;模型通过预测海量互联网文本数据中的下一个 token&#xff0c;学习语言的通用规律&#xff0c;得…

单张照片可生成写实3D头部模型!Adobe提出FaceLift,从单一的人脸图像中重建出360度的头部模型。

FaceLift是Adobe和加州大学默塞德分校推出的单图像到3D头部模型的转换技术,能从单一的人脸图像中重建出360度的头部模型。FaceLift基于两阶段的流程实现:基于扩散的多视图生成模型从单张人脸图像生成一致的侧面和背面视图;生成的视图被输入到GS-LRM重建器中,产出详细的3D高斯表…

一文了解服务端渲染及前端在服务端渲染上的应用

文章目录 服务端和 vs 客户端概念应用场景需求考量优缺点 服务端渲染应用Nuxt.js 介绍概念特点应用场景 Nuxt 与 Vue 3 的综合应用支持情况开发步骤1. 项目初始化2. 使用组合式 API3. 集成 Vue 3 插件4. 静态站点生成和服务器端渲染 优势 Next.js 介绍概念特点应用场景 Next 与…

机器学习:朴素贝叶斯分类器

贝叶斯决策论是概率框架下实施决策的基本方法,对分类任务来说,在所有相关概率都已知的理想情形下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。 贝叶斯定理是贝叶斯决策论的基础&#xff0c;描述了如何根据新的证据更新先验概率&#xff0c;贝叶斯定理&…

差速驱动机器人MPC算法实现-C++

差速驱动机器人&#xff0c;其运动学模型需要考虑线速度和角速度。MPC&#xff08;模型预测控制&#xff09;需要建立预测模型&#xff0c;并在每个控制周期内求解优化问题。 差速驱动机器人的运动学方程通常包括位置&#xff08;x, y&#xff09;和航向角θ&#xff0c;线速度…

5 计算机网络

5 计算机网络 5.1 OSI/RM七层模型 5.2 TCP/IP协议簇 5.2.1:常见协议基础 一、 TCP是可靠的&#xff0c;效率低的&#xff1b; 1.HTTP协议端口默认80&#xff0c;HTTPSSL之后成为HTTPS协议默认端口443。 2.对于0~1023一般是默认的公共端口不需要注册&#xff0c;1024以后的则需…

mysql8 从C++源码角度看sql生成抽象语法树

在 MySQL 8 的 C 源码中&#xff0c;SQL 语句的解析过程涉及多个步骤&#xff0c;包括词法分析、语法分析和抽象语法树&#xff08;AST&#xff09;的生成。以下是详细的解析过程和相关组件的描述&#xff1a; 1. 词法分析器&#xff08;Lexer&#xff09; MySQL 使用一个称为…

excel合并表格

上一章说到excel拆分表格&#xff0c;可以按一列的不重复数据自动拆分成多个表格。这个功能主要适用于有多个下级机构的部门分发表格使用。表格分发完成&#xff0c;下级单位修改后&#xff0c;上传到我们这里。我们还得把这些表格合并成一个表。如果利用复制粘性&#xff0c;工…

区块链100问之加密算法

区块链100问之加密算法 文章目录 区块链100问之加密算法哈希算法是什么&#xff1f;有什么特征&#xff1f;哈希碰撞是什么?雪崩效应呢&#xff1f;如何解决&#xff1f;哈希算法的作用&#xff1f;对称加密和非对称加密有什么区别&#xff1f;为什么会引入非对称加密&#xf…

模型压缩中的四大核心技术 —— 量化、剪枝、知识蒸馏和二值化

一、量化 (Quantization) 量化的目标在于将原始以 32 位浮点数表示的模型参数和中间激活,转换为低精度(如 FP16、INT8、甚至更低位宽)的数值表示,从而在减少模型存储占用和内存带宽的同时,加速推理运算,特别适用于移动、嵌入式和边缘计算场景。 1.1 概念与目标 基本思想…

【LLM】o1/R1系列LLM数据篇

关于思维链推理的10开源数据集&#xff1a; 目前开源的数据主要有如下&#xff1a; 1、Magpie-Reasoning-V2数据集&#xff0c;其中包含DeepSeek-R1生成的250K思路链推理样本&#xff0c;这些示例涵盖了数学推理、编码和一般问题解决等各种任务。https://huggingface.co/datas…

elasticsearch实战应用从入门到高效使用java集成es快速上手

Elasticsearch 因其出色的性能、可扩展性和易用性,成为了处理大规模数据和构建搜索引擎的首选工具。本文将通过一个实际案例,详细讲解如何在 Spring Boot 项目中集成 Elasticsearch,进行数据索引、搜索、聚合分析等操作。 一、Elasticsearch 简介 Elasticsearch 是一个基于…

Centos Stream 10 根目录下的文件夹结构

/ ├── bin -> usr/bin ├── boot ├── dev ├── etc ├── home ├── lib -> usr/lib ├── lib64 -> usr/lib64 ├── lostfound ├── media ├── mnt ├── opt ├── proc ├── root ├── run ├── sbin -> usr/sbin ├── srv ├─…

旋转位置编码(RoPE)讲解和代码实现

旋转位置编码(Rotary Position Embedding:RoPE)讲解和代码实现 1. 什么是位置编码? 在 Transformer 模型中,位置编码的作用是为模型提供序列中每个 token 的位置信息。因为 Transformer 本身没有像 RNN 那样的顺序结构,所以需要通过位置编码来告诉模型 token 的顺序。 …

绘制中国平安股价的交互式 K 线图

在本文中,探索如何使用 Python 的强大库进行股市数据分析与可视化。我们将以中国平安(股票代码:sh601318)为例,展示如何获取其股票数据,并绘制一张交互式 K 线图。 K 线图是股市分析中不可或缺的工具,它能够直观地显示股票的波动情况,包括开盘价、收盘价、最高价和最低…