手写Looper(二)---- 使用epoll

917 阅读5分钟

手写Looper(二)---- 使用epoll

根据前面手写Looper(一)---- 使用eventfd了解的eventfd ,已经可以很容易实现Looper了。因为netive层的Looper就是一个简单的睡眠唤醒机制的实现,而eventfd本身来实现这个机制的。所以通过对eventfd包装一下就可以实现一个简单的Looper了。但eventfd还有一些缺陷,当一个线程阻塞在read方法中时,会一直阻塞下去,直到另一个线程写入内容。而我们还有超时的诉求,即read的时候设定一个超时时间,若是阻塞的时间超过了超时时间,则也唤醒的当前线程。这是考虑到我们会发送延迟消息,所以在延迟的时间到达后唤醒线程去处理事件。

epoll简介

epoll是一种事件通知机制,可以用来监控多个fd的读写状态。如前面了解的eventfd,若是存在多个eventfd,有一个线程的需求是监听这多个fd,不论哪个可读的时候,都去处理某些事件。我们就很难去做到上述的场景,或许可以创建eventfd的时候将其设置为非阻塞式的,然后循环read这几个fd,若是有可读的就去处理事件,否则继续循环读取。但这样实现有很大的问题,首先是CPU的问题,由于read是非阻塞式的,会导致程序在fd都不可读的时候一直处于循环中,耗费大量的CPU资源。其次,若是fd不是当前线程打开的,即无法控制其read是否是阻塞式的,上述方案也是不可行的。

epoll就是用来应对这种情况的,使用epoll可以同时监听多个fd,当添加其中的fd可读或者可写后就会唤醒epoll。同时也可以设置超时时间,超时后也会返回。

创建epoll

#include <sys/epoll.h>

int epoll_create (int __size)
int epoll_create1 (int __flags)

创建epoll有两个方法,第一个方法传入了size的大小,表示epoll可以监听的fd的个数,目前已经弃用,若仍使用的话,size会被舍弃,但是为了兼容旧版本,size的值不能是复数。第二个方法创建epoll可以传入flag,可以传入EPOLL_CLOEXEC.

操作注册fd

#include <sys/epoll.h>

#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2 
#define EPOLL_CTL_MOD 3 

int epoll_ctl (int __epfd, int __op, int __fd,  struct epoll_event *__event)

epoll增加、删除、修改fd都是通过epoll_ctl去操作的,其中参数__epfd是epoll实例,而参数__op代表具体的操作,是定义的三个常量,分别是EPOLL_CTL_ADDEPOLL_CTL_DELEPOLL_CTL_MOD,即当前操作是增加还是删除或者修改。第三个参数是需要操作的文件描述符。 第四个参数epoll_event是当前要操作的文件描述符的事件,跟文件描述符绑定的。

struct epoll_event

typedef union epoll_data  
{  
    void *ptr;  
    int fd;  
    uint32_t u32;  
    uint64_t u64;  
} epoll_data_t;  
  
struct epoll_event  
{  
    uint32_t events; 
    epoll_data_t data;
} __EPOLL_PACKED;

如上,其有两个字段。events代表的是监听的fd的事件操作,定义了一堆的枚举量,最常用的就是EPOLLIN(可读时触发)和EPOLLOUT(可写时触发)。第二个字段data是携带的参数,用于辨别当前触发的是哪个文件描述符。通常会将data.fd赋值为对应的fd。当epoll唤醒的时候,会将注册时传的event返回回来,需要通过event去判读是哪个fd唤醒的。

EPOLL_EVENT

enum EPOLL_EVENTS  
{  
    EPOLLIN = 0x001,  // fd可读时触发
    #define EPOLLIN EPOLLIN  
    EPOLLOUT = 0x004,  // fd可写时触发
    #define EPOLLOUT EPOLLOUT  
    EPOLLET = 1u << 31  // 将epoll的触发方式设置为ET模式
    #define EPOLLET EPOLLET  
    ...
};

EPOLL_EVENT是注册fd的时候需要设置的事件,用于设置当前注册的fd可以如何被触发唤醒。若是设置为了EPOLLIN,当文件可读的时候,会唤醒EPOLL。若是设置了EPOLLOUT,则当fd可写时唤醒EPOLL。

事件中还有一个比较重要的值,EPOLLET,即将触发方式设置为ET模式。EPOLL支持两种触发模式,level-triggeed(LT)模式和edge-triggered(ET)模式,EPOLL默认是LT模式

