一般而言 Redis 都是部署到 Linux 系统上,所以我们就看看使用 Redis 是怎么利用 linux 提供的 epoll 实现I/O 多路复用。
首先看看 epoll 提供的三个方法:
/*
- 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
/int epoll_create(int size);/
-
可以理解为,增删改 fd 需要监听的事件
-
epfd 是 epoll_create() 创建的句柄。
-
op 表示 增删改
-
epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态
*/int epoll_ctl(int epfd, int op, int fd, struct epoll_event event);/
-
可以理解为查询符合条件的事件
-
epfd 是 epoll_create() 创建的句柄。
-
epoll_event 用来存放从内核得到事件的集合
-
maxevents 获取的最大事件数
-
timeout 等待超时时间
*/int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
再看 Redis 对文件事件,封装epoll向上提供的接口:
/*
- 事件状态
*/typedef struct aeApiState { // epoll_event 实例描述符 int epfd; // 事件槽 struct epoll_event *events;
} aeApiState/*
- 创建一个新的 epoll
*/static int aeApiCreate(aeEventLoop eventLoop)/
- 调整事件槽的大小
*/static int aeApiResize(aeEventLoop eventLoop, int setsize)/
- 释放 epoll 实例和事件槽
*/static void aeApiFree(aeEventLoop eventLoop)/
- 关联给定事件到 fd
*/static int aeApiAddEvent(aeEventLoop eventLoop, int fd, int mask)/
- 从 fd 中删除给定事件
*/static void aeApiDelEvent(aeEventLoop eventLoop, int fd, int mask)/
- 获取可执行事件
*/static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
所以看看这个ae_peoll.c 如何对 epoll 进行封装的:
-
aeApiCreate()
是对epoll.epoll_create()
的封装。 -
aeApiAddEvent()
和aeApiDelEvent()
是对epoll.epoll_ctl()
的封装。 -
aeApiPoll()
是对epoll_wait()
的封装。
这样 Redis 的利用 epoll 实现的 I/O 复用器就比较清晰了。
再往上一层次我们需要看看 ea.c 是怎么封装的?
首先需要关注的是事件处理器的数据结构:
typedef struct aeFileEvent {
// 监听事件类型掩码,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask;
/* one of AE_(READABLE|WRITABLE) */
// 读事件处理器
aeFileProc *rfileProc;
// 写事件处理器
aeFileProc *wfileProc;
// 多路复用库的私有数据
void *clientData;
} aeFileEvent;
mask
就是可以理解为事件的类型。
除了使用 ae_peoll.c 提供的方法外,ae.c 还增加 “增删查” 的几个 API。
-
增:
aeCreateFileEvent
-
删:
aeDeleteFileEvent
-
查: 查包括两个维度
aeGetFileEvents
获取某个 fd 的监听类型和aeWait
等待某个fd 直到超时或者达到某个状态。
事件分发器(dispatcher)
Redis 的事件分发器 ae.c/aeProcessEvents
不但处理文件事件还处理时间事件,所以这里只贴与文件分发相关的出部分代码,dispather 根据 mask 调用不同的事件处理器。
//从 epoll 中获关注的事件
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 确保读/写事件只能执行其中一个
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++;
}
可以看到这个分发器,根据 mask 的不同将事件分别分发给了读事件和写事件。
文件事件处理器的类型
Redis 有大量的事件处理器类型,我们就讲解处理一个简单命令涉及到的三个处理器:
-
acceptTcpHandler 连接应答处理器,负责处理连接相关的事件,当有client 连接到Redis的时候们就会产生 AE_READABLE 事件。引发它执行。
-
readQueryFromClinet 命令请求处理器,负责读取通过 sokect 发送来的命令。
-
sendReplyToClient 命令回复处理器,当Redis处理完命令,就会产生 AE_WRITEABLE 事件,将数据回复给 client。
文件事件实现总结
我们按照开始给出的 Reactor 模型,从上到下讲解了文件事件处理器的实现,下面将会介绍时间时间的实现。
时间事件
====
Reids 有很多操作需要在给定的时间点进行处理,时间事件就是对这类定时任务的抽象。
先看时间事件的数据结构:
/* 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 *next;
} aeTimeEvent;
看见 next
我们就知道这个 aeTimeEvent 是一个链表结构。看图:
注意这是一个按照id倒序排列的链表,并没有按照事件顺序排序。
processTimeEvent
Redis 使用这个函数处理所有的时间事件,我们整理一下执行思路:
-
记录最新一次执行这个函数的时间,用于处理系统时间被修改产生的问题。
-
遍历链表找出所有 when_sec 和 when_ms 小于现在时间的事件。
-
执行事件对应的处理函数。
-
检查事件类型,如果是周期事件则刷新该事件下一次的执行事件。
-
否则从列表中删除事件。
综合调度器(aeProcessEvents)
======================
综合调度器是 Redis 统一处理所有事件的地方。我们梳理一下这个函数的简单逻辑:
// 1. 获取离当前时间最近的时间事件
shortest = aeSearchNearestTimer(eventLoop);
// 2. 获取间隔时间
timeval = shortest - nowTime;
// 如果timeval 小于 0,说明已经有需要执行的时间事件了。
if(timeval < 0){
timeval = 0
}
// 3. 在 timeval 时间内,取出文件事件。
numevents = aeApiPoll(eventLoop, timeval);
// 4.根据文件事件的类型指定不同的文件处理器
if (AE_READABLE) {
// 读事件
rfileProc(eventLoop,fd,fe->clientData,mask);
}
最后
关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:
- Java基础部分
- 算法与编程
- 数据库部分
- 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)
这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。
作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。
下几大专题:
- Java基础部分
[外链图片转存中…(img-IM7n037t-1721187613432)]
- 算法与编程
[外链图片转存中…(img-TtYOrGxM-1721187613433)]
- 数据库部分
[外链图片转存中…(img-47AQueGd-1721187613434)]
- 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)
[外链图片转存中…(img-IYUDPiGa-1721187613434)]
这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。
作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。