一扇门铃,万向感应——用 eventfd 实现零延迟通信

🔍 本篇概要

  • eventfdLinux 提供的一种轻量级事件通知机制。你可以把它想象成一个“计数器盒子”。
  • 它里面维护的是一个64位的计数器。
  • 写入:往盒子里放一些数字(比如 1、5、10),表示有几件事发生了。
  • 读取:从盒子里取出数字,每取一次就减少一个(或者一次性全拿走)。

它非常适合用来做:

  • 多线程/进程之间的通信。
  • 异步通知(如 epoll 配合使用)。
  • 控制并发资源访问(类似信号量)。

在这里插入图片描述

一· 🛠 如何创建 eventfd

如下:

#include <sys/eventfd.h>
int efd = eventfd(initval, flags);

解释:

参数名含义
initval初始化值(初始计数器数值)
flags标志位(可选,影响行为)
  • 这里对于initval是给它设置初始值,如果不write写入,计数器就是这个初始值,否者就是write写入的那个值。

二.🎯 eventfd 的所有标志详解(Flags

下面是 eventfd() 支持的所有标志:

标志名含义是否推荐使用
EFD_SEMAPHORE以信号量方式工作(每次读取减 1)✅ 推荐
EFD_CLOEXEC执行 exec 时自动关闭描述符✅ 推荐
EFD_NONBLOCK设置为非阻塞模式✅ 视需求而定
EFD_SHARED_FCNTL_FLAGS允许在 fork 后共享文件锁(Linux 4.7+)⚠️ 较少用
0(默认)默认行为(不带任何标志)✅ 可用

详解 (通俗解释通俗易懂版本)

1️⃣ EFD_SEMAPHORE —— 类似信号量

效果:

启用这个标志后,每次 read() 会把计数器减去 1,并返回 1。

🧠 比如:

你有一个糖果罐子,里面有 5 颗糖:

  • 不加这个标志 → 第一个人来,把 5 颗都拿走了。
  • 加了这个标志 → 每个人只能拿 1 颗,共 5 个人能拿到。

下面我们代码演示下:

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE| EFD_NONBLOCK); // 启用两个标志uint64_t val;for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}

效果如下:

在这里插入图片描述

  • 这里我们往文件里写了个5;也就可以理解成它的计数器被从一开始的0变成了5;而我们设置了semaphore模式,也就是每次计数器都会自动减一(一般如果设置了这个(在允许的条件下),此时每次read读出来的都是1)。
  • 然后我们又设置了非阻塞模式(nonblock),也就是不会阻塞,因此看到了上面的效果。

2️⃣ EFD_CLOEXEC —— 自动关闭(exec 时)

效果:

当你调用 exec()(运行新程序)时,这个 eventfd 描述符会自动关闭,防止被新程序继承。

🧠 比如:

你在执行一个新的程序,不想让这个程序看到你之前的“糖果罐子”,那就加上这个标志。

efd = eventfd(0, EFD_CLOEXEC); // exec 时自动关闭
  • 这里我们一般使用的时候默认加上就好。

3️⃣ EFD_NONBLOCK —— 非阻塞读写

效果:

设置为非阻塞模式后:

  • 如果当前没有数据可读,read() 不会等待,而是立即返回错误码 EAGAIN如果缓冲区满了,write() 也不会等待,而是立即返回 EAGAIN

🧠 比如:

你去看糖果罐子,如果里面没糖了,你不等,直接离开。

演示下:

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(0, EFD_NONBLOCK);uint64_t val;
ssize_t s = read(efd, &val, sizeof(val));
if (s == -1 && errno == EAGAIN) {printf("现在没有事件发生\n");
}return 0;
}

效果:

发现如果设置了:

在这里插入图片描述

  • 直接返回-1,然后查看错误码即可判断是非阻塞模式。

如果没设置:

在这里插入图片描述

  • 发现一直阻塞住。

4️⃣ EFD_SHARED_FCNTL_FLAGS(Linux 4.7+)

效果:

允许多个 fork 出来的子进程共享这个 eventfd 的文件锁状态(很少用)。

🧠 举例理解:

多个小孩一起管理同一个糖果罐子,不会互相干扰。

⚠️ 这个标志只在较新的 Linux 内核中支持,一般用户不需要关心。

因此,这里就不演示,也不常用。

三.综合测试体验下

代码如下:

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE|EFD_CLOEXEC| EFD_NONBLOCK); // 启用两个标志uint64_t val;for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}

效果:

