事件驱动:客户端消息是如何处理的

事件驱动:客户端消息是如何处理的

Redis 系列目录:https://www.cnblogs.com/binarylei/p/11721921.html

现在,我们开始学习新的请求处理模块。相信大家一定都听说过,Reids 之所以快,一个重要的原因就是 NIO 网络模型和单线程处理,我们就带着这个问题,看一下 Redis 是如何实现的。

在这个模块,我们重点分析请求是如何处理的,包括事件驱动机制、请求解析处理的过程。今天,我们先来学一下 Redis 是如何实现事件驱动的。

我们知道,NIO 是一种 IO 多路复用技术,网络 IO 操作都会触发读写事件,那如何处理这些事件呢?Redis 统一封装了一个轻量级的事件驱动库 - src/ae.h 和 ae.c。这是一个易于复用的库,可以脱离 Redis 而存在。事实上,ae.c 除封装文件事件,还是时间事件来处理内部的定时任务。

AE 库

AE(async event) 库实现类似 Netty 中的 event loop 机制,并支持多种 IO 多路复用。在深入学习 AE 库的实现之前,我们先整体看一下整体的工作逻辑。

图1 AE库工作流程

可以看到,ae 库由两大部分组成:aeEventLoop 事件处理器和 epoll 管理器。

aeEventLoop 事件处理器,定义在 ae.c 和 ae.h 文件中,是 AE 库的核心,它存放了所有的文件事件 event 和 时间事件 timeEventHead。

epoll 管理器,即 polling API,统一封装了 IO 多路复用,给 ae.c 文件调用。包括 ae_epoll.c、 ae_select.c、 ae_kqueue.c、 ae_evport.c 这 4 个文件,他们的功能是完全一样的,提供一致的 API 接口。

这两个部分的交互点在于,aeEventLoop 将这些文件事件 event 注册到 epoll 管理器上,epoll 管理器上不断地通轮询拉取触发的文件事件到 fired 中。

同时,aeEventLoop 事件处理器除了文件事件外,还管理了时间事件。

aeEventLoop

前面说了,aeEventLoop 是整个 AE 库最核心的结构体。它虽然是用 c 开发的,但却采用了面向对象的设计。下面我用注释的方式说明各字段的作用。

typedef struct aeEventLoop {
    int maxfd;                      /* 当前注册的最大fd */
    int setsize;                    /* 监视的fd的最大数量 */
    long long timeEventNextId;      /* 下一个时间事件的ID */
    time_t lastTime;                /* 上次时间事件处理时间 */
    aeFileEvent *events;            /* 已注册文件事件数组 */
    aeFiredEvent *fired;            /* 就绪的文件事件数组 */
    aeTimeEvent *timeEventHead;     /* 时间事件链表的头 */
    int stop;                       /* 是否停止(0:否;1:是)*/
    void *apidata;                  /* 各平台polling API所需的特定数据 */
    aeBeforeSleepProc *beforesleep; /* 事件循环休眠开始的处理函数 */
    aeBeforeSleepProc *aftersleep;  /* 事件循环休眠结束的处理函数 */
} aeEventLoop;

其中,我们重点要关注 eventsfiredtimeEventHead 这几个字段。

  • events:保存了所有的文件事件,这些文件事件需要注册具体的读写事件到 epoll 上。events是一个数组结构,数组的下标是文件句柄 fd,数组的大小是 setsize

    events 需要多大合适呢? maxclients 参数表示 Redis 服务器最大的客户端连接数,默认是 10000;同时,Liunx ulimit -n 可以查看每个进程自大的句柄数,默认为 1024。实际 Reids 的最大连接数受这两个参数限制。

  • fired:注册的 event 事件触发读写后,epoll 会先将事件保存到 fired 字段中。 fired 也是一个数组结构,大小和 events 相同。fired 保存了触发读写事件的句柄 fd,以及触发的事件类型,这样通过 fd 就可以找到具体的事件 events 了。

  • timeEventHead:就是时间事件,这是一个链表结构。每次轮询时,都会遍历这个链表,如果已经到了执行时间,就会触发任务执行,我想着就是为啥叫它时间事件的原因了。

两种事件

