当前位置: 首页 > news >正文

【高级IO-4】I/O多路转接 之 epoll(概念及代码实例)

文章目录

  • 前言
  • epoll
    • 1. 相比于 select 和 poll,epoll 的优点
    • 2. epoll的三个系统调用
      • ① epoll_create():
      • ② epoll_ctl():
      • ③ epoll_wait():
      • ④ 对于 struct epoll_event类型
    • 3. epoll 的工作原理
    • 4. epoll 使用过程
    • 5. epoll 的两种工作模式
      • ① LT 模式(Level-Triggered - 水平触发)
      • ② ET 模式(Edge-Triggered - 边缘触发)
      • ③ LT 与 ET 的对比
    • 6. ET模式 为什么要用 非阻塞文件描述符
    • 7. epoll 惊群问题
      • ① 发生原因
      • ② 示例场景
      • ③ 解决方案
    • 8. epoll 代码实例
      • ① LT 模式的 epoll 服务器
      • ② ET 模式的 epoll 服务器
      • ③ 实现时的关键区别


前言

在学习epoll前,需要先对select与poll进行了解:

IO多路转接之Select
I/O多路转接 之 poll


epoll

epoll 是 Linux 下用于处理大量文件描述符的高效 I/O 多路复用机制。在 2.6 版本的 Linux 内核中引入,旨在解决 selectpoll 的性能瓶颈。


1. 相比于 select 和 poll,epoll 的优点

  1. 效率更高: epoll 使用了红黑树(RB-tree)和事件链表(eventpoll)来管理文件描述符,这使得在大量文件描述符中的事件监听更加高效。

  2. 没有最大文件描述符数量限制: selectpoll 有一个固定的文件描述符集大小限制,而 epoll 则没有这个限制,因此能够处理更多的文件描述符。

  3. 更好的扩展性: epoll 使用事件通知的方式来管理文件描述符,可以有效地处理成千上万的并发连接,而且随着文件描述符数量的增加,性能不会线性下降。


2. epoll的三个系统调用

在Linux系统下,使用epoll进行I/O多路复用时,通常会用到以下三个系统调用:

① epoll_create():

int epoll_create(int size);
  • 功能:创建一个epoll实例,返回一个用于后续操作的文件描述符。
  • 参数
    • size:epoll实例的大小,该参数在新版本的内核中已经被忽略,但是仍然需要提供一个大于0的值。
  • 返回值:成功时返回一个新的文件描述符,失败时返回-1,并设置errno来指示错误类型。

② epoll_ctl():

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:控制epoll实例中的事件,包括添加、修改和删除文件描述符等操作。
  • 参数
    • epfd:epoll实例的文件描述符,即通过epoll_create()创建的返回值。
    • op:操作类型,可以是EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符)或者EPOLL_CTL_DEL(删除文件描述符)。
    • fd:需要操作的文件描述符。
    • event:指向一个struct epoll_event结构体的指针,用于描述需要监听的事件类型和附加数据。
  • 返回值:成功时返回0,失败时返回-1,并设置errno来指示错误类型。

③ epoll_wait():

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 功能:等待文件描述符上的事件发生,并返回就绪的文件描述符列表。
  • 参数
    • epfd:epoll实例的文件描述符,即通过epoll_create()创建的返回值。
    • events:指向一个struct epoll_event数组的指针,用于存储就绪的事件。
    • maxeventsevents数组的最大大小,即最多能够存放多少个事件。
    • timeout:超时时间(以毫秒为单位),指定在没有就绪事件时等待的最长时间,如果设为-1,则表示永远等待直到有事件发生。
  • 返回值:返回就绪事件的个数,如果超时则返回0,失败时返回-1,并设置errno来指示错误类型。

这三个系统调用是使用epoll实现高效I/O多路复用的关键。通过epoll_create()创建epoll实例,使用epoll_ctl()注册监听的文件描述符和事件,最后通过epoll_wait()等待并处理就绪的事件。


④ 对于 struct epoll_event类型

在使用 epoll 实例进行事件监听时,struct epoll_event 用于描述事件。定义如下:

struct epoll_event {uint32_t events;   // 表示感兴趣的事件类型epoll_data_t data; // 用户数据
};

