手写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;  
}

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

总结

epoll类似于一个管家,它能够办理着多个文件描述符,当文件描述符的状况触发某个条件后,就会直接唤醒eoll的等候。epoll能够增加多个文件描述符,而且支撑多种类型的fd,不仅仅是eventfd,像file、socketfd、pipe等都是能够的,而且能够设置超时唤醒。