【用示例学习与理解C++系列】eventfd与epoll的使用

应用场景及优势

在这里插入图片描述

引用自 通俗易懂说多路复用(3)eventfd 事件通知

Eventfd在信号通知的场景下,相对比pipe有非常大的资源和性能优势, 本质其实是counter(计数器)和channel(数据信道)的区别。
打开文件数量的差异 由于pipe是半双工的传统IPC实现方式,所有两个线程通信需要两个pipe文件描述符,而用eventfd只需要打开一个文件描述符。 总所周知,文件描述符是系统中非常宝贵的资源,linux的默认值只有1024个而已,pipe只能在两个进程/线程间使用,面向连接,使用之前就需要创建好两个pipe,而eventfd是广播式的通知,可以多对多。
内存使用的差别 eventfd是一个计数器,内核维护的成本非常低,大概是自旋锁+唤醒队列的大小,8个字节的传输成本也微乎其微,而pipe完全不同,一来一回数据在用户空间和内核空间有多达4次的复制,而且最糟糕的是,内核要为每个pipe分配最少4k的虚拟内存页,哪怕传送的数据长度为0.
与epoll完美结合 eventfd设计之初就与epoll完美结合,支持非阻塞读取等,就是为epoll而生的,而pipe是Unix时代就有了,那时候不仅没有epoll,连linux还没诞生。
当pipe只用来发送通知,放弃它,放心的使用eventfd。
eventfd配合epoll才是它存在的原

PS: Andorid的Handler机制的底层实现目前也用eventfd替换原来的pipefd

eventfd的头文档

#include <sys/eventfd.h>
头文件中除了定义eventfd的创建接口,还提供了eventfd的计数器值的读与写的接口 eventfd_write与eventfd_read,它们是对write与read的封装

/**
 * [eventfd(2)](http://man7.org/linux/man-pages/man2/eventfd.2.html) creates a file descriptor
 * for event notification.
 *
 * Returns a new file descriptor on success, and returns -1 and sets `errno` on failure.
 */
int eventfd(unsigned int __initial_value, int __flags);

/** The type used by eventfd_read() and eventfd_write(). */
typedef uint64_t eventfd_t;

/**
 * [eventfd_read(3)](http://man7.org/linux/man-pages/man2/eventfd.2.html) is a convenience
 * wrapper to read an `eventfd_t` from an eventfd file descriptor.
 *
 * Returns 0 on success, or returns -1 otherwise.
 */
int eventfd_read(int __fd, eventfd_t* __value);

/**
 * [eventfd_write(3)](http://man7.org/linux/man-pages/man2/eventfd.2.html) is a convenience
 * wrapper to write an `eventfd_t` to an eventfd file descriptor.
 *
 * Returns 0 on success, or returns -1 otherwise.
 */
int eventfd_write(int __fd, eventfd_t __value);

eventfd的创建

创建eventfd的实例的接口为

int eventfd(unsigned int __initial_value, int __flags);

第一个参数为:eventfd的计数器的初始化
第二个参数为:eventfd的属性,配置参数是bitset的flag

三个bitset如下

/** The eventfd() flag to provide semaphore-like semantics for reads. */
#define EFD_SEMAPHORE (1 << 0)
/** The eventfd() flag for a close-on-exec file descriptor. */
#define EFD_CLOEXEC O_CLOEXEC
/** The eventfd() flag for a non-blocking file descriptor. */
#define EFD_NONBLOCK O_NONBLOCK

EFD_SEMAPHORE: 配置eventfd的计数器形式为信号量,即调一次read接口,计数器减一;如果不是信号量的形式,调一次read接口后计数量清零
EFD_CLOEXEC: 配置进程fork时,父进程的eventfd要关闭
EFD_NONBLOCK: 配置eventfd于非阻塞方式,阻塞与非阻塞体现在对eventfd的读写上

event的读写

event的读写可以用read与write接口,但eventfd也有作了封装,即用 #include <sys/eventfd.h>的读写接口比较方式

计数器的属性与读写

对eventfd的每次写操作,eventfd的计数器都为累加;需要注意的是读操作依赖于计数器的属性,如果信号量方式的eventfd,每次对它做写操作后计数器减1,否侧计数器清零;
还有注意一点是信号量方式下,每次可读操作读出来值是1,而不是具体的计算器的值;

阻塞模式与读写