前面一直提到文件事件和时间事件,这到底是什么呢?我稍微解释一下:

  • 文件事件:也就是 IO 读写事件。因为 Linux 中一切皆文件,所以这里把网络 IO 读写事件统一称为文件事件。
  • 时间事件:可以理解为 Redis 内部的定时任务,最主要的定时任务就是 service.c#serverCron,默认每 100ms 执行一次。根据执行时间判断是否被触发,所以称为时间事件。

接下来,我们看一下,文件事件和时间事件的定义,它们都是在 ae.h 文件中。

aeFileEvent

文件事件 aeFileEvent 定义了事件的类型和处理函数。这里你可能会问,为啥没有句柄 fd?聪明的你可能已经想到了,aeEventLoop.events 的下标就是句柄 fd

其中 AE_READABLE 是读事件,包括了 AcceptRead 事件。rfileProc 定义了对应的读事件处理函数,Accept 事件是 network.c#acceptTcpHandler,Read 事件是 network.c#readQueryFromClient。AE_WRITABLE 是写事件,对应的处理函数是 network.c#sendReplyToClient

typedef struct aeFileEvent {
    int mask;               // 事件类型 AE_(READABLE|WRITABLE|BARRIER)
    aeFileProc *rfileProc;	// 读事件处理:network.c#readQueryFromClient or acceptTcpHandler
    aeFileProc *wfileProc;  // 写事件处理:network.c#sendReplyToClient
    void *clientData;		// 对应的client
} aeFileEvent;

我们再看一下文件事件触发的定义。也很简单,保存了文件句柄和事件类型。通过下标 fd 找到对应的事件,再调用对应的事件处理函数。

typedef struct aeFiredEvent {
    int fd;		// 句柄
    int mask;   // 触发的事件类型
} aeFiredEvent;

aeTimeEvent

时间事件就不做过多解释了,大家看下代码注释就明白了。

typedef struct aeTimeEvent {	 	
    monotime when;			// 执行时间 
    aeTimeProc *timeProc;	// 执行方法,返回下次执行的事件间隔,如果不为-1则表示是周期执行
    struct aeTimeEvent *prev;
    struct aeTimeEvent *next;
} aeTimeEvent;

事件处理流程

ae.c 库处理事件驱动的入口是 aeMain,它不断调用 aeProcessEventsprocessTimeEvents 函数分别处理文件事件和时间事件。我画了一张图,包含这两种事件的处理流程,如下图所示:

图2 AE库执行流程
  • aeMain:函数入口,不断轮询,调用 aeProcessEvents 函数执行。
  • aeProcessEvents:调用 polling API 拉取已触发的文件事件,并执行文件事件和时间事件。
  • processTimeEvents:执行时间事件。遍历所有的时间事件链表,如果执行时间 when 大于当前时间,则触发任务执行;如果执行返回的下一次执行事件间隔不为 -1,则更新该事件的下次执行时间 when,否则删除该任务。

aeProcessEvents

aeProcessEvents 函数是这个 AE 库的核心,下面我用代码注释的方式,说明事件处理的大体逻辑。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
                int j;
        struct timeval tv, *tvp;
        int64_t usUntilTimer = -1;
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            usUntilTimer = usUntilEarliestTimer(eventLoop);
        if (usUntilTimer >= 0) {
            tv.tv_sec = usUntilTimer / 1000000;
            tv.tv_usec = usUntilTimer % 1000000;
            tvp = &tv;
        } else {
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                tvp = NULL;
            }
        }
        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }
        
		// 1.1 eventLoop->beforesleep,调用 server.c->beforesleep
        if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
            eventLoop->beforesleep(eventLoop);

		// 1.2 select->poll,底层NIO事件轮询
        numevents = aeApiPoll(eventLoop, tvp);

		// 1.3 eventLoop->aftersleep,调用 server.c->aftersleep
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);
		
		// 2 文件事件处理 aeFileEvent
        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 fired = 0; /* Number of events fired for current fd. */
            int invert = fe->mask & AE_BARRIER;
            // 2.1 处理读事件 AE_READABLE
            if (!invert && fe->mask & mask & AE_READABLE) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
            }

            // 2.2 处理写事件 AE_WRITABLE
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            processed++;
        }
    }
    
	// 3. 时间事件处理 aeTimeEvent
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed;
}