其中,events 表示感兴趣的事件类型,而 data 用于存储与该事件相关的用户数据。具体来说:

  • events 字段是一个32位的无符号整数,表示感兴趣的事件类型,它的取值可以是以下几个常量的按位或运算的结果:

    • EPOLLIN:表示对应的文件描述符可以读取(包括普通数据、带外数据和优先级数据)。
    • EPOLLOUT:表示对应的文件描述符可以写入。
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(如带外数据)。
    • EPOLLERR:表示对应的文件描述符发生错误。
    • EPOLLHUP:表示对应的文件描述符被挂起。
    • EPOLLET:表示设置边缘触发模式(Edge Triggered),默认为水平触发模式(Level Triggered)。
    • EPOLLONESHOT:表示设置一次性触发模式(One Shot),即当一个事件被触发后,相应的文件描述符将被从epoll实例中移除,直到再次通过epoll_ctl()重新添加。
  • data 字段是一个联合体,可以存储不同类型的用户数据。定义如下:

typedef union epoll_data {void    *ptr;int      fd;uint32_t u32;uint64_t u64;
} epoll_data_t;
  • ptr:指向用户数据的指针。
  • fd:一个文件描述符。
  • u32:一个32位的无符号整数。
  • u64:一个64位的无符号整数。

3. epoll 的工作原理

epoll底层工作主要由 红黑树 与 双向链表 构成:

在这里插入图片描述
如图所示,由红黑树与就绪队列和该回调函数形成的整体就是epoll模型的大致内容;

  • 当我们使用epoll_create()时,本质就是构建了一个epoll模型,
  • 使用epoll_ctl()时,就是用户将fd等相关字段告知内核,去对红黑树和队列进行操作
  • 使用epoll_wait()时,就是内核给用户将要关注的fd从队列中取出来

将这方面的内容总结而言如下:

在这里插入图片描述


4. epoll 使用过程

根据上面的内容,epoll的使用过程总结为三步:

  1. 调用epoll_create创建一个epoll句柄;
  2. 调用epoll_ctl, 将要监控的文件描述符进行注册;
  3. 调用epoll_wait, 等待文件描述符就绪

5. epoll 的两种工作模式

epoll 在使用上可以有两种工作模式:

  • LT(Level-Triggered)模式ET(Edge-Triggered)模式。

如何理解这两种工作模式?用一个简单的例子解释:

你的朋友李田所想向你借钱,有以下两种可能:

  1. 李田所跟你发消息借钱,你没有回复,他会发第二次、第三次…一直发下去(LT)
  2. 李田所跟你发消息借钱,你没有回复,他就不再过问(ET)

① LT 模式(Level-Triggered - 水平触发)

  • 在 LT 模式下,当文件描述符就绪时,epoll_wait() 将会返回,然后应用程序可以对这些就绪的文件描述符进行操作,直到没有文件描述符就绪为止。
  • 即在 LT 模式下,epoll_wait()重复通知应用程序文件描述符的状态,直到应用程序处理完所有就绪的文件描述符。

LT 模式的特点:

  • 每次调用 epoll_wait() 都会返回就绪的文件描述符,无论它们是否被处理过。
  • 应用程序需要确保及时处理所有就绪的文件描述符,否则可能导致 CPU 资源浪费。

② ET 模式(Edge-Triggered - 边缘触发)

在 ET 模式下,epoll_wait() 只会在文件描述符状态发生变化时返回,一旦返回后,应用程序需要尽快处理所有就绪的文件描述符,否则不会再次收到通知,直到下一次文件描述符状态发生变化。

ET 模式的特点:

  • 只有在文件描述符状态发生变化时才会通知应用程序,因此需要应用程序确保及时处理就绪的文件描述符,否则可能导致事件丢失。
  • ET 模式相比于 LT 模式可以减少不必要的重复通知,提高效率。

③ LT 与 ET 的对比

LT 模式(Level-Triggered)

  • 默认行为LT 是 epoll 的默认行为,当文件描述符就绪时,每次调用 epoll_wait() 都会返回这些就绪的文件描述符,无论它们是否被处理过。
  • 处理方式:在 LT 模式下,应用程序需要确保及时处理所有就绪的文件描述符,否则可能导致 CPU 资源浪费。
  • 优点
    • 实现简单,易于理解。
    • 应用程序只需处理每个就绪事件一次,不会错过事件。
  • 缺点
    • 可能会有大量的重复通知,导致性能损耗。

ET 模式(Edge-Triggered)

  • 减少触发次数:ET 模式可以减少 epoll 触发的次数,只有在文件描述符状态发生变化时才会通知应用程序。
  • 强制一次性处理:在 ET 模式下,应用程序需要一次性处理所有就绪的文件描述符,否则不会再次收到通知,直到下一次文件描述符状态发生变化。
  • 优点
    • 减少了不必要的重复通知,提高了效率。
  • 缺点
    • 复杂度更高,因为应用程序需要确保及时处理就绪的文件描述符,以免发生事件丢失。