对eventfd的读写操作,默认是阻塞的。即当eventfd不可读或不可写了时读调用对应的读写接口会阻塞住当前线程;如果是非阻塞模式下,读与写不会阻塞,只是读写失败,接口返回码为-1

  • 在阻塞模式下如果对eventfd作写操作
    计数器最大能存储的值是0xfffffffffffffffe(以MAX表示此值),
    那么写尝试将value累加到计数器上,如果结果超过MAX,侧阻塞住当前线程,直到其它线程成功对eventfd做了读操作

  • 在阻塞模式下对eventfd作读操作的
    如果计数器的值等于零,即阻塞住当前线程,直到它线程成功对evenfd做了写操作

eventfd的示例

eventfd的读写-wirte与read

/**
 * 创建一个非阻塞的eventfd,并使用read与write读写数据
 */
void demo_write_read() {
    int eventfd1 = eventfd(2, EFD_NONBLOCK);
    uint64_t ret = 0;
    int invokeRet = 0;
    uint64_t writeValue = 3;
    write(eventfd1, &writeValue, sizeof(uint64_t));
    invokeRet = read(eventfd1, &ret, sizeof(uint64_t));
    __android_log_print(ANDROID_LOG_INFO, TAG, "invokeRet: %d, read:%u", invokeRet, ret);

    //测试当计数据为零时做读操作
    for (int i = 0; i < 2; i++) {
        ret = 0;
        invokeRet = read(eventfd1, &ret, sizeof(uint64_t));
        __android_log_print(ANDROID_LOG_INFO, TAG, "i: %d, invokeRet: %d, read:%u", i, invokeRet,
                            ret);
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "demo_write_read end");
}

运行结果
在这里插入图片描述

eventfd的读写-eventfd_write与eventf_read

/**
 * 创建一个非阻塞的eventfd,并使用eventfd_red与eventfd_write 读写数据
 * 设置eventfd为非阻塞是因为示例会在eventfd的值为0时,还尝试从中读取值
 * 如果是阻塞模式下,当eventfd为零时,读操作会阻塞住当时调用线程
 */
void demo_eventfd_write_read() {
    int eventfd1 = eventfd(2, EFD_NONBLOCK);
    uint64_t ret = 0;
    int invokeRet = 0;
    eventfd_write(eventfd1, 3);
    invokeRet = eventfd_read(eventfd1, &ret);
    __android_log_print(ANDROID_LOG_INFO, TAG, "invokeRet: %d, read:%u", invokeRet, ret);

    for (int i = 0; i < 2; i++) {
        ret = 0;
        invokeRet = eventfd_read(eventfd1, &ret);
        __android_log_print(ANDROID_LOG_INFO, TAG, "i: %d, invokeRet: %d, read:%u", i, invokeRet,
                            ret);
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "demo_eventfd_write end");
}

运行结果
在这里插入图片描述

eventfd与epoll-计数器为普通模式

 demo_epoll_eventfd()

运行结果
在这里插入图片描述

eventfd与epoll-计数器为信号量模式

demo_epoll_eventfd_semaphore()

运行结果
在这里插入图片描述

eventfd示例主要代码

PS: 用Anddroid A创建一个JNI工程,然后copy如下代码覆盖到native-lib.cpp文件

#include <jni.h>
#include <string>
#include <thread>
#include <unistd.h>
#include <sys/epoll.h>
#include <android/log.h>
#include <sys/eventfd.h>

using namespace std;

static char *TAG = "JNI-DEMO";
static static int MAX_EVENTS_SIZE = 1024;

/**
 * 创建一个非阻塞的eventfd,并使用read与write读写数据
 */
void demo_write_read() {
    int eventfd1 = eventfd(2, EFD_NONBLOCK);
    uint64_t ret = 0;
    int invokeRet = 0;
    uint64_t writeValue = 3;
    write(eventfd1, &writeValue, sizeof(uint64_t));
    invokeRet = read(eventfd1, &ret, sizeof(uint64_t));
    __android_log_print(ANDROID_LOG_INFO, TAG, "invokeRet: %d, read:%u", invokeRet, ret);

    //测试当计数器为零时做读操作,默认的计数器是一次性消费,即作一个读操作后,计数器清零
    for (int i = 0; i < 2; i++) {
        ret = 0;
        invokeRet = read(eventfd1, &ret, sizeof(uint64_t));
        __android_log_print(ANDROID_LOG_INFO, TAG, "i: %d, invokeRet: %d, read:%u", i, invokeRet,
                            ret);
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "demo_write_read end");
}

/**
 * 创建一个非阻塞的eventfd,并使用eventfd_red与eventfd_write 读写数据
 * 设置eventfd为非阻塞是因为示例会在eventfd的值为0时,还尝试从中读取值
 * 如果是阻塞模式下,当eventfd为零时,读操作会阻塞住当时调用线程
 */
