eventfd 和 epoll 的结合使用

一.eventfd介绍
eventfd 是 Linux 的一个系统调用,创建一个文件描述符用于事件通知,自 Linux 2.6.22 以后开始支持。

接口及参数介绍

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

eventfd() 创建一个 eventfd 对象,可以由用户空间应用程序实现事件等待/通知机制,或由内核通知用户空间应用程序事件。
该对象包含了由内核维护的无符号64位整数计数器 count 。使用参数 initval 初始化此计数器。

struct eventfd_ctx {
    struct kref kref;
    wait_queue_head_t wqh;
    /*
     * Every time that a write(2) is performed on an eventfd, the
     * value of the __u64 being written is added to "count" and a
     * wakeup is performed on "wqh". A read(2) will return the "count"
     * value to userspace, and will reset "count" to zero. The kernel
     * side eventfd_signal() also, adds to the "count" counter and
     * issue a wakeup.
     */
    __u64 count;
    unsigned int flags;
};

flags 可以是以下值的 OR 运算结果,用以改变 eventfd 的行为。

  • EFD_CLOEXEC (since Linux 2.6.27)
    文件被设置成 O_CLOEXEC,创建子进程 (fork) 时不继承父进程的文件描述符。
  • EFD_NONBLOCK (since Linux 2.6.27)
    文件被设置成 O_NONBLOCK,执行 read / write 操作时,不会阻塞。
  • EFD_SEMAPHORE (since Linux 2.6.30)
    提供类似信号量语义的 read 操作,简单说就是计数值 count 递减 1。

操作方法
一切皆为文件是 Linux 内核设计的一种高度抽象,eventfd 的实现也不例外,我们可以使用操作文件的方法操作 eventfd。

read(): 读取 count 值后置 0。如果设置 EFD_SEMAPHORE,读到的值为 1,同时 count 值递减 1。read
write(): 其实是执行 add 操作,累加 count 值。
epoll()/poll()/select(): 支持 IO 多路复用操作。
close(): 关闭文件描述符,eventfd 对象引用计数减 1,若减为 0,则释放 eventfd 对象资源。

那获取到eventfd 文件描述符后,我们对其可以做哪些操作呢?

static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS
        .show_fdinfo    = eventfd_show_fdinfo,
#endif
        .release        = eventfd_release,
        .poll           = eventfd_poll,
        .read           = eventfd_read,
        .write          = eventfd_write,
        .llseek         = noop_llseek,
};

通过上面 eventfd 实现的调用可知, 我们可以对eventfd进行 read、write、poll、close等操作。
接下来我们通过一个例子来了解下 eventfd 的具体使用,完整代码可用过 man eventfd 获取 。

int main(int argc, char *argv[])
{
    int efd;
    uint64_t u;
    ssize_t s;

    //创建一个eventfd对象,返回一个文件描述符
    efd = eventfd(0, 0);

    switch (fork()) 
    {
        case 0: //子进程
            for (int j = 1; j < argc; j++) 
            {
                printf("Child writing %s to efd\n", argv[j]);
                u = strtoull(argv[j], NULL, 0);
                //向eventfd内部写一个8字节大小的数据
                s = write(efd, &u, sizeof(uint64_t));
            }

            printf("Child completed write loop\n");

            exit(EXIT_SUCCESS);

       default: //父进程
           sleep(2); //先休眠2秒,等待子进程写完数据

           printf("Parent about to read\n");
           //从eventfd中读取数据
           s = read(efd, &u, sizeof(uint64_t));

           rintf("Parent read %"PRIu64" (%#"PRIx64") from efd\n", u, u);
           exit(EXIT_SUCCESS);

       case -1:
           handle_error("fork");
    }
}

执行结果:

$ ./a.out 1 2 4 7 14
  Child writing 1 to efd
  Child writing 2 to efd
  Child writing 4 to efd
  Child writing 7 to efd
  Child writing 14 to efd
  Child completed write loop
  Parent about to read
  Parent read 28 (0x1c) from efd  // 父进程读到的值为28(1+2+4+7+14)

