Redis事件驱动框架

通信模型的选择

Redis作为一个Client-Server架构的数据库,其源码中不少都是用来实现网络通信的部分。最常见的网络通信方式是使用Socket编程模型(Socket套接字编程可参考文末链接1),包括创建Socket、监听端口、处理连接请求和读写请求。但Socket编程模型一次只能处理一个客户端的请求,所以当要处理高并发请求时,一种方案就是使用多线程,让每个线程负责处理一个客户端的请求。而Redis负责客户端请求解析和处理的线程只有一个,如果直接使用Socket模型则会影响Redis支持高并发的客户端访问。因此Redis选择epoll模型进行网络通信。

为什么不使用Socket编程模型?

socket模型实现网络通信有下面三个步骤

  1. 创建一个套接字(调用socket函数),通常把这个套接字称为主动套接字(Active Socket)。
  2. 绑定IP和端口(调用bind函数),将主动套接字和当前服务器的IP和监听端口进行绑定。
  3. 监听套接字(调用listen函数),将主动套接字转换为监听套接字,开始监听客户端的连接。 

完成上面三步后服务器端就可以收到客户端的连接请求。

服务器端的简易代码逻辑如下,

// 调用socket函数创建一个主动套接字
listenSocket = socket();
// 绑定IP和端口
bind(listenSocket);
// 主动套接字转换为被动套接字
listen(listenSocket);
// 循环监听是否有客户端连接请求
while(1){
    // 接受客户端连接
    connSocket = accept(listenSocket);
    // 从客户端读取数据,只能处理一个客户端
    recv(connSocket);
    // 给客户端返回数据
    send(connSocket);
}

从代码可以看出程序监听到客户端请求时,只能处理一个客户端请求,如果想要处理多个客户端请求,那么就需要使用多线程来进行处理并返回客户端请求。简易代码如下,

// 调用socket函数创建一个主动套接字
listenSocket = socket();
// 绑定IP和端口
bind(listenSocket);
// 主动套接字转换为被动套接字
listen(listenSocket);
// 循环监听是否有客户端连接请求
while(1){
    // 接受客户端连接
    connSocket = accept(listenSocket);
    // 创建新线程对已连接套接字进行处理
    pthred_create(processData, connSocket);    
}


// 处理已连接套接字上的读写请求
processData(connSocket){
    // 从客户端读取数据,只能处理一个客户端
    recv(connSocket);
    // 给客户端返回数据
    send(connSocket);
}

但该方法虽然能提升服务器端的并发处理能力,但并不适用Redis。Redis的主执行流程是单线程执行。epoll模型使用了IO多路复用机制,在单线程的条件下可以进行多个任务处理。

Linux操作系统中的select、poll和epoll编程模型

多路复用机制

在信号传输中,一个信道同时传输多路信号,这就是所谓的多路复用技术(Multiplexing),常见的有频分复用、时分复用、波分复用、码分复用等。IO多路复用则是多个客户端请求在请求到服务器端时,采用了类似的复用技术,如下图,

select模型

Linux针对每一个套接字都会有一个文件描述符,也就是一个非负整数,用来唯一标识该套接字。所以在IO多路复用机制的函数中,Linux通常就会用文件描述符作为参数。有了文件描述符,select函数就能找到对应的套接字,从而进行监听、读写等操作。select函数对每一个描述符集合,都可以监听1024个描述符。

select函数实现网络通信的基本流程如下图,

 select函数的简易代码如下,

// 监听套接字和已连接套接字变量
int sock_fd,conn_fd;
// 创建套接字
sock_fd = socket();
// 绑定套接字
bind(sock_fd);
// 监听套接字
listen(sock_fd);
// 被监听的描述符集合
fd_set rset;

int max_fd = sock_fd;
// 初始化rset数组,使用FD_ZERO宏设置每个元素为0
FD_ZERO(&rset)
// 使用FD_SET宏设置rset数组中位置为sock_fd的文件描述符为1,表示需要监听该文件描述符
FD_SET(sock_fd, &rset)
// 设置超时事件
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;

