【源码位置】:redis中关于事件循环的API位于ae.h及ae.c文件中

一.文件事件

1.什么是文件事件

redis中将套接字和管道产生的通信事件称为文件事件,并使用事件回调处理这些文件事件,如可读回调,可写回调,连接成功回调等等。

2.文件事件结构

redish中每个文件事件都以一个aeFileEvent结构表示,其结构如下所示:

typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);

/* File event structure */
typedef struct aeFileEvent {
    int mask;              //标记为读或者写事件,也用于标识该事件结构体是否正在使用
    aeFileProc *rfileProc; // 指向读回调函数
    aeFileProc *wfileProc; // 指向写回调函数
    void *clientData;      // 指向读写回调函数所需要用到的结构
} aeFileEvent;

【注】:可以发现文件事件中并未保存其对应的文件描述符,那么是如何对应的呢?其实redis中将文件描述符与事件循环中文件事件数组下标进行了一 一对应。

3.文件事件的创建

/**
* @功能 根据文件描述符等信息创建文件事件,并添加到指定的事件循环中去
* @参数
* eventLoop:指向事件循环的指针,新创建的文件事件将添加到该事件循环中
* fd:		 文件描述符
* mask:		 要关注事件的掩码(读写)
* proc:		 该文件事件使用的回调函数指针
* clientData:回调函数中所要使用的数据
*/
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) { // 若注册的文件描述符超过了最大值则返回错误
        errno = ERANGE;
        return AE_ERR;
    }

    // 将文件描述符对应到事件循环中保存的文件事件数组下标,并获取对应文件事件地址,以便填充。
    // 即redis中文件描述符与数组下标是一一对应的,所以上面才要检查文件描述符的大小
    aeFileEvent *fe = &eventLoop->events[fd]; 

    // 根据选用的IO复用API的不同而填充不同的数据结构,如若选用的是select则会填充fd_set结构
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;	// 填充关注的事件
    if (mask & AE_READABLE) fe->rfileProc = proc; // 读回调
    if (mask & AE_WRITABLE) fe->wfileProc = proc; // 写回调
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd; // 更新当前最大文件描述符
    return AE_OK;
}

4.文件事件的解注册

/**
* @功能:注销文件事件的某些关注事件(读或写)
* @参数:
* eventLoop: 文件事件所属的事件循环
* fd:		 文件描述符
* mask:      要取消关注的事件的掩码
*/
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{
    if (fd >= eventLoop->setsize) return;

	// 指向文件描述符fd所对应的文件事件
    aeFileEvent *fe = &eventLoop->events[fd];

	// 若该文件事件已无关注事件,则直接返回
    if (fe->mask == AE_NONE) return;

	// 根据选用的IO复用函数不同,而使用不同方法去除再IO/复用函数上注册的事件
    aeApiDelEvent(eventLoop, fd, mask);
    fe->mask = fe->mask & (~mask); // 取消对指定事件的关注(读或写)

	// 若在取消关注指定事件后该文件事件无其它关注事件,则需更新最大描述符记录
    if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
        /* Update the max fd */
        int j;

        for (j = eventLoop->maxfd-1; j >= 0; j--)
            if (eventLoop->events[j].mask != AE_NONE) break;
        eventLoop->maxfd = j;
    }
}

5.获取某文件事件的关注事件

// 获取在某个文件描述符上关注的事件
int aeGetFileEvents(aeEventLoop *eventLoop, int fd) {
    if (fd >= eventLoop->setsize) return 0;
    aeFileEvent *fe = &eventLoop->events[fd]; // 获取该文件描述符相关联的文件事件

    return fe->mask; // 返回其关注的事件
}

 

二.时间事件

1.什么是时间事件

时间事件定时事件,即在指定事件点执行,根据某个时间事件是否是周期性的又可分为周期性时间事件与定时事件。redis中的时间事件并未像muduo那样进行过排序,而是使用的无序链表,因此不宜有过多的时间事件,否则效率会很低。

2.时间事件结构

redis中每一个时间事件用一个aeTimeEvent表示,其结构如下:

 