由于 eventfd 实现的逻辑是累计计数,因此上述例子中父进程读取到的是总计数,读完后内核中的计数会清零。
当用户调用 write 时,内核中会调用 eventfd_write 接口;
eventfd_write 实现了如下功能:

获取用户要写的数据 ucnt,判断 ucnt 和当前 ctx->count 的和是否在最大值范围内:
若在范围内,则把 ucnt 加入到 ctx->count 中,返回。
若超过范围,则判断是否需要阻塞:
若是阻塞调用,则进程休眠,直到满足二者之和在最大值范围内。
若非阻塞调用,则直接返回。

当用户调用 read 时,内核中会调用 eventfd_read 接口。
eventfd_read 实现了如下功能:

判断是否可读(ctx->count),若可读,则读取数据,把数据拷贝到用户态,返回。
若不可读,判断是否是阻塞读:
若是阻塞读,进程休眠,直到可读。
若是非阻塞读,则直接返回。
另外,读取数据时,若是以信号量的方式读取,则每次读到的值为1,计数器减1,否则读取计数器全部的值,同时计数器清零。

二、epoll函数介绍
与 poll 不同的是,epoll 本身并不是一个系统调用。它是一个允许进程在多个文件描述符上复用 I/O 的内核数据结构。
该数据结构通过以下三个系统调用创建、修改、删除。

epoll_create
size参数向内核指定内核进程需要监控的文件描述符的个数,这有助于内核决定epoll实例的大小。从Linux2.6.8开始,这个参数就被忽略了,因为epoll数据结构会随着文件描述符的添加或删除而动态调整大小。

进程通过调用epoll_create来创建epoll实例,后续通过epoll返回的指向epoll实例的文件描述符来进行各种操作,比如添加、删除或者修改它想要件事epoll实例的I/O的其他文件描述符。

#include <sys/epoll.h>
int epoll_create(int size);

在Linux系统中,还有另外一个系统调用函数epoll_create1,其声明如下:

int epoll_create1(int flags);

其中,flags参数可以是0或EPOLL_CLOEXEC。
当flags为0时候,epoll_create1(0)与epoll_create功能一致。
如果设置为EPOLL_CLOEXEC,那么由当前进程fork出来的任何子进程,其都会关闭其父进程的epoll实例所指向的文件描述符,也就是说子进程没有访问父进程epoll实例的权限。
需要注意的是,与epoll实例关联的文件描述符需要通过close()系统调用来释放。多个进程可能持有同一epoll实例的描述符。这是因为,假如没有设置EPOLL_CLOEXEC标志的fork将把描述符复制到子进程中的epoll实例,当这些进程中的某一个或者多个进程关闭了其中一个文件描述符,那么可能会导致程序的不可用,或者不在我们的预期之内。

 有一点需要特别注意,关联 epoll 实例的文件描述符需要通过close()系统调用来释放。多个进程可能持有同一个 epoll 实例的文件描述符(如:当 EPOLL_CLOEXEC标记没有指定时,fork 出来的子进程会复制该文件描述符)。当所有的进程都不再使用该描述符时(通过调用 close() 或者退出),内核才会销毁 epoll 实例。

epoll_ctl
进程可以通过 epoll_ctl 来添加它想要监听的描述符给 epoll 实例。所有注册到 epoll 实例的文件描述符统称为epoll set 或interest list。
在这里插入图片描述上图中,pid 为 483 的进程在 epoll 实例中注册了 FD1,FD2,FD3,FD4,FD5 文件描述符。以上就是 epoll 实例的 interest list 或者 epoll set。随后,当任何文件描述符已经准备好 I/O 时,它们就会放到 ready list 中。ready list 是 interest list 的子集,如图所示:
在这里插入图片描述
epoll_ctl函数的声明如下:

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