while(1){
    // 调用select函数,检查rset数组保存的文件描述符是否已有读事件就绪,返回就绪的文件描述符个数
    n = select(max_fd+1, &rset, NULL, NULL, &timeout);
    // 调用FD_ISSET宏,在rset数组中检测sock_fd对应的文件描述符是否就绪
    if(FD_ISSET(sock_fd, &rset)){
        // 如果sock_fd已经就绪,表明已有客户端连接;调用accept函数建立连接
        conn_fd = accept();
        // 设置rset数组中位置为conn_fd的文件描述符为1,表示需要监听该文件描述符
        FD_SET(conn_fd, &rset);
    }
    
    // 依次检查已连接套接字的文件描述符
    for(i=0;i<max_fd;i++){
        if(FD_ISSET(i, &rset)){
            //有数据可读,则进行读数据处理
            ... ...
        }
    }
}

从上可以看出使用select函数进行监听文件描述符的缺点在于监听数量有限,默认能监听的文件描述符是1024,即使修改了默认值,在大并发的情况下还是有数量限制。另外select函数返回后,最后还需要遍历所有描述符集合才能知道哪些文件描述符就绪。

poll模型

poll函数完成网络通信的主要流程如下:

  1. 创建pollfd数组和监听套接字,并进行绑定;
  2. 将监听套接字加入pollfd数组,并设置其监听读事件,也就是客户读的连接请求;
  3. 循环调用poll函数,检测pollfd数组中是否有就绪的文件描述符。

 主要的简易代码逻辑如下,

// 监听套接字和已连接套接字变量
int sock_fd,conn_fd;
// 创建套接字
sock_fd = socket();
// 绑定套接字
bind(sock_fd);
// 监听套接字
listen(sock_fd);

// poll函数可以监听的文件描述符数量,可以大于1024
#define MAX_OPEN = 2048

// pollfd结构体数组,对应文件描述符
struct pollfd client[MAX_OPEN];

// 将创建的监听套接字加入pollfd数组,并监听其可读事件
client[0].fd = sock_fd;
client[0].events = POLLRDNORM;
maxfd = 0;

//初始化client数组其他元素为-1
for(i = 1;i < MAX_OPEN; i++){
    client[i].fd = -1;
}

while(1){
    // 调用poll函数,检测client数组里的文件描述符是否有就绪的,返回就绪的文件描述符个数
    n = poll(client, maxfd+1, &timeout);
    // 如果监听套接字的文件描述符有可读事件,则进行处理
    if(client[0].revents & POLLRDNORM){
        conn_fd = accept();
        
        // 保存已建立连接套接字
        for(i = 1;i< MAX_OEPN;i++){
            if(client[i].fd < 0){
                client[i].fd = conn_fd;
                client[i].events = POLLRDNORM;
                break;
            }
        }
        maxfd = i;
    }
        
    // 依次检查已连接套接字的文件描述符
    for(i=0;i<MAX_OPEN;i++){
        if(client[i].revents & (POLLRDNORM | POLLERR)){
            //有数据可读,则进行读数据处理
            ... ...
        }
    }
}

与select函数相比,poll函数的改进之处主要就在它允许一次监听超过1024个文件描述符。但是调用poll函数后,仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。

epoll模型

epoll机制使用epoll_event结构体来记录待监听的文件描述符和监听事件类型。常见的事件类型有下面几种:

  • EPOLLIN:读事件,表示文件描述符对应套接字有数据可读;
  • EPOLLOUT:写事件,表示文件描述符对应套接字有数据要写;
  • EPOLLERR:错误事件,表示文件描述符对应套接字出错。

epoll_data联合体以及epoll_event结构体如下,

typedef union epoll_data
{
    ...
    int fd;
    ...
} epoll_data_t;

struct epoll_event
{
    // epoll监听的事件类型
    uint32_t events;
    // 应用程序数据
    epoll_data_t data;
}

select和poll函数在创建好文件描述符集合或pollfd数组后,就可以往数组中添加需要监听的文件描述符了。而对于epoll首先需要调用epoll_create函数,创建一个epoll实例。这个实例里维护了待监听的文件描述符和已就绪的文件描述符。这样就不需要在后面进行遍历查询哪些是已经就绪的文件描述符了。epoll具体的进行网络通信的流程如下,

epoll简易代码流程如下,

// 监听套接字和已连接套接字变量
int sock_fd,conn_fd;
// 创建套接字
sock_fd = socket();
// 绑定套接字
bind(sock_fd);
// 监听套接字
listen(sock_fd);

