epoll API解析及在redis中的应用

20 篇文章 0 订阅

本文主要参考https://man7.org/linux/man-pages/man7/epoll.7.html这个Linux/unix线上操作手册。 

epoll

epoll API的功能和select,poll等多路io复用接口类似,是用来监听多个 文件描述是否有io事件发生。epoll API的核心概念是epoll实体,从用户空间的的角度来看,epoll实体是操作系统内核的一个数据结构,可以先简单的认为它包含有两个链表:

  1. interest list,或者叫epoll set,即epoll监听事件集合,是用来保存epoll实体所有监听的文件描述符。它是一个文件描述符集合。比如说,我对某个socket感兴趣,我要监听这个socket,我可以通过epoll API把这个socket的文件描述符加入到一个epoll实体的监听集合里边,委托epoll来监听这个socket的io事件,如果有io事件发生时让epoll来通知。
  2. ready list,就绪链表,用来保存已经有io事件发生了的文件描述符集合。这个ready list可以认为是interest list的子集,意思是ready list保存的是interest list中已经发生了事件的文件描述符。当内核监听到interest list中的文件描述符有io事件发生后就会把这个文件描述符加入到ready list中。

Linux提供了几个系统调用来创建和管理一个epoll实体,分别是epoll_create,epoll_ctl,epoll_wait. 

int epoll_create(int size) 

创建一个epoll实体,返回一个新epoll实体相关的文件描述符,size用来告诉内核这个监听的数目一共有多大。size参数只是告诉内核这个 epoll实体会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没实际已经没有意义,但是为了兼容最好是填一个正数。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

这个系统调用是用来添加,修改,或者删除epoll对象的interest list监听事件集合条目。epfd是epoll实体的文件描述符;op是操作类型,增删改查等;fd是所监听事件的文件描述符;event则是监听的io事件类型,可读可写等;

其中op的有效值有以下这三种,分别是增删改:

EPOLL_CTL_ADD往监听集合中添加一个事件
EPOLL_CTL_MOD修改监听集合的一个事件
EPOLL_CTL_DEL删除监听集合的一个事件

 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 events */
               epoll_data_t data;        /* User data variable */
           };

当一个文件描述符已经就绪的时候,data成员是用来指定内核返回值的(epoll_wait的返回值)。它是一个union,一般是返回就绪事件的fd。

The data member of the epoll_event structure specifies data that the kernel should save and then return (via epoll_wait(2)) when this file descriptor becomes ready.

后面解析的epoll_wait接口也会将返回的数据复制到epoll_event数组,对于epoll_event的data成员,epoll_wait返回的就是调用epoll_ctl注册事件的时候所填入的值。例如,epoll_ctl填的是一个fd,那么epoll_wait返回的也是fd。后面会举一个具体的例子,以redis为例。

events是一个位掩码,一般包含以下这几种类型:

EPOLLIN文件描述符可以读
EPOLLOUT文件描述符可以写
EPOLLPRI文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR文件描述符发生错误
EPOLLHUP文件描述符被挂断
EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

 wait for an I/O event on an epoll file descriptor

 

顾名思义,epoll_wait是用来 等待一个文件描述符io事件发生的。参数events是分配好的epoll_event结构体数组,内核将ready list就绪列表中的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,避免复制的时候越界,当然maxevents必须大于0。

timeout参数用来指定epoll_wait()堵塞的毫秒数。epoll_wait()会一直堵塞直到发生了以下事件:

  1. 监听的文件描述符发生了一个事件;
  2. 调用被一个信号处理程序打断,这种情况errno为EINTR
  3. 超时退出,timeout指定的时候超时了也没有事件发生也会返回。

Note that the timeout interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the blocking interval may overrun by a small amount. Specifying a timeout of -1 causes epoll_wait() to block indefinitely, while specifying a timeout equal to zero cause epoll_wait() to return immediately, even if no events are available. 