void demo_eventfd_write_read() {
    int eventfd1 = eventfd(2, EFD_NONBLOCK);
    uint64_t ret = 0;
    int invokeRet = 0;
    eventfd_write(eventfd1, 3);
    invokeRet = eventfd_read(eventfd1, &ret);
    __android_log_print(ANDROID_LOG_INFO, TAG, "invokeRet: %d, read:%u", invokeRet, ret);

    for (int i = 0; i < 2; i++) {
        ret = 0;
        invokeRet = eventfd_read(eventfd1, &ret);
        __android_log_print(ANDROID_LOG_INFO, TAG, "i: %d, invokeRet: %d, read:%u", i, invokeRet,
                            ret);
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "demo_eventfd_write end");
}

void demo_eventfd_write_read_semaphore() {
    int eventfd1 = eventfd(2, EFD_NONBLOCK | EFD_SEMAPHORE);
    uint64_t ret = 0;
    int invokeRet = 0;
    eventfd_write(eventfd1, 3);
    invokeRet = eventfd_read(eventfd1, &ret);
    __android_log_print(ANDROID_LOG_INFO, TAG, "invokeRet: %d, read:%u", invokeRet, ret);

    //由于是信号量的方式,故可以再读4次,上前已经读了一次,读一个计数器减1 (上面计数器初始值为2,然后读操作累加3,然后读一次现在计算器为4)
    for (int i = 0; i < 5; i++) {
        ret = 0;
        invokeRet = eventfd_read(eventfd1, &ret);
        __android_log_print(ANDROID_LOG_INFO, TAG, "i: %d, invokeRet: %d, read:%u", i, invokeRet,
                            ret);
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "demo_eventfd_write end");
}

/**
 *
 * eventfd与epoll的使用,这里是结合两个线程的通讯为示例,不是进程交互的示例
 * @param semaphore 是否为信号量的工作方式
 */
void demo_epoll_eventfd_imp(bool semaphore) {
    int efd = eventfd(1, EFD_NONBLOCK | (semaphore ? EFD_SEMAPHORE : 0));
    if (efd == -1) {
        __android_log_print(ANDROID_LOG_WARN, TAG, "eventfd fail");
        return;
    }

    int epollFd = epoll_create(1024);
    if (epollFd == -1) {
        __android_log_print(ANDROID_LOG_WARN, TAG, "epoll_create fail");
        return;
    }

    struct epoll_event ev;
    ev.data.fd = efd;
    //水平触发方式,监听可读事件,即管道有数据可以读取了
    ev.events = EPOLLIN | EPOLLET;
    //监听事件
    int result = epoll_ctl(epollFd, EPOLL_CTL_ADD, efd, &ev);
    if (result == -1) {
        __android_log_print(ANDROID_LOG_WARN, TAG, "epoll_ctl fail");
        return;
    }

    thread threadWrite([efd] {
        while (true) {
            int invokeRet = eventfd_write(efd, 2);
            __android_log_print(ANDROID_LOG_INFO, TAG, "write:2, invokeRet:%d", invokeRet);
            invokeRet = eventfd_write(efd, 3);
            __android_log_print(ANDROID_LOG_INFO, TAG, "write:3, invokeRet:%d", invokeRet);
            this_thread::sleep_for(chrono::seconds(2));
        }
    });
    threadWrite.detach();

    thread threadRead([efd, epollFd] {
        struct epoll_event events[MAX_EVENTS_SIZE];
        while (true) {
            uint64_t ret = 0;
            int count = epoll_wait(epollFd, events, MAX_EVENTS_SIZE, 3000);
            __android_log_print(ANDROID_LOG_INFO, TAG, "epoll_wait, count:%d", count);
            for (int i = 0; i < count; i++) {
                if ((events[i].data.fd == efd) && (events[i].events && POLL_IN)) {
                    for (int i = 0; i < 7; i++) {
                        ret = 0;
                        int invokeRet = eventfd_read(efd, &ret);
                        __android_log_print(ANDROID_LOG_INFO, TAG, "[i=%d], read:%u, invokeRet:%d",
                                            i, ret,
                                            invokeRet);
                    }
                }
            }
        }
    });
    threadRead.detach();
}

void demo_epoll_eventfd() {
    demo_epoll_eventfd_imp(false);
}

void demo_epoll_eventfd_semaphore() {
    demo_epoll_eventfd_imp(true);
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
//    demo_write_read();
//    demo_eventfd_write_read();
//    demo_eventfd_write_read_semaphore();
//    demo_epoll_eventfd();
    demo_epoll_eventfd_semaphore();
    return env->NewStringUTF(hello.c_str());
}

参考文章

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值