// 创建epoll实例对象
epfd = epoll_create(EPOLL_SIZE);
// 创建epoll_event结构体数组,保存套接字对应文件
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE);

// 创建epoll_event变量
struct epoll_event ee
// 监听读事件
ee.events = EPOLLIN;
// 监听的文件描述符是刚创建的监听套接字
ee.data.fd = sock_fd;
// 将监听套接字加入到监听列表中
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ee);

while(1){
    n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    for(i=0; i<n; i++) {
        // 如果是监听套接字描述符就绪,表明有一个新客户端连接到来
        if(ep_events[i].data.fd == sock_fd){
            conn_fd = accept(sock_fd);
            ee.events = EPOLLIN;
            ee.data.fd = conn_fd;
            // 添加对新创建的已连接套接字描述符的监听
            epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &&ee);
        } else {
            // 读取数据并处理
            ...    
        }
    }
}

这样Redis在实现网络通信框架时,基于epoll机制中的epoll_create、epoll_ctl和epoll_wait等函数和读写事件,实现了网络通信的事件驱动框架,从而使得Redis虽然是单线程运行,但仍然能高效应对高并发的客户端访问。

小结

Linux三种IO多路复用机制的差异点可以用下面这张图来表示,

Redis源码进阶

从上面了解了事件驱动框架的基础,那么下面来看看Redis源码中的实现,

找到ae_select.c文件,它就使用了select机制实现IO多路复用,

前面定义了fd_set 结构的读事件集合和写事件集合,_表示已就绪的文件描述符集合。

先看看fd_set的数据结构,

// long int类型的数组,数组一共32个元素(1024/32=32),每个元素是32位(long int类型的大小),每一位可以用来表示一个文件描述符的状态
typedef struct{
    ...
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    ...
} fd_set
#include <sys/select.h>
#include <string.h>

typedef struct aeApiState {
    fd_set rfds, wfds;
    /* We need to have a copy of the fd sets as it's not safe to reuse
     * FD sets after select(). */
    fd_set _rfds, _wfds;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    FD_ZERO(&state->rfds);
    FD_ZERO(&state->wfds);
    eventLoop->apidata = state;
    return 0;
}

static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
    /* Just ensure we have enough room in the fd_set type. */
    if (setsize >= FD_SETSIZE) return -1;
    return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
    zfree(eventLoop->apidata);
}

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

    if (mask & AE_READABLE) FD_SET(fd,&state->rfds);
    if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);
    return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;

    if (mask & AE_READABLE) FD_CLR(fd,&state->rfds);
    if (mask & AE_WRITABLE) FD_CLR(fd,&state->wfds);
}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, j, numevents = 0;

    memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
    memcpy(&state->_wfds,&state->wfds,sizeof(fd_set));

    retval = select(eventLoop->maxfd+1,
                &state->_rfds,&state->_wfds,NULL,tvp);
    if (retval > 0) {
        for (j = 0; j <= eventLoop->maxfd; j++) {
            int mask = 0;
            aeFileEvent *fe = &eventLoop->events[j];

            if (fe->mask == AE_NONE) continue;
            if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
                mask |= AE_READABLE;
            if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
                mask |= AE_WRITABLE;
            eventLoop->fired[numevents].fd = j;
            eventLoop->fired[numevents].mask = mask;
            numevents++;
        }
    }
    return numevents;
}

static char *aeApiName(void) {
    return "select";
}

再看ae_epoll.c文件,它使用了epoll机制实现IO多路复用,

#include <sys/epoll.h>

typedef struct aeApiState {
    int epfd;
    struct epoll_event *events;
} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    eventLoop->apidata = state;
    return 0;
}

static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
    aeApiState *state = eventLoop->apidata;

    state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);
    return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
    aeApiState *state = eventLoop->apidata;

    close(state->epfd);
    zfree(state->events);
    zfree(state);
}

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    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.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    int mask = eventLoop->events[fd].mask & (~delmask);

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (mask != AE_NONE) {
        epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
    } else {
        /* Note, Kernel < 2.6.9 requires a non null event pointer even for
         * EPOLL_CTL_DEL. */
        epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
    }
}

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