/* Time event structure */
typedef struct aeTimeEvent {
    long long id;     // 时间事件ID,这样可以防止两个相同结束时间的事件冲突
    long when_sec;    // 到期时间点的秒数
    long when_ms;     // 到期时间点的毫秒数
    aeTimeProc *timeProc; // 时间事件回调函数
    aeEventFinalizerProc *finalizerProc; //时间事件的最后一次处理程序,若已设置,则删除时间事件时会被调用
    void *clientData; // 指向时间事件的回调函数会用到的结构
    struct aeTimeEvent *next; // 指向下一个时间事件
} aeTimeEvent;

 

3.时间事件的创建

从时间事件的创建中我们可以看到,redis使用无序链表来维护所有的时间事件,对于新的时间事件采用头插法链入。

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    long long id = eventLoop->timeEventNextId++; // 获取用于创建时间事件的Id
    aeTimeEvent *te;

    // 开辟空间并进行初始化
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    te->id = id;
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms); // 设置时间事件的到期时间
    te->timeProc = proc;	// 设置时间事件的回调函数
    te->finalizerProc = finalizerProc; // 设置时间事件删除前的回调函数
    te->clientData = clientData;

    // 将新的时间事件连接到循环的时间事件链表的首部
    te->next = eventLoop->timeEventHead; 
    eventLoop->timeEventHead = te;
    return id;
}

 

4.时间事件的解注册

int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
    aeTimeEvent *te, *prev = NULL;

    te = eventLoop->timeEventHead; // 指向本循环中时间事件的链表
    // 通过id查找时间事件,并将其从时间事件链表断开
    while(te) {
        if (te->id == id) {
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
            if (te->finalizerProc) // 调用解注册回调函数
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            return AE_OK;
        }
        prev = te;
        te = te->next;
    }
    return AE_ERR; /* NO event with the specified ID found */
}

5.搜索最近到达的时间事件

// 遍历整个时间事件链表,找到最近到达的时间事件,时间复杂度为(N)
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
    aeTimeEvent *te = eventLoop->timeEventHead;
    aeTimeEvent *nearest = NULL;

    while(te) {
        if (!nearest || te->when_sec < nearest->when_sec ||
                (te->when_sec == nearest->when_sec &&
                 te->when_ms < nearest->when_ms))
            nearest = te;
        te = te->next;
    }
    return nearest;
}

 

三.事件循环

1.什么是事件循环(eventLoop)

所谓事件循环,实际上是使用I/O复用,事件队列及事件回调来实现的,当出现某个I/O事件或定时事件时醒来,并调用相应的事先注册的回调函数。

2.事件循环结构

在redis中每一个事件循环由一个aeEventLoop结构描述,其结构如下所示:

typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop);

/* State of an event based program */
typedef struct aeEventLoop {
    int maxfd;   // 当前注册的最大文件描述符(select会用到)
    int setsize; // 能注册的文件描述符的最大值
    long long timeEventNextId;	// 下一个时间事件将使用的id
    time_t lastTime;            //用于检测系统时间是否变更(判断标准 now<lastTime)
    aeFileEvent *events;        // 文件事件数组,每个数组下标便是该文件事件对应的文件描述符
    aeFiredEvent *fired;        // 已触发的事件(即将要被处理的事件)
    aeTimeEvent *timeEventHead; // 时间事件数组
    int stop;
    void *apidata; // 根据使用的I/O复用API不同而指向不同的数据结构,每种数据结构中都保存了对应API所要使用的数据
    aeBeforeSleepProc *beforesleep; // 每次进入事件循环前调用一次
} aeEventLoop;

3.事件循环的主循环体

// 事件循环的主循环体
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) { // 若事件循环未停止,则继续
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop); // 调用循环前回调
        aeProcessEvents(eventLoop, AE_ALL_EVENTS); // 进入本次事件循环
    }
}

4.处理循环中的事件

 redis中通过函数aeProcessEvents来决定如何处理文件事件和时间事件,该函数有阻塞与非阻塞两种执行方式。当为阻塞处理时,redis会将I/O复用函数的超时时间设置为最近的一个时间事件的到期事件,这样aeProcessEvents函数将阻塞至最近的时间事件到达或有其它事件到达(如socket可读)。当设置了非阻塞处理,则将I/O复用函数的超时时间设置为0,即不进行任何等待。

