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

🔍 本篇概要

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

它非常适合用来做:

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

一· 🛠 如何创建 eventfd

如下:

cpp 复制代码
#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 个人能拿到。

下面我们代码演示下:

cpp 复制代码
#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");
            else
                perror("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 描述符会自动关闭,防止被新程序继承。

🧠 比如:

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

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

3️⃣ EFD_NONBLOCK ------ 非阻塞读写

效果:

设置为非阻塞模式后:

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

🧠 比如:

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

演示下:

cpp 复制代码
#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 内核中支持,一般用户不需要关心。

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

三.综合测试体验下

代码如下:

cpp 复制代码
#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");
            else
                perror("read");
        } else {
            printf("Read: %llu\n", (unsigned long long)val);
        }
    }

    close(efd);
    return 0;
}

效果:

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

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

代码如下:

cpp 复制代码
#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");
            else
                perror("read");
        } else {
            printf("Read: %llu\n", (unsigned long long)val);
        }
    }

    close(efd);
    return 0;
}
  • 因此当它读完5个1后还会继续读,直到完成10个:

效果:

如果不设置EFD_SEMAPHORE呢?

代码如下:

cpp 复制代码
#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");
            else
                perror("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 触发事件。
  • 所有监听线程收到事件并处理。

看图:

源码:

cpp 复制代码
#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 添加进 epoll
    struct 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),你可以控制它是"一次全拿走"还是"每次拿一个",还可以让它"不阻塞"、"自动关闭"等等。掌握这些标志,就能灵活运用它来做多线程同步、异步通知等高级功能!

相关推荐
哈哈浩丶几秒前
Linux驱动开发2:字符设备驱动
linux·运维·驱动开发
啊森要自信1 分钟前
【Linux 学习指南】网络基础概念(一):从协议到分层,看透计算机通信的底层逻辑
linux·运维·服务器·网络·网络协议·tcp/ip·ip
asdfg12589632 分钟前
策略路由Policy-Based Routing(PBR)
linux·网络·wireshark·网络工程·策略路由
铃木隼.25 分钟前
docker容器高级管理-dockerfile创建镜像
运维·docker·容器
小坏坏的大世界27 分钟前
ROS2中的QoS(Quality of Service)详解
linux·机器人
Ronin3051 小时前
【Linux系统】进程状态 | 进程优先级
linux·运维·服务器·ubuntu
易知嵌入式小菜鸡1 小时前
CCS-MSPM0G3507-7-模块篇-MPU6050的基本使用
linux·运维·服务器
彬彬醤2 小时前
ChatGPT无法登陆?分步排查指南与解决方案
服务器·网络·数据库·网络协议·chatgpt
ᥬ 小月亮2 小时前
webpack高级配置
运维·前端·webpack
浅水鲤鱼2 小时前
欧拉系统安装UKUI桌面环境
linux·运维·服务器