static char *aeApiName(void) {
    return "epoll";
}

Reactor模型的工作机制

Reactor模型是网络服务器端用来处理高并发网络IO请求的一种编程模型。

它基于三类事件和三个角色来处理高并发请求。

三类事件分别是连接事件、写事件、读事件;

三个角色分别是reactor、acceptor、handler。

事件类型与关键角色

如果了解过RBAC(Role-Based Access Control)基于角色的访问控制模型的人应该知道用户、角色、权限三者之间的关系。类似的,我们先来看看这三类事件和Reactor模型的关系。

Reactor模型处理的是客户端和服务器端交互过程中,不同类请求在服务器端引发的待处理事件。

  • 连接事件:当一个客户端要和服务端进行交互时,客户端向服务器端发送连接请求,以建立连接,这里对应的服务器端的一个连接事件;
  • 写事件:连接建立后,客户端会给服务器端发送读请求,以便读取数据。服务器端在处理读请求时,需要向客户端写回数据,这里对应的服务器端的一个写事件;
  • 读事件:无论客户端给服务器端发送读或写请求,服务器端都需要从客户端读取请求内容,这里对应的服务器端的一个读事件。

 这三类事件和三个角色的关系如下,

首先,连接事件由acceptor来处理,负责接收连接;

acceptor在接收连接后,会创建handler,用于网络连接上对后续读写事件的处理;

其次,读写事件由handler处理;

最后,在高并发场景中,连接事件、读写事件会同事发生,所以,我们需要有一个角色专门监听和分配事件,这个就是reactor角色。当有连接请求时,reactor将产生的连接事件交由acceptor处理;当有读写请求时,reactor将读写事件交由handler处理。

它们和事件的关系如下图,

事件驱动框架

事件驱动框架包括两部分:

  1. 事件初始化;
  2. 事件捕获、分发和处理主循环。

事件初始化是在服务器程序启动时就执行的,它的作用主要是创建需要监听的事件类型,以及该类事件对应的handler。而一旦服务器完成初始化后,事件初始化也就相应完成了,服务器程序就需要进入到事件捕获、分发和处理的主循环中。 

Reactor模式也叫反应堆模式,它是一种事件驱动机制,逆转了事件处理的流程,不再是主动地等事件就绪,而是提前注册好回调函数,当有对应事件发生就调用回调函数。在while循环中,我们需要捕获发生的事件、判断事件类型,并根据事件类型调用初始化创建好的事件的回调函数来处理事件。主要的处理流程如下,

如上图所示,Reactor模型的基本工作机制就是,客户端的不同类型的请求会在服务器端出发连接、读、写三类事件,这三类事件的监听、分发和处理又是由reactor、acceptor、handler三类角色来完成的,然后这三类角色会通过事件驱动框架来实现交互和事件处理。

Redis对Reactor模型的实现

Redis的网络框架实现了Reactor模型,并且自行开发实现了一个事件驱动框架(对应的文件为ae.c)。在ae.h头文件中Redis为了实现事件驱动框架,相应地定义了事件的数据结构框架主循环函数事件捕获分发函数、事件和handler注册函数。下面依次来看一下,

ae.h中事件的数据结构定义

事件的数据结构是关联事件类型和事件处理函数的关键要素。而Redis的事件驱动框架定义了两类事件——IO事件和时间事件,分别对应了客户端发送的网络请求和Redis自身的周期性操作。

如下,IO事件aeFileEvent中,

mask:用来表示事件类型的掩码。对于网络通信的事件来说,主要有AE_READABLE、AE_WRITEABLE和AE_BARRIER三类事件。框架在分发事件时,依赖的就是结构体中的事件类型。

refileProc和wfileProc:分别指向AE_REABLE和AE_WRITEABLE这两类事件的处理函数,也就是Reactor模型中的handler。框架在分发事件后,就需要调用结构体中定义的函数进行事件处理。

clientData:用来指向客户端私有数据的指针。

/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

事件事件aeTimeEvent如下,

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *prev;
    struct aeTimeEvent *next;
} aeTimeEvent;

除了事件的数据结构以外,在头文件中还定义了支撑框架运行的主要函数,有负责事件和handler注册的aeCreateFileEvent函数,有负责事件捕获和分发的aeProcessEvents函数,有框架主循环的aeMain函数,头文件中的原型定义如下,