在这里插入图片描述

  • epfd: epoll_create函数返回的文件描述符,用于标识内核中的epoll实例。
  • fd:需要被操作的文件描述符
  • op:对fd文件描述符的操作类型。主要有如下几个
  • EPOLL_CTL_ADD 向epfd实例进行注册,在有I/O事件时候获得通知
  • EPOLL_CTL_DEL 从EPOL实例中删除/注销fd。这意味着进程将不再收到关于该文件描述符上事件的任何通知 (EPOLL_CTL_DEL )。如果文件描述符已添加到多个EPOL实例中,则关闭它将从添加到该实例的所有EPOL目标监控列表中删除它。
  • EPOLL_CTL_MOD 修改正在监视的fd事件
  • 在这里插入图片描述
    event: 一个指向一个名为epoll_event的结构的指针,它存储了我们实际要监视fd的事件

在这里插入图片描述
以下是 epoll_event 结构体:

struct epoll_event
{
  uint32_t events;   /* Epoll events */
  epoll_data_t data;    /* User data variable */
} __attribute__ ((__packed__));

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

epoll_event事件结构的第一个字段事件是一个位掩码,它指示要监视哪个事件fd。
在这里插入图片描述
epoll_event事件结构的第二个字段是一个联合字段。

epoll_wait
epoll_wait系统调用,用来监视epoll set/interest集上发生的事件。如果被监视的epoll set/interest集上没有任何I/O事件,则该调用会一直被阻塞,直至有I/O事件产生。
该函数声明如下:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
  • epfd: epoll_create函数返回的文件描述符,用于标识内核中的epoll实例。
  • evlist: epoll事件结构的数组。evlist由调用进程分配,当epoll_wait返回时,修改此数组以指示有关目标监控列表中处于就绪状态的文件描述符子集的信息(这称为就绪列表)
  • maxevents : evlist数组大小
  • timeout:此参数的意思与poll或select相同。此值指定epoll_wait系统调用的阻塞时间
    当设置为0时,代表该函数不会被阻塞,其在检查完目标监控列表中有无I/O事件之后,马上就返回。

当设置为-1时候,该函数将被永久阻塞,进程将处于休眠状态,直到满足下面两个条件(1) 有I/O事件发生 (2) 被信号处理程序中断。
当设置为非负值和非零值时,epoll_wait将阻塞,直到满足有如下几个条件之一(1) 在epfd的目标监控列表中指定的一个或多个描述符就绪,(2) 调用被信号处理程序中断 (3) timeout毫秒指定的时间量已过期。

epoll_wait 函数的返回值有以下几种:
如果发生错误(EBADF或EINTR或EFAULT或EINVAL),则返回代码为-1
如果调用在目标监控列表中的任何文件描述符就绪之前超时,则返回代码为0。
如果目标监控列表中的一个或多个文件描述符准备就绪,则返回代码为正整数,表示evlist数组中的文件描述符总数。然后检查evlist以确定哪些事件发生在哪些文件描述符上。

用下面的代码进行模拟:

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>

int event_fd = -1;

void *read_thread(void *dummy)
{
    uint64_t inc = 1;
    int ret = 0;
    int i = 0;
    for (; i < 2; i++) {
        ret = write(event_fd, &inc, sizeof(uint64_t));
        if (ret < 0) {
            perror("child thread write event_fd fail.");
        } else {
            printf("child thread completed write %llu (0x%llx) to event_fd\n", (unsigned long long) inc, (unsigned long long) inc);
        }
        sleep(4);
    }
}