超时时间会被对齐到系统时钟 粒度,加上由于内核调度的延时意味着实际的堵塞时长会稍稍超出timeout。如果timeout为-1,那就一直堵塞到有事件发生。如果timeout等于0,epoll_wait不管有没有事件发生都会马上返回。

The data field of each returned epoll_event structure contains the same data as was specified in the most recent call to epoll_ctl(2) (EPOLL_CTL_ADD, EPOLL_CTL_MOD) for the corresponding open file descriptor.

 events数组指针这个参数,其实是个出参,返回的每个epoll_event结构体的data字段的数据就是epoll_ctl接口所指定的。evens是位掩码,用来表面发生的事件类型。 

如果成功,epoll_wait会返回发生了事件的文件描述符数量;如果一直到超时结束都没有事件发生就返回0;如果发生错误就返回-1,具体的错误原因可以由errno判断。

epoll_wait这个接口的作用简单讲就是,通过返回值告诉你有多少个事件发生,事件的具体信息保存到了一个数组中,里边保存有事件的fd和事件的类型。这样上层就可以根据fd和事件的类型来进行相应的处理。

redis应用示例

这里以redis3.0中epoll中的实际使用为例子, 文件是src/ae_epoll.c。只有一百多行代码,非常简洁。具体解析见代码注释。

typedef struct aeApiState {

    // epoll实体描述符,也就是epoll_creat创建的实体fd
    int epfd;

    // 事件槽,用来保存epoll_wait返回的数据
    struct epoll_event *events;

} aeApiState;

/*
 * 创建一个新的 epoll 实例,并将它赋值给 eventLoop,eventLoop这里先不用管,是redis对事件的一个封装
 */
static int aeApiCreate(aeEventLoop *eventLoop) {

    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;

    // 初始化事件槽空间,给events数组分配内存
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }

    // 创建 epoll 实例
    state->epfd = epoll_create(1024);
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }

    // 赋值给 eventLoop
    eventLoop->apidata = state;
    return 0;
}

上面这段代码主要就是创建一个epoll实体,并为events数据分配内存,后面epoll_wait会用到。 接下来就是要往epoll中注册监听事件。

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;

    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. 
     *
     * 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。
     *
     * 如果已经关联了某个/某些事件,那么这是一个 MOD 操作。
     */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;
    //上面是填写操作类型
    // 注册事件到 epoll
    ee.events = 0;//事件位掩码,指明要监听可读还是可写事件
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;//这里指的了data为socket的fd,那么后面epoll_wait返回时也是填fd

    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;

    return 0;
}

源码中还有一个删除监听事件的接口,过程都差不多,这里就不讲了,接下来看事件发生时epoll_wai如何处理。 

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    //state->events就是第一步申请了内存的那个数组,用来保存返回值
    // 如果没有设置等待时间,那就把等待时间设为-1,表示会一直堵塞直到事件发生
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

    // 有至少一个事件就绪
    if (retval > 0) {
        int j;

        // 为已就绪事件设置相应的模式
        // 并加入到 eventLoop 的 fired 数组中
        numevents = retval;
        //挨个把events数组中的epoll_event取出来
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;
            //根据掩码判断事件类型
            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    //redis并没有在这里直接处理事件,而是先把事件保存到fired数组,后面统一处理
    // 返回已就绪事件个数
    return numevents;
}

 通过epoll_wait ,我们已经知道了那些fd发生了事件,我们还能通过mask知道事件的类型。例如,我们可以根据fd找到某个socket,如果mask是可读,那么我就掉read函数从这个socket读取数据,如果是可写那就调writ写数据。

redis中,上面已经把事件保存到了fired数组,那么接下来就挨个遍历这个数组对事件进行处理,调用事先注册好的回调函数来处理读写事件。

// 处理文件事件,阻塞时间由 tvp 决定
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            // 从已就绪数组中获取事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

           /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            // 读事件
            if (fe->mask & mask & AE_READABLE) {
                // rfired 确保读/写事件只能执行其中一个
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);//读回调函数
            }
            // 写事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);//写回调函数
            }

            processed++;
        }

 

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值