代码看上去有点多,但其实这个方法做的事情却很简单。aeProcessEvents 函数先处理了网络 IO 读写事件(文件事件),再处理时间事件。

我们先看一下文件事件的处理。先调用 aeApiPoll 函数轮询这段时间触发的 IO 事件,将触发的事件放到 fired 数组中,然后一次遍历这个 fired 数组,分别处理读事件和写事件。

那你可能会想,既然 aeMain 函数会不断调用 aeProcessEvents 函数,会不会陷入死循环当中啊?先告诉你答案,并不会,因为 aeApiPoll 函数会没有网络读写时会阻塞的。那阻塞多长时间合适呢?这就和时间事件有关了,Redis 会计算最近一个时间事件的触发时间,最多阻塞到这个时间点。这也很好理解,保证即使没有网络读写,时间事件也能正常处理。usUntilEarliestTimer 函数,就是用来计算这个触发时间的。

processTimeEvents

时间事件执行就简单多了,依次遍历 timeEventHead 链表,如果到了执行时间就触发任务执行。timeProc 执行函数返回下次的执行时间间隔,单位是毫秒;如果不为 -1,则更新该任务的下次执行时间,否则标记任务为删除状态。

static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    monotime now = getMonotonicUs();
    while(te) {
        long long id;

        // 删除废弃的任务
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->refcount) {
                te = next;
                continue;
            }
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc) {
                te->finalizerProc(eventLoop, te->clientData);
                now = getMonotonicUs();
            }
            zfree(te);
            te = next;
            continue;
        }

        if (te->id > maxId) {
            te = te->next;
            continue;
        }

		// 核心:如果定时任务过期,则调用timeProc方法执行任务。
		//       如果该方法返回值不为AE_NOMORE=-1,则设置下一次执行时间
        if (te->when <= now) {
            int retval;

            id = te->id;
            te->refcount++;
            retval = te->timeProc(eventLoop, id, te->clientData);
            te->refcount--;
            processed++;
            now = getMonotonicUs();
            if (retval != AE_NOMORE) {
                te->when = now + retval * 1000;
            } else {
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}

epoll管理器

Reids 统一封装了 epoll API,提供了 IO 读写事件的注册及轮询功能。

  • aeApiAddEvent:网络读写事件注册。
  • aeApiDelEvent:网络读写事件取消注册。
  • aeApiPoll:轮询已触发的读写事件。
  • aeApiCreate:创建 epoll 管理器。

polling API 定义在 ae_epoll.c、 ae_select.c、 ae_kqueue.c、 ae_evport.c 文件中,分别兼容不同的平台。以 ae_epoll.c 为例,最终都是调用最底层的 epoll_create(创建epoll)、epoll_ctl(注册感兴趣的事件)、epoll_wait(阻塞等待)方法。

aeApiAddEvent

// 注册读或写事件到 epoll 管理器
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0};
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

aeApiPoll

// 轮询触发的文件事件,保存到 fired 数组
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);
    if (retval > 0) {
        int j;

        numevents = retval;
        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|AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

总结

今天,我重点分析了 Reids 事件驱动原理,我们复习一下。

AE 库提供了一个轻量级的事件驱动库,提供了文件事件和时间事件的处理逻辑。文件事件就是 NIO 网络读写事件,时间事件则是内部的定时任务等。AE 库可以分为两步部分,一是 aeEventLoop 事件处理器;二是 epoll 管理器。

  • aeEventLoop 事件处理器:定义在 ae.c 文件中,保存了文件事件和时间事件。最核心的逻辑是,不断调用 epoll 管理器,轮询已经触发的事件,并依次处理读事件和写事件。
  • epoll 管理器:统一封装了 polling API,提供创建 epoll、注册感兴趣的事件、取消事件注册,及已触发的事件轮询等方法。

最后,我用一张思维导图来帮助你理解和记忆事件驱动的逻辑:

图3 AE库时间驱动总结

参考

https://zhuanlan.zhihu.com/p/517974884?utm_id=0


每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2024-04-07 21:57  binarylei  阅读(81)  评论(0编辑  收藏  举报

导航