int main(int argc, char *argv[])
{
    int ret = 0;
    pthread_t pid = 0;

    event_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

    if (event_fd < 0) {
        perror("event_fd create fail.");
    }

    ret = pthread_create(&pid, NULL, read_thread, NULL);
    if (ret < 0) {
        perror("pthread create fail.");
    }

    uint64_t counter;
    int epoll_fd = -1;
    struct epoll_event events[16];

    if (event_fd < 0)
    {
        printf("event_fd not inited.\n");
    }

    epoll_fd = epoll_create(8);
    if (epoll_fd < 0)
    {
        perror("epoll_create fail:");
    }

    struct epoll_event read_event;
    read_event.events = EPOLLIN;
    read_event.data.fd = event_fd;
    ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, event_fd, &read_event);
    if (ret < 0) {
        perror("epoll_ctl failed:");
    }

    while (1) {
        printf("main thread epoll is waiting......\n");
        ret = epoll_wait(epoll_fd, events, 16, 2000);
        printf("main thread epoll_wait return ret : %d\n", ret);
        if (ret > 0) {
            int i = 0;
            for (; i < ret; i++) {
                int fd = events[i].data.fd;
                if (fd == event_fd) {
                    uint32_t epollEvents = events[i].events;
                    if (epollEvents & EPOLLIN) {
                        ret = read(event_fd, &counter, sizeof(uint64_t));
                        if (ret < 0) {
                            printf("main thread read fail\n");
                        } else {
                            printf("main thread read %llu (0x%llx) from event_fd\n", (unsigned long long) counter, (unsigned long long) counter);
                        }
                    } else {
                        printf("main thread unexpected epoll events on event_fd\n");
                    }
                }
            }
        } else if (ret == 0) {
            printf("main thread epoll_wait timed out. continue epoll\n");
        } else {
            perror("main thread epoll_wait error.");
        }
    }
}

设置 eventfd 的计数器初始值为 0 且 flags 为 EFD_NONBLOCK | EFD_CLOEXEC。执行实例代码,结果如下(为了方便分析,让每一次写完阻塞 4 秒,epoll_wait 的超时时间为 2 秒):

wufan@Frank-Linux:~/Linux/test$ ./epoll_eventfd 
main thread epoll is waiting...... // main 线程阻塞在读端
child thread completed write 1 (0x1) to event_fd // 第一次写入后阻塞 4 秒
main thread epoll_wait return ret : 1 // 第一次写完后,立即唤醒 main 线程去进行读操作
main thread read 1 (0x1) from event_fd // main 线程读到了数据
main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒
main thread epoll_wait return ret : 0 // main 线程阻塞等待时间到,返回
main thread epoll_wait timed out. continue epoll
main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒
child thread completed write 1 (0x1) to event_fd // // 第二次写入后阻塞 4 秒
main thread epoll_wait return ret : 1 // 第二次写完后,立即唤醒 main 线程去进行读操作
main thread read 1 (0x1) from event_fd // main 线程读到了数据
main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒
main thread epoll_wait return ret : 0 // main 线程阻塞等待时间到,返回
main thread epoll_wait timed out. continue epoll
main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒
只要没有写入数据,就会在这个死循环中阻塞 -> 超时 -> 阻塞...

在通过实例来理解 eventfd 函数机制中,我们知道了 eventfd 的 EFD_NONBLOCK 模式下,读到计数器的值为 0 后,再继续读,会直接返回一个错误值,不会阻塞。但是上述的例子发现,eventfd 和 epoll 结合使用后,即使我将 flags 设置为 0 和上述执行的结果是一样的。这是为什么?因为按照 Looper.cpp 中的代码逻辑,分别对 epoll_wait 的返回值做了条件判断:

1.ret > 0 说明有可读的值,才会去从 eventfd 中去读;
2.ret == 0 说明超时,不会从 eventfd 中去读;
3.ret < 0 说明 epoll 异常,不会从 eventfd 中去读;

三、应用实例
Android Looper.cpp 的代码,使用 eventfd 和 epoll 这两个结合

# \system\core\libutils\Looper.cpp(Android 8.0 源码)
Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    // 创建 eventfd 的句柄,返回该文件(Linux 中一切皆为文件)读写的描述符
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s",
                        strerror(errno));

    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
    ......
    // 创建一个 epoll 的句柄,EPOLL_SIZE_HINT 是指监听的描述符个数
    // 现在内核支持动态扩展,该值的意义仅仅是初次分配的 fd 个数,后面空间不够时会动态扩容。
    // 当创建完 epoll 句柄后,占用一个 fd 值.
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);

    struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mWakeEventFd;
    // 对 mWakeEventFd 文件描述符进行注册,这样 mEpollFd 就能监听到 mWakeEventFd 的读写事件。
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
    ......
}

int Looper::pollInner(int timeoutMillis) {
#if DEBUG_POLL_AND_WAKE
    ALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);
#endif
    ...... 
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    // 等待 mEpollFd 上的 IO 事件
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ......
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值