美文网首页
通过实例理解 eventfd 和 epoll 的结合使用

通过实例理解 eventfd 和 epoll 的结合使用

作者: wufanguitar | 来源:发表于2019-02-02 00:32 被阅读1次

    一、背景

      如果你看过 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);
        ......
    }
    

      如果你对 eventfd 还不怎么了解,可以先看下这篇文章:通过实例来理解 eventfd 函数机制
      如果你对 epoll 也不怎么了解,可以先看下这篇文章:聊聊 Linux 五种 IO 模型
      之所以写这篇文章,主要是想看下 eventfd 的非阻塞模式在 epoll 中是怎么个表现形式,查了不少文章都没有说清楚,所以才想模拟 Looper.cpp 中的用法,看下究竟到底是怎么样的。

    二、eventfd 和 epoll 的结合

      这里以 Android 8.0 源码 Looper.cpp 作为参考,它的 write 操作时在 wake 函数中实现的:

    void Looper::wake() {
    #if DEBUG_POLL_AND_WAKE
        ALOGD("%p ~ wake", this);
    #endif
    
        uint64_t inc = 1;
        ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
        ......
    }
    

      它的 read 操作时在 awoken 函数中实现的:

    void Looper::awoken() {
    #if DEBUG_POLL_AND_WAKE
        ALOGD("%p ~ awoken", this);
    #endif
    
        uint64_t counter;
        TEMP_FAILURE_RETRY(read(mWakeEventFd, &counter, sizeof(uint64_t)));
    }
    

      所以,用下面的代码进行模拟:

    #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.");
            }
        }
    }
    

      按照 Looper.cpp 源码,设置 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 中去读;

      好,那我们改一下代码,当 ret == 0 时,去执行一下 read 操作。同时将 flags 设置为 0:

        while (1) {
            ......
            } else if (ret == 0) {
                int status = read(event_fd, &counter, sizeof(uint64_t));
                printf("main thread epoll_wait timed out. continue epoll : %d\n", status);
            } else {
                perror("main thread epoll_wait error.");
            }
        }
    
        wufan@Frank-Linux:~/Linux/test$ ./epoll_eventfd 
        main thread epoll is waiting......
        child thread completed write 1 (0x1) to event_fd
        main thread epoll_wait return ret : 1
        main thread read 1 (0x1) from event_fd
        main thread epoll is waiting......
        main thread epoll_wait return ret : 0
        child thread completed write 1 (0x1) to event_fd
        main thread epoll_wait timed out. continue epoll : 8
        main thread epoll is waiting......
        main thread epoll_wait return ret : 0
        一直阻塞在这儿
    

      将 flags 设置为 EFD_NONBLOCK | EFD_CLOEXEC 时,就不会阻塞了。

    三、小结

      底层的知识我也不懂,也刚开始学,搜索了半天、看了不少文章,还是很容陷入一种似懂非懂、不确定的状态,可能这些知识对于懂 Linux 编程的人应该很 Easy。因此,特意将这个学习、验证的过程记录下来。
      另外,Android Looper.cpp 中给 mWakeEventFd 设置 EFD_NONBLOCK,其实并没有发挥它真正地作用(也是在 epoll_wait 返回值 > 0 时才会去 read 操作)。不过,印象中在某处看到过(建议在创建 eventfd 时设置为非阻塞模式,可能是担心代码出问题了,一旦阻塞住了,出现卡死现象),不过,现在终于算是弄明白了!!!

    相关文章

      网友评论

          本文标题:通过实例理解 eventfd 和 epoll 的结合使用

          本文链接:https://www.haomeiwen.com/subject/pzedsqtx.html