/* Prototypes */
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData);
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
void aeMain(aeEventLoop *eventLoop);

ae.c中框架主循环函数

找到对应的循环主函数实现如下,循环判断停止标记,如果循环标记设置为true,那么针对事件捕获、分发和处理的整个主循环就停止了。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

 那么主循环函数是在哪呗调用的呢?找到server.c文件,在Redis服务器初始化时调用了aeMain函数开始执行事件驱动框架,如下,

int main(int argc, char **argv) {
    ... ...
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;
}

ae.c中事件捕获分发函数

找到aeProcessEvents函数,有三个if判断,分别对应了三种情况,

  • 情况一:既没有时间事件,也没有网络事件;
  • 情况二:有IO事件或者需要紧急处理的时间事件;
  • 情况三:只有普通的时间事件。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    // 若没有事件处理,则立刻返回
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    // 如果有IO事件发生,或者紧急的时间事件发生,则开始处理
    if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        ... ...
        /* Call the multiplexing API, will return only on timeout or when
         * some event fires. */
        numevents = aeApiPoll(eventLoop, tvp);
        ... ...
    }
    // 检查是否有时间事件,若有,则调用processTimeEvents函数处理
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    // 返回已经处理的IO事件或时间事件
    return processed; /* return the number of processed file/time events */
}

在情况二时,Redis需要捕获发生的网络事件并进行相应的处理,在代码中是调用aeApiPoll函数来捕获事件。Redis依赖操作系统底层提供的IO多路复用机制来实现捕获,检查是否有新的连接、读写事件发生。为了适配不同的操作系统,Redis对不同操作系统实现的网络IO多路复用函数都进行了统一封装,封装的代码通过下面四个文件中的函数实现,

  • ae_epoll.c:对应Linux操作系统上的IO复用函数epoll;
  • ae_evport.c:对应Solaris操作系统上的IO复用函数evport;
  • ae_kqueue.c:对应macOS或FreeBSD操作系统上的IO复用函数kqueue;
  • ae_select.c:对应Linux或Windows操作系统上的IO复用函数select。

这样Redis在不同的操作系统上调用IO多路复用API时就可以通过统一的接口来进行调用。比如Linux系统的ae_epoll.c的代码如下,使用epoll_wait API检测内核中发生的网络IO事件,

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    // 调用epoll_wait获取监听到的事件
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/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;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

事件驱动框架对epoll_wait的调用链可以用下图表示,

ae.c中事件注册函数

找到aeCreateFileEvent函数,Linux提供了epoll_ctl API,用于增加新的观察事件,而Redis在此基础上,封装了aeApiAddEvent函数,对epoll_ctl进行调用,

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

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

事件注册函数是Redis启动后,main函数调用initServer函数进行服务器初始化时,initServer函数在初始化过程中调用的,AE_READABLE事件就是客户端的网络连接事件,而对应的处理函数就是接收TCP连接请求。

void initServer(void) {
    /* Open the TCP listening socket for the user commands. */
    ... ...
    /* Open the listening Unix domain socket. */
    ... ...
    /* Create the Redis databases, and initialize other internal state. */
    ... ...

    /* Register a readable event for the pipe used to awake the event loop
     * when a blocked client in a module needs attention. */
    if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
        moduleBlockedClientPipeReadable,NULL) == AE_ERR) {
            serverPanic(
                "Error registering the readable event for the module "
                "blocked clients subsystem.");
    }

    /* Open the AOF file if needed. */
    ... ...

    /* 32 bit instances are limited to 4GB of address space, so if there is
     * no explicit limit in the user provided configuration we set a limit
     * at 3 GB using maxmemory with 'noeviction' policy'. This avoids
     * useless crashes of the Redis instance for out of memory. */
    ... ...
}

具体的说initServer函数的执行过程中,它会根据启动的IP端口个数,为每个IP端口上的网络事件调用aeCreateFileEvent函数,创建对AE_READABLE事件的监听,并且注册AE_READABLE事件的处理handler,也就是acceptTcpHandler函数,过程如下,

 

参考链接:

1、Socket通信详解_四问四不知的博客-CSDN博客_socket通信

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值