redis事件机制

redis并没有采用libevent库作为事件机制的底层实现,而是自己对io多路复用进行了封装,即可以采用select、epoll、evport、kqueue作为底层的实现。redis客户端与服务端进行通信时,redis提供了命令事件(也就是文件事件)。另外redis还提供了定时事件,用于对系统实时性要求进行处理,以及处理用户的业务需求。
先来看下redis事件机制的整体结构, 接下来的每个小结都围绕这个结构进行分析。

一、创建/监听socket
redis服务端在初始化函数initServer中会进行创建socket、绑定socket的操作。将服务端的指定ip或者所有ip都设置为可以监听来自客户端的连接请求。
//创建套接字、绑定套接字
int listenToPort(int port, int *fds, int *count);

参数port指绑定socket的具体端口

fds用于存储所有绑定的socket, 例如:"假设服务端有两个ip:192.168.100.10 , 192.168.100.11,如果对这两个ip都进行绑定,则fds中将存储绑定后的两个套接字fd"

count 用于存储具体有多少个套接字进行了绑定

          创建并绑定socket后、服务端可以在这些socket上监听来自客户端的连接请求。

二、创建事件管理器

redis服务端在初始化函数initServer中,会创建一个事件管理器对象,用于管理命令事件、时间事件。函数aeCreateEventLoop将创建一个事件管理器,setsize参数指的是socket描述符的个数,服务端初始化时将该参数设为最大socket个数。函数将返回一个事件管理器对象。

aeEventLoop *aeCreateEventLoop(int setsize) 
{
    aeEventLoop *eventLoop;
    int i;
    // 创建事件状态结构
    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) 
	goto err;

    <span style="color:#ff0000;">// 创建未就绪事件表、就绪事件表</span>
    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;
<span style="color:#ff0000;">    //将多路复用io与事件管理器关联起来</span>
    if (aeApiCreate(eventLoop) == -1)
		goto err;

    // 初始化监听事件
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;

    // 返回事件循环
    return eventLoop;
err:
   ...
}

1、首先创建一个aeEventLoop对象

2、创建一个未就绪事件表、就绪事件表。events指针指向未就绪事件表、fired指针指向就绪事件表。表的内容在后面添加具体事件时进行初始化。

3、调用aeApiCreate创建一个epoll实例

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

    // 初始化epoll就绪事件表
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);

    // 创建 epoll 实例
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */

    // 事件管理器与epoll关联
    eventLoop->apidata = state;
    return 0;
}
typedef struct aeApiState 
{
    // epoll_event 实例描述符
    int epfd;
    <span style="color:#ff0000;">// 存储epoll就绪事件表</span>
    struct epoll_event *events;
} aeApiState;
aeApiCreate内部将创建一个aeApiState对象,并将对象存储在apidata中,这样aeApiCreate与aeApiState就关联起来了。

对象中epfd存储epoll的标识,events是一个就绪事件数组,当有事件发生时,所有发生的事件都将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。

三、创建命令事件(也就是文件事件)

创建来事件管理器后、接下来就可以把具体的事件插入到事件管理器中。

typedef struct aeFileEvent 
{
    // 监听事件类型掩码,// 值可以是 AE_READABLE 或 AE_WRITABLE ,
    // 或者 AE_READABLE | AE_WRITABLE
    int mask; 
    // 读事件处理器
    aeFileProc *rfileProc;
    // 写事件处理器
    aeFileProc *wfileProc;
    // 多路复用库的私有数据
    void *clientData;
} aeFileEvent;
aeFileEvent是命令事件结构,对于每一个具体的事件,都有读处理函数、写处理函数等。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{
    // 取出文件事件结构
    aeFileEvent *fe = &eventLoop->events[fd];

    // 监听指定 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;
    return AE_OK;
}
创建一个具体命令事件时,参数fd指的是具体的soket套接字,proc指fd产生事件时,具体的处理过程。
根据fd为索引在未就绪表中找到相应的元素,该数组元素就被fd占用。这个过程就相当于把fd插入到未就绪事件表中。接下来填充事件的回调、参数、事件类型等参数。

除了将事件插入到未就绪表中外,还需要把fd对应的事件插入到具体的io复用中,本例为epoll。

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

    //如果 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;
    //将事件加入epoll中
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1)
		return -1;

    return 0;
}
epoll内部使用红黑树维护每一个事件。红黑树中的每一个节点都代表一个fd。当应用层注册一个命令事件时,会将事件fd插入到红黑树中。应用层删除一个事件时,也相应会从红黑树中删除一个事件节点。

因此创建一个事件时、将会发生下面3个操作

1、创建事件并给事件赋值

2、将事件插入到未就绪事件表

3、将事件插入到epoll维护的红黑树中。

四、事件循环

程序将使用一个while死循环,一直维持着服务端的运转。

//事件处理器的主循环
void aeMain(aeEventLoop *eventLoop) 
{
    eventLoop->stop = 0;

    while (!eventLoop->stop) 
    {

        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}
aeProcessEvents将开始进行事件处理。即处理定时事件也处理命令事件。定时事件与命令事件可以同时发生。定时事件将在下一节讲述,本节只讲述命令事件。
// 处理文件事件,阻塞时间由 tvp 决定
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) 
{
	// 从已就绪数组中获取事件
<span style="white-space:pre">	</span>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 确保读/写事件只能执行其中一个
	      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);
	}
}
aeApiPoll调用时将阻塞,直到有事件发生,或者超时,该函数才返回。函数返回时,已就绪表中存储了所有就绪事件。

遍历所有已经发生的事件,根据fd在未就绪表中找到相应的事件,然后调用事件处理函数。

当一轮事件执行完后,程序又进入最外层的事件循环中,接着处理剩于的事件。

aeApiPoll内部调用epoll系统调用,等待指定事件发生或者超时。如果有事件发生则eploo_wait返回非0,如果为超时,则返回0。

当有事件发生时,内核会把所有发生的事件由内核层拷贝到应用层。eploo_wait返回时state->events就是保存是由内核返回的就绪表。


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;

        // 为已就绪事件设置相应的模式
        // 并加入到 eventLoop 的 fired 数组中
        numevents = retval;
        for (j = 0; j < numevents; j++) 
	{
            int mask = 0;
            struct epoll_event *e = <span style="color:#ff0000;">state->events</span>+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就绪表 state->events中。之后aeApiPoll会把epoll就绪表 state->events中的就绪事件拷贝到fired就绪表中

五、删除事件

当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。

void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{

    // 取出文件事件结构
    aeFileEvent *fe = &eventLoop->events[fd];

    // 未设置监听的事件类型,直接返回
    if (fe->mask == AE_NONE)
	return;

    // 计算新掩码
    fe->mask = fe->mask & (~mask);

    // 取消对给定 fd 的给定事件的监视
    aeApiDelEvent(eventLoop, fd, mask);
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) 
{
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;
    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.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (mask != AE_NONE) 
    {
        epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
    } 
    else 
    {
        epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
    }
}
删除的过程可以总结为以下几个步骤

1、根据fd在未就绪表中查找到事件

2、取消该fd对应的相应事件标识符

3、通知内核,内核会将红黑树上的相应事件也给取消。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值