总结对比

  • LT 模式相对简单,但可能存在性能损耗,特别是在大量文件描述符处于就绪状态时。
  • ET 模式可以提高效率,减少重复通知,但需要应用程序确保及时处理就绪的文件描述符,且代码复杂度更高。

6. ET模式 为什么要用 非阻塞文件描述符

使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞,这并非接口要求,而是考虑实际情况所注意到的。

  1. 假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求;

在这里插入图片描述

  1. 如果服务端代码用的是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,可能被信号中断),剩下的9k数据就会待在缓冲区中;
    在这里插入图片描述

  2. 此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪。epoll_wait 就不会再次返回。剩下的 9k 数据会一直在缓冲区中。直到下一次客户端再给服务器写数据。epoll_wait 才能返回。

问题就显而易见了:

  • 服务器只读到1k个数据, 要10k读完才会给客户端返回响应数据.
  • 客户端要读到服务器的响应, 才会发送下一个请求
  • 客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.

在这里插入图片描述

所以, 为了解决上述问题(阻塞式read不一定能把完整的请求一次读完),就需要使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。

对于LT模式,不存在这个问题;只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪


7. epoll 惊群问题

epoll“惊群效应”(“惊群问题” / “惊群现象”) 是在使用 epoll 处理大量文件描述符时可能遇到的性能问题。

这种效应发生在大量的线程或进程被唤醒来处理 I/O 事件(多进程 / 多线程),而实际上只有少数几个描述符有活动。这个问题主要影响的是高并发的服务器场景,如网络服务器。


① 发生原因

惊群效应的根本原因当某个事件(例如网络连接到达)触发时,所有注册的线程或进程都被唤醒以处理这个事件。可能导致:

  1. 资源浪费:虽然只有少数的描述符有活动,但所有线程或进程都被唤醒,消耗了大量 CPU 和内存资源。
  2. 上下文切换:大量线程或进程被唤醒会导致频繁的上下文切换,增加了系统的开销和延迟。

② 示例场景

假设一个高并发服务器使用 epoll来处理大量的客户端连接。当一个客户端发来数据时,所有的工作线程都被唤醒来检查这些连接,虽然实际上只有一个连接有数据需要处理。这种情况可能导致系统的性能下降,因为很多线程会无用地参与处理过程。


③ 解决方案

为了解决惊群效应问题,常见的解决方案包括:

  1. 线程/进程池:使用线程池或进程池来限制并发处理的线程或进程数量。这样即使有大量的事件触发,也不会导致所有线程都被唤醒。
  2. 事件处理优化:优化事件处理逻辑,确保只有必要的线程或进程被唤醒。比如,在处理 I/O 事件时,避免过多的线程竞赛。
  3. 避免多线程唤醒:使用 epoll 的边缘触发模式(ET, Edge Triggered)而不是水平触发模式(LT, Level Triggered),这样只有在状态改变时才会唤醒线程,减少无效的唤醒。

8. epoll 代码实例

下面我们分别实现LT工作模式 以及 ET工作模式下的epoll服务器:

① LT 模式的 epoll 服务器

LT 模式是 epoll 的默认工作模式。它会在事件被触发期间持续报告,直到事件被处理。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>#define PORT 8080
#define MAX_EVENTS 10int main() {int server_fd, new_socket, epoll_fd, nfds;struct epoll_event ev, events[MAX_EVENTS];struct sockaddr_in address;int addrlen = sizeof(address);// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项int opt = 1;if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听套接字if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}// 创建 epoll 实例if ((epoll_fd = epoll_create1(0)) == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}// 添加监听套接字到 epollev.events = EPOLLIN;ev.data.fd = server_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {perror("epoll_ctl: listen_sock");exit(EXIT_FAILURE);}// 主循环while (1) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; i++) {if (events[i].data.fd == server_fd) {// 处理新的连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}ev.events = EPOLLIN;ev.data.fd = new_socket;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {perror("epoll_ctl: add");exit(EXIT_FAILURE);}} else {// 处理数据char buffer[1024] = {0};int bytes_read = read(events[i].data.fd, buffer, sizeof(buffer));if (bytes_read == 0) {// 连接关闭close(events[i].data.fd);} else {printf("Received: %s\n", buffer);send(events[i].data.fd, buffer, bytes_read, 0);}}}}return 0;
}

② ET 模式的 epoll 服务器

