【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,一经查实,立即删除!

相关文章

接入 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;下载编辑使用吧

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

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

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

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

excel合并表格

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

区块链100问之加密算法

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

HTML应用指南:利用GET请求获取全国盒马门店位置信息

随着新零售业态的发展,门店位置信息的获取变得至关重要。作为新零售领域的先锋,盒马鲜生不仅在商业模式创新上持续领先,还积极构建广泛的门店网络,以支持其不断增长的用户群体。本篇文章,我们将继续探究GET请求的实际应用,我们使用Python的requests库通过GET请求,从盒马…

(原创,可用)SSH实现内外网安全穿透(安全不怕防火墙)

目前有A、B终端和一台服务器&#xff0c;A、B机器不能直接访问&#xff0c;服务器不能直接访问A、B终端但是A、B终端可以访问服务器&#xff0c;这个场景很像我们有一台电脑在单位内网&#xff0c;外机器想访问内网系统&#xff0c;可能大家目前想到的就是frp之类穿透工具&…

keil5显示[NO J-link found]解决办法——【J-LINK】驱动下载安装教程

打开电脑的设备管理器&#xff0c;检查通用串行总线控制器 看这里发现没有J-link driver&#xff0c;于是开始安装下驱动。 一、下载 打开J-link下载官网链接&#xff1a; SEGGER - The Embedded Experts - Downloads 点击左下角下载按钮 二、安装 等待下载完成以后双击下载…

Maven的dependencyManagements锁定版本依赖

Pom工程的父项目定义的依赖 <dependencyManagement><dependencies><!--springcloud 2023.0.0--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version&g…

Coze(扣子)+ Deepseek:多Agents智能体协作开发新范式

前言 在当今数字化浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;技术的迅猛发展正深刻改变着我们的生活和工作方式。从智能语音助手到自动化流程机器人&#xff0c;AI 的应用无处不在&#xff0c;为我们提供了更加便捷、高效的服务。然而&#xff0c;对于非专业人士来…

【文献讲解】《Non-local Neural Networks》

一、引言 传统的深度学习方法(如卷积神经网络CNN和循环神经网络RNN)在捕捉长距离依赖关系时存在局限性。CNN主要关注局部邻域的特征,而RNN则依赖于序列的递归计算,无法直接捕捉全局信息。为了解决这一问题,本文提出了一种非局部神经网络(Non-local Neural Networks),通…

BiGRU双向门控循环单元多变量多步预测,光伏功率预测(Matlab完整源码和数据)

代码地址&#xff1a;BiGRU双向门控循环单元多变量多步预测&#xff0c;光伏功率预测&#xff08;Matlab完整源码和数据) BiGRU双向门控循环单元多变量多步预测&#xff0c;光伏功率预测 一、引言 1.1、研究背景和意义 随着全球对可再生能源需求的不断增长&#xff0c;光伏…

经典题型:求数组中逆序对的数量

经典题型&#xff1a;求数组中逆序对的数量 题目思路代码 题目 链接 思路 步骤一&#xff1a;两个指针指向两个有序区间&#xff0c;进行合并操作。 步骤二&#xff1a;1与前面的区间&#xff0c;构成逆序对&#xff0c;个数就是前面区间的个数。 步骤三&#xff1a;前面数组…

ASP.NET Core 如何使用 C# 向端点发出 POST 请求

使用 C#&#xff0c;将 JSON POST 到 REST API 端点&#xff1b;如何从 REST API 接收 JSON 数据。 本文需要 ASP .NET Core&#xff0c;并兼容 .NET Core 3.1、.NET 6和.NET 8。 要从端点获取数据&#xff0c;请参阅本文。 使用 . 将 JSON 数据发布到端点非常容易HttpClien…

Flink CDC YAML:面向数据集成的 API 设计

摘要&#xff1a;本文整理自阿里云智能集团 、Flink PMC Member & Committer 徐榜江&#xff08;雪尽&#xff09;老师在 Flink Forward Asia 2024 数据集成&#xff08;一&#xff09;专场中的分享。主要分为以下四个方面&#xff1a; Flink CDC YAML API Transform A…

清理软件——软媒清理大师

随着电脑使用时间的增加&#xff0c;系统垃圾文件也会不断累积&#xff0c;导致C盘空间逐渐变得紧张。今天&#xff0c;我为大家推荐一款专业的清理软件——软媒清理大师&#xff0c;它能够高效地清理电脑中的无用垃圾文件&#xff0c;为您的电脑“减负”。 清理软件——软媒清…