应用场景及优势
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());
}