ET 模式会在事件状态改变时才会报告,不会重复报告,直到事件被处理。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>#define PORT 8080
#define MAX_EVENTS 10int main() {int server_fd, new_socket, epoll_fd, nfds;struct epoll_event ev, events[MAX_EVENTS];struct sockaddr_in address;int addrlen = sizeof(address);// 创建服务器套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项int opt = 1;if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听套接字if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}// 创建 epoll 实例if ((epoll_fd = epoll_create1(0)) == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}// 添加监听套接字到 epollev.events = EPOLLIN | EPOLLET;  // 设置边缘触发模式ev.data.fd = server_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {perror("epoll_ctl: listen_sock");exit(EXIT_FAILURE);}// 主循环while (1) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; i++) {if (events[i].data.fd == server_fd) {// 处理新的连接while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) >= 0) {ev.events = EPOLLIN | EPOLLET;  // 设置边缘触发模式ev.data.fd = new_socket;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {perror("epoll_ctl: add");exit(EXIT_FAILURE);}}if (new_socket == -1 && (errno != EAGAIN && errno != EWOULDBLOCK)) {perror("accept");exit(EXIT_FAILURE);}} else {// 处理数据char buffer[1024] = {0};ssize_t bytes_read;while ((bytes_read = read(events[i].data.fd, buffer, sizeof(buffer))) > 0) {printf("Received: %s\n", buffer);send(events[i].data.fd, buffer, bytes_read, 0);}if (bytes_read == 0 || (bytes_read == -1 && errno != EAGAIN)) {close(events[i].data.fd);}}}}return 0;
}

③ 实现时的关键区别

  • LT 模式:每次 epoll_wait 调用都会报告事件,直到事件处理完成。
  • ET 模式:只会在事件状态发生变化时报告事件,处理过程中需要确保完全读取数据,否则可能会丢失事件。

希望这些示例代码能帮助你理解 LT 和 ET 模式的区别及实现方式。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • P1801 黑匣子
  • 【Docker】以思源笔记为例,谈谈什么是端到端加密
  • 计算机网络参考模型
  • WPF-实现多语言的静态(需重启)与动态切换(不用重启)
  • 设备实时数据采集:开启制造业智能化、自动化的新篇章
  • 编译原理(极速版)
  • 单元格里显示曲线
  • 2024.8.22(Docker)
  • swagger使用
  • 京东2025届秋招 算法开发工程师 第2批笔试
  • 【Unity脚本】使用脚本修改游戏对象静态属性
  • DHCP DNS 欺骗武器化——实用指南
  • 蓝队技能-应急响应篇钓鱼攻击邮件与文件EML还原蠕虫分析线索定性
  • k8s教程
  • 【C语言】进程和线程详解
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • golang 发送GET和POST示例
  • js操作时间(持续更新)
  • python 学习笔记 - Queue Pipes,进程间通讯
  • Python学习之路16-使用API
  • Vue.js 移动端适配之 vw 解决方案
  • Vue.js源码(2):初探List Rendering
  • 基于组件的设计工作流与界面抽象
  • 解析 Webpack中import、require、按需加载的执行过程
  • 理清楚Vue的结构
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 如何利用MongoDB打造TOP榜小程序
  • 在weex里面使用chart图表
  • 找一份好的前端工作,起点很重要
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • "无招胜有招"nbsp;史上最全的互…
  • # 职场生活之道:善于团结
  • #if等命令的学习
  • (1)(1.8) MSP(MultiWii 串行协议)(4.1 版)
  • (1)(1.9) MSP (version 4.2)
  • (2024)docker-compose实战 (8)部署LAMP项目(最终版)
  • (31)对象的克隆
  • (C语言)fgets与fputs函数详解
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (pojstep1.1.2)2654(直叙式模拟)
  • (草履虫都可以看懂的)PyQt子窗口向主窗口传递参数,主窗口接收子窗口信号、参数。
  • (十三)Flink SQL
  • (新)网络工程师考点串讲与真题详解
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (原創) 物件導向與老子思想 (OO)
  • (转)JAVA中的堆栈
  • (转载)Linux网络编程入门
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET 应用架构指导 V2 学习笔记(一) 软件架构的关键原则
  • .NET/C#⾯试题汇总系列:集合、异常、泛型、LINQ、委托、EF!(完整版)
  • .NetCore部署微服务(二)
  • .net下简单快捷的数值高低位切换
  • /ThinkPHP/Library/Think/Storage/Driver/File.class.php  LINE: 48
  • @Async注解的坑,小心