LT

fd只要可读,就一直触发可读事件。fd只要可写入,就会一直触发可写事件。因此若是fd可读,需要将缓存区中的所有内容都读完,否则下次循环还会触发可读事件。同理,可写时也是如此。

ET

可读事件触发方式:1.fd从不可读变为可读时;2.fd缓存区内容增多时,即缓存区已有内容,然后又被写入了其他内容的时候会触发;3.fd缓存区不为空,并且通过EPOLL_CTL_MOD修改后出触发。可写事件的触发方式同理一样。

等待epoll返回

#include <sys/epoll.h>

int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

epoll_wait是阻塞式的,该方法会等待注册的fd进行唤醒。例如注册的fd是EPOLLIN,那么当fd可读的时候,epoll_wait就会返回。第一个参数是epoll实例,第二个参数是一个数组,其中包含的是唤醒的fd对应的poll_event,因为epoll是同时监听多个fd的。第三个参数是最大的event个数,通常是events数组的长度。最后一个参数是超时时间,单位毫秒,超过该时间后会直接唤醒,不论是否有fd注册的条件达到,设置为-1的话表示没有超时时间。

返回值是唤醒的fd的个数,此时会将fd对应的event写入events数组中,从第一个开始写入。因此唤醒后,需要根据返回值去遍历events,然后查看是哪个fd唤醒的。

epoll使用示例

#include <stdio.h>  
#include <sys/epoll.h>  
#include <sys/eventfd.h>  
#include <pthread.h>  
#include <unistd.h>  
  
#define MAX_COUNT 10  
  
void *thread_write(void *data) {  
    int *eventfd = (int *) data;  
    eventfd_t ecount = 11;  
    // 子线程每隔1秒向eventfd中写入一个值用于将eventfd唤醒  
    for (int i = 0; i < 3; ++i) {  
        int res = eventfd_write(*eventfd, ecount);  
        if (!res) {  
            printf("write count: %zu\n", ecount);  
        }  
        sleep(1);  
    }  
    return NULL;  
}  
  
  
int main_epoll() {  
    // 创建epoll实例  
    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);  
    if (epoll_fd == -1) {  
    printf("create epoll error.\n");  
        return -1;  
    }  
    // 创建eventfd,使用epoll实现阻塞,eventfd就不需要再阻塞了  
    int eventfd_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);  
    if (eventfd_fd == -1) {  
        printf("create efd error.\n");  
        return -1;  
    }  
    // 注册事件,监听可读事件  
    struct epoll_event event;  
    event.events = EPOLLIN;  
    event.data.fd = eventfd_fd;  
    int res = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, eventfd_fd, &event);  
    if (res) {  
        printf("epoll_ctl error.\n");  
    }   
    pthread_t thread;  
    res = pthread_create(&thread, 0, thread_write, &eventfd_fd);  
    if (res) {  
        printf("create thead fail.\n");  
    }  
    // 定义接收事件的event数组  
    struct epoll_event events[MAX_COUNT];  
    while (1) {  
        int count = epoll_wait(epoll_fd, events, MAX_COUNT, -1);  
        printf("wait count: %d\n", count);  
        if (count > 0) {  
            // 遍历唤醒的event,然后去判读是否是我们关注的可读事件  
            for (int i = 0; i < count; ++i) {  
                struct epoll_event tmp = events[i];  
                if (tmp.events == EPOLLIN) {  
                    printf("fd: %d\n", tmp.data.fd);  
                    // 默认时LT模式,所以需要将eventfd中的值读取出来,否则会一直触发可读事件  
                    eventfd_t event_value;  
                    eventfd_read(tmp.data.fd, &event_value);  
                } else {  
                    printf("other event.\n");  
                }
            }  
        } else {  
            printf("wait count zero.\n");  
        }  
    }  
    // 不会执行到这里,不过不要忘记关闭fd  
    close(eventfd_fd);  
    close(epoll_fd);  
    return 0;  
}

image.png

总结

epoll类似于一个管家,它可以管理着多个文件描述符,当文件描述符的状态触发某个条件后,就会直接唤醒eoll的等待。epoll可以添加多个文件描述符,并且支持多种类型的fd,不仅仅是eventfd,像file、socketfd、pipe等都是可以的,并且可以设置超时唤醒。