在这里插入图片描述

  • 这里我们设置了非阻塞,因此最后会看到 NO more events,其次就是计数器写成5,每次读取都减1,然后每次读出的都是1,当最后一次减完0了,然后是非阻塞因此会这样。

如果我们写入的值和eventfd本身初始化的值不同呢(他就会按照写入的来初始化计数器了):

代码如下:

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main() {int efd = eventfd(5, EFD_SEMAPHORE|EFD_CLOEXEC| EFD_NONBLOCK); // 启用两个标志uint64_t val=10;ssize_t s = write(efd, &val, sizeof(val));for (int i = 0; i < 6; ++i) {ssize_t s = read(efd, &val, sizeof(val));if (s == -1) {if (errno == EAGAIN|EWOULDBLOCK)printf("No more events.\n");elseperror("read");} else {printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
  • 因此当它读完5个1后还会继续读,直到完成10个:

效果:

在这里插入图片描述

如果不设置EFD_SEMAPHORE呢?

代码如下:

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>int main()
{int efd = eventfd(5, EFD_CLOEXEC | EFD_NONBLOCK); // 启用两个标志uint64_t val;for (int i = 0; i < 6; ++i){ssize_t s = read(efd, &val, sizeof(val));if (s == -1){if (errno == EAGAIN | EWOULDBLOCK)printf("No more events.\n");elseperror("read");}else{printf("Read: %llu\n", (unsigned long long)val);}}close(efd);return 0;
}
  • 此时它就会一次性都读出来,然后计数器瞬间清零。

效果:

在这里插入图片描述

四.相关问题及使用技巧

相关问题:

问题回答
eventfd 是不是只能用于线程间通信?不是,也可以用于父子进程之间通信
eventfd 能不能和 epoll 一起用?当然可以!这是最常见用法之一
eventfd 和 pipe 有什么区别?eventfd 更轻量,适合简单通知;pipe 适合传输大量数据
eventfd 的最大值是多少?最大值是 0xFFFFFFFFFFFFFFFE(接近 18e18)
eventfd 会不会导致内存泄漏?不会,只要记得 close(efd) 就行

使用技巧:(个人看法)

一般我们可以默认把EFD_CLOEXEC加上;然后对于需求来决定是否加上EFD_NONBLOCK;对于EFD_SEMAPHORE也就是想让一次读完还是多次也是根据自己需求来完成的(常用的也就是这三个)。

五.简单基于eventfdepoll多线程通知测试

大致测流程:

  • 初始化 eventfd。
  • 初始化 epoll。
  • 将 eventfd 注册到 epoll。
  • 启动多个线程调用 epoll_wait 等待事件。
  • 主线程写入 eventfd 触发事件。
  • 所有监听线程收到事件并处理。

看图:

在这里插入图片描述

源码:

#include <iostream>
#include <thread>
#include <vector>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <cstring>
#include <cstdint>
#include <functional>
#include <chrono>// 线程数量
const int THREAD_COUNT = 3;int main() {// 1. 创建 eventfd (初始值为0)int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);if (efd == -1) {perror("eventfd");return 1;}// 2. 创建 epoll 实例int epfd = epoll_create1(0);if (epfd == -1) {perror("epoll_create1");close(efd);return 1;}// 3. 将 eventfd 添加进 epollstruct epoll_event ev;ev.events = EPOLLIN;   // 只关心可读事件ev.data.fd = efd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev) == -1) {perror("epoll_ctl: add");close(epfd);close(efd);return 1;}// 4. 创建线程池,每个线程调用 epoll_wait 等待事件std::vector<std::thread> threads;for (int i = 0; i < THREAD_COUNT; ++i) {threads.emplace_back([=]() {std::cout << "Thread [" << std::this_thread::get_id() << "] is waiting for event..." << std::endl;struct epoll_event events[10];while (true) {int n = epoll_wait(epfd, events, 10, -1); // 永远等待if (n == -1) {perror("epoll_wait error");break;}for (int j = 0; j < n; ++j) {if (events[j].data.fd == efd && (events[j].events & EPOLLIN)) {uint64_t u;ssize_t s = read(efd, &u, sizeof(uint64_t));if (s != sizeof(uint64_t)) {perror("read eventfd");continue;}std::cout << "Thread [" << std::this_thread::get_id()<< "] received event, count: " << u << std::endl;}}}});}// 5. 主线程休眠一段时间后发送事件std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "Main thread is sending event to all workers..." << std::endl;uint64_t u = 1;if (write(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("write eventfd");}// 6. 等待所有线程结束(这里为了简单,实际应优雅退出)std::this_thread::sleep_for(std::chrono::seconds(2)); // 给子线程足够时间响应for (auto& t : threads) {if (t.joinable()) {t.detach(); // 或者 join()}}// 7. 清理资源close(epfd);close(efd);return 0;
}

先看现象:

在这里插入图片描述

解释下:

  • 首先搞三线程,然后往epoll模型的监测fd中加入efd,三个线程都进行监测;如果主线程往efd中写入了1,那么就只会被一个线程读取然后打印出来,其他线程都在epoll这里阻塞;最后全部线程都被终止即结束。

六. 小白总结

下面是博主总结的一张使用图:

在这里插入图片描述

通俗总结:

eventfd 就像一个“计数器盒子”,你可以往里放数字,也可以往外取。通过设置不同的标志(flag),你可以控制它是“一次全拿走”还是“每次拿一个”,还可以让它“不阻塞”、“自动关闭”等等。掌握这些标志,就能灵活运用它来做多线程同步、异步通知等高级功能!

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

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

相关文章

基于Node.js的线上教学系统的设计与实现(源码+论文+调试+安装+售后)

感兴趣的可以先收藏起来&#xff0c;还有大家在毕设选题&#xff0c;项目以及论文编写等相关问题都可以给我留言咨询&#xff0c;我会一一回复&#xff0c;希望帮助更多的人。系统背景近年来&#xff0c;全球数字化浪潮的推进与教育公平化需求的增长&#xff0c;促使线上教学迎…

互斥锁详解(操作系统os)

1. 互斥锁 (Mutex) - 档案室的“智能锁”首先&#xff0c;我们给之前讨论的那些“锁”一个正式的名字&#xff1a;互斥锁 (Mutex)。概念&#xff1a;你可以把它简单理解成档案室门上的一把“智能锁”。它只有两种状态&#xff1a;locked (已上锁) 或 unlocked (未上锁)。操作&a…

自动润滑系统:从 “盲目养护“ 到智能精注的工业运维革命

​在工业运维的漫长历史中&#xff0c;传统润滑模式如同"定时喂饭"——无论设备实际需求&#xff0c;仅凭经验或固定周期执行润滑作业。这种模式埋下两大隐患&#xff1a;过度润滑&#xff1a;某汽车生产线曾因季度性强制润滑&#xff0c;每年浪费1.2吨润滑脂&#x…

【Java八股文总结 — 包学会】(二)计算机网络

1.一条url输入到浏览器最后显示页面的过程 URL解析与处理 浏览器解析URL&#xff08;如https://www.example.com/page&#xff09; 分离协议&#xff08;https&#xff09;、域名&#xff08;www.example.com&#xff09;和资源路径&#xff08;/page&#xff09; 检查HSTS预加…

力扣61.旋转链表

给你一个链表的头节点 head &#xff0c;旋转链表&#xff0c;将链表每个节点向右移动 k 个位置。示例 1&#xff1a;输入&#xff1a;head [1,2,3,4,5], k 2 输出&#xff1a;[4,5,1,2,3]示例 2&#xff1a;输入&#xff1a;head [0,1,2], k 4 输出&#xff1a;[2,0,1]提示…

深度剖析:std::vector 内存机制与 push_back 扩容策略

深度剖析&#xff1a;std::vector 内存机制与 push_back 扩容策略 1. std::vector 核心内部结构 #mermaid-svg-8HOj3MqsD6UVgEeA {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-8HOj3MqsD6UVgEeA .error-icon{fill:…

GROW领导力模型

GROW领导力模型是由英国教练格雷厄姆亚历山大&#xff08;Graham Alexander&#xff09;、艾伦Fine和约翰惠特默&#xff08;John Whitmore&#xff09;在20世纪80年代提出的&#xff0c;最初用于体育教练领域&#xff0c;后来被广泛应用于企业管理、领导力发展和个人成长中。它…

打破并发瓶颈:虚拟线程实现详解与传统线程模型的性能对比

目录 一、定义与特性 二、虚拟线程实现 2.1 使用 Thread.startVirtualThread() 创建 2.2 使用 Thread.ofVirtual() 创建 2.3 使用 ThreadFactory 创建 2.4 使用 Executors.newVirtualThreadPerTaskExecutor()创建 三、虚拟线程和普通线程的区别 3.1 线程管理方式不同 3…

“28项评测23项SOTA——GLM-4.1V-9B-Thinking本地部署教程:10B级视觉语言模型的性能天花板!

一、模型介绍 GLM-4.1V-9B-Thinking是由智谱AI联合清华大学团队推出的多模态大模型&#xff0c;以GLM-4-9B-0414基座模型为底&#xff0c;通过引入“思维链推理机制”和“课程采样强化学习策略”&#xff08;Reinforcement Learning with Curriculum Sampling&#xff09;&…

推荐系统-Random算法

Random算法总结引言 在推荐系统研究与应用中&#xff0c;我们常常需要一些简单的基线算法来衡量更复杂算法的性能提升。Random&#xff08;随机推荐&#xff09;算法是最基础的基线方法之一&#xff0c;它通过随机生成评分来模拟用户对物品的偏好。虽然这种方法看似简单&#x…

Django--02模型和管理站点

Django–02模型与站点管理 Part 2: Models and the admin site 本教程承接Django–01的内容。我们将设置数据库、创建你的第一个模型&#xff0c;并快速了解 Django 自动生成的管理站点。 文章目录Django--02模型与站点管理前言一、设置数据库1.1 参考文档链接1.2 默认设置1.3…

CS课程项目设计1:交互友好的井字棋游戏

最近突然想开设一个专栏了&#xff0c;专门为计算机专业的同行分享一些入门级的课程项目设计&#xff0c;旨在让同学更好地了解CS项目的设计流程&#xff0c;同时给出代码来介绍coding过程。 今天要分享的是第一个CS课程项目&#xff1a;交互友好的井字棋游戏。 1. 研究目的 井…

首个自动驾驶VLA综述介绍

当视觉(Vision)、语言(Language)和行动(Action)三大能力在一个模型中融合,自动驾驶的未来将走向何方? 近日,来自麦吉尔大学、清华大学、小米公司和威斯康辛麦迪逊的研究团队联合发布了全球首篇针对自动驾驶领域的视觉-语言-行动(Vision-Language-Action, VLA)模型的…

C# 接口(接口可以继承接口)

接口可以继承接口 之前我们已经知道接口实现可以从基类被继承&#xff0c;而接口本身也可以从一个或多个接口继承而来。要指定某个接口继承其他的接口&#xff0c;应在接口声明中把基接口名称以逗号分隔的列表形式 放在接口名称后面的冒号之后&#xff0c;如下所示。类在基类列…

linux----------------------线程同步与互斥(上)

1.线程互斥 1-1 进程线程间的互斥相关背景概念 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源 临界区&#xff1a;每个线程内部访问临界资源的代码就叫做临界区 互斥&#xff1a;任何时刻&#xff0c;互斥保证只有一个执行进入临界区&#xff0c;对临界资源起…

百度AI的开放新篇章:文心4.5本地化部署指南与未来生态战略展望

百度AI的开放新篇章&#xff1a;文心4.5本地化部署指南与未来生态战略展望 一起来玩转文心大模型吧&#x1f449;文心大模型免费下载地址&#xff1a;https://ai.gitcode.com/theme/1939325484087291906 &#x1f31f; 嗨&#xff0c;我是IRpickstars&#xff01; &#x1f30…

笔记/sklearn中的数据划分方法

文章目录一、前言二、数据划分方法1. 留出法&#xff08;Hold-out&#xff09;2. K折交叉验证&#xff08;K-Fold&#xff09;3. 留一法&#xff08;Leave-One-Out&#xff09;三、总结一、前言 简要介绍数据划分在机器学习中的作用。 二、数据划分方法 1. 留出法&#xff0…

Android14 开屏页SplashScreen设置icon圆角的原理

简介 我们在看到一个应用在启动的时候会看到一个启动的icon,这个图标是应用的icon当然也是可以应用自己去控制的如 <item name="android:windowSplashScreenAnimatedIcon">@drawable/adas_icon</item> 图上的效果明显不理想,图标是自带圆角,而且还是…

flutter redux状态管理

&#x1f4da; Flutter 状态管理系列文章目录 Flutter 状态管理(setState、InheritedWidget、 Provider 、Riverpod、 BLoC / Cubit、 GetX 、MobX 、Redux) setState() 使用详解&#xff1a;原理及注意事项 InheritedWidget 组件使用及原理 Flutter 中 Provider 的使用、注…

AMIS全栈低代码开发

amis是百度开源的前端低代码框架&#xff0c;它通过JSON配置来生成各种后台页面&#xff0c;旨在简化前端开发过程&#xff0c;提高开发效率&#xff0c;降低开发门槛。以下是详细介绍&#xff1a; 核心特点&#xff1a; 可视化开发&#xff1a;允许开发者通过可视化方式构建页…