/** 
 * @功能:该函数用于处理文件事件与时间事件
 * @参数:
 *  eventLoop:要处理的循环
 *	flags:该标志用于说明处理哪些事件,以及是否非阻塞
 *	- flags == 0 : 不处理任何事件,直接return
 *  - flags has AE_FILE_EVENTS: 处理文件事件,即需要调用I/O复用函数
 *  - flags has AE_TIME_EVENTS: 处理时间事件
 *  - flags has AE_DONT_WAIT: 非阻塞的执行各个事件,即调用I/O复用时超时时间为0,
 *                            否则将被设置为最近达到的时间事件的达到时间,或一直阻塞
 */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
     /**
     * 若存在监听的文件事件,或者 要处理时间事件且未设置非阻塞
     * 此时若时间事件还未到达,那么将在之后的I/O复用函数上使用
     * 最近的时间事件的到达时间做为超时时间,即之后的I/O复用将
     * 等待最近的时间事件到达,或有其它事件产生。
     */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;
		
        // 若要处理时间事件,且未设置非阻塞,则取出到达时间离当前时间最近的时间事件
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            long now_sec, now_ms;

            // 取得当前时间,并计算最近时间时间距离到达还有多少毫秒
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;

            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            // 如果最近的时间事件已到达,那么结果应该<0,此时将其设置为0
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            /** 当监听了文件事件却没有时间事件时会进入该else,此时分为以下两种情况处理:
            *   - 若设置了非阻塞,那么之后的I/O复用函数的超时时间为0。
            *   - 若未设置了非阻塞,那么之后的I/O复用函数将被一直阻塞,直至有事件发生(如某套接字可读)
            */
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                tvp = NULL; /* wait forever */
            }
        }

        // 根据设置不同而调用不同的I/O复用函数(如select, poll,epoll等),
        // 并将发生的文件事件添加到循环的已触发事件数组中,即eventLoop->fired
        // eventLoop为事件循环,其中包括了I/O复用函数要使用的数据
        // 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;

            if (fe->mask & mask & AE_READABLE) {
                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++;
        }
    }

    // 处理时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

具体处理时间事件的函数如下所示:

/*处理指定循环中的时间事件*/
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);

    /** 若上次处理时间事件的时间大于当前时间,那么说明系统时间被调整过
     *  此时应该尽快处理完所有现有的时间事件,即将现有时间事件的超时时间
     *  设置为0。
     */
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }

    // 更新最后处理时间事件的时间
    eventLoop->lastTime = now;

    // 遍历时间事件链表,并调用到期时间事件的回调函数
    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    while(te) {
        long now_sec, now_ms;
        long long id;

        /*	在一个时间事件处理完后,链表的结构可能会被更改,可能会增加新的时间事件,
         *	对于这些新的时间事件不在本次循环中处理
         */
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        aeGetTime(&now_sec, &now_ms);
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;

            id = te->id;
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            /* After an event is processed our time event list may
             * no longer be the same, so we restart from head.
             * Still we make sure to don't process events registered
             * by event handlers itself in order to don't loop forever.
             * To do so we saved the max ID we want to handle.
             *
             * FUTURE OPTIMIZATIONS:
             * Note that this is NOT great algorithmically. Redis uses
             * a single time event so it's not a problem but the right
             * way to do this is to add the new elements on head, and
             * to flag deleted elements in a special way for later
             * deletion (putting references to the nodes to delete into
             * another linked list). */
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                aeDeleteTimeEvent(eventLoop, id);
            }
            te = eventLoop->timeEventHead;
        } else {
            te = te->next;
        }
    }
    return processed; // 返回此次处理的时间事件的数量
}

 

四.Redis中初始化事件循环

1.redis中事件循环的初始化

Redis在initServer()函数中完成对Redis服务器的初始化,包括客户端列表,数据库初始化等等,当然其中就包括创建一个事件循环,启动监听套接字并为其关联文件事件(监听套件字的回调函数为acceptTcpHandler),还将serverCron函数添加到了时间事件链表中。函数具体如下所示:

// 创建一个事件循环,该循环内文件事件的最大量为setsize,即能支持的文件描述符数量最多为setsize
// 因为每个文件描述符对应一个文件事件
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    int i;

    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
    eventLoop->setsize = setsize;
    eventLoop->lastTime = time(NULL);
    eventLoop->timeEventHead = NULL;
    eventLoop->timeEventNextId = 0;
    eventLoop->stop = 0;
    eventLoop->maxfd = -1;
    eventLoop->beforesleep = NULL;
    if (aeApiCreate(eventLoop) == -1) goto err;
    /* Events with mask == AE_NONE are not set. So let's initialize the
     * vector with it. */
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;
    return eventLoop;

err:
    if (eventLoop) {
        zfree(eventLoop->events);
        zfree(eventLoop->fired);
        zfree(eventLoop);
    }
    return NULL;
}

void initServer(void) {

    server.pid = getpid();
    server.current_client = NULL;
    server.clients = listCreate();
    server.clients_to_close = listCreate();
    server.slaves = listCreate();
    server.monitors = listCreate();
    
    ...

    // server.maxclients为最多能同时服务的客户端数量
    // REDIS_EVENTLOOP_FDSET_INCR是用于与客户端连接的起始文件描述符(之前的做为保留位,比如前32位用于哨兵系统)
    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);


     /* Open the TCP listening socket for the user commands. */
    // 创建监听套接字
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
        exit(1);

    ...

    // 创建时间事件,回调函数为serverCron
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }
    
    // 为每个监听建立文件事件,并添加到事件循环中
    // 监听套接字的连接到达回调函数为acceptTcpHandler
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }

    ...
}

 

2.监听套接字的回调函数

在initServer中为每个监听套接字注册了读事件。当连接到达时,监听套接字会变为可读状态,造成I/O复用函数返回,转而调用读回调函数acceptTcpHandler。该函数会调用accept接受一个连接,并为新连接的socket文件描述符关联一个文件事件,具体函数如下:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    // 由于该函数内未使用以下几个变量,为了避免产生警告而使用了REDIS_NOTUSED宏,#define REDIS_NOTUSED(V) ((void) V)
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);

    // 每次调用最多接受的连接数为max(1000)
    while(max--) {
        // 调用anetTcpAccept接受连接,并返回该连接的文件描述符。即一个对accept的封装函数
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        // 成功接受连接后为该客户端建立一个对应的redisClient结构,并为其关联一个文件事件(读事件)
        acceptCommonHandler(cfd,0);
    }
}

//  用于成功接受连接后为该客户端建立一个对应的redisClient结构,并为其关联一个文件事件(读事件)
static void acceptCommonHandler(int fd, int flags) {
    redisClient *c;
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
    /* If maxclient directive is set and this is one client more... close the
     * connection. Note that we create the client instead to check before
     * for this condition, since now the socket is already set in non-blocking
     * mode and we can send an error for free using the Kernel I/O */
    if (listLength(server.clients) > server.maxclients) {
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }
    server.stat_numconnections++;
    c->flags |= flags;
}

// 为文件描述符fd创建客户端结构,并关联文件事件(可读事件)
redisClient *createClient(int fd) {
    redisClient *c = zmalloc(sizeof(redisClient));

    /* passing -1 as fd it is possible to create a non connected client.
     * This is useful since all the Redis commands needs to be executed
     * in the context of a client. When commands are executed in other
     * contexts (for instance a Lua script) we need a non connected client. */
    if (fd != -1) {
        anetNonBlock(NULL,fd);
        anetEnableTcpNoDelay(NULL,fd);
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);

        // 客户端套接字的读回调函数为readQueryFromClient
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }

    selectDb(c,0);
    c->id = server.next_client_id++;
    c->fd = fd;
    c->name = NULL;
    c->bufpos = 0;
    c->querybuf = sdsempty();
    c->querybuf_peak = 0;
    c->reqtype = 0;
    c->argc = 0;
    c->argv = NULL;
    ...

    return c;
}

 

3.普通客户端的读回调

redis中为每个客户端连接都对应了一个redisClient,并为其关联了文件事件,我们可以在上一小节介绍的createClient函数中看到,客户端文件描述符关联的读事件回调处理函数为readQueryFromClient函数,该函数会将客户端发送的请求字符串转化为相应的命令。

 

4.写回调

写事件并不会一直被注册,因为在电平触发时这会造成一直产生可写事件,因此只有redis服务器需要向客户端发送数据时才会注册写事件,当写完后再解注册写事件,redis中写回调函数为networking.c/sendReplyToClient函数