在Redis中,module.c 文件是用于支持动态加载和运行 Redis 模块的核心文件。Redis 模块允许开发者通过插件的方式,扩展和定制 Redis 的功能,而不需要修改 Redis 的核心源代码。以下是 module.c 文件的主要作用:
- 模块加载和卸载:module.c 包含了与模块加载和卸载相关的函数,如 moduleLoad 用于加载模块,moduleUnload 用于卸载模块。这些函数通过动态链接库(.so 文件)加载模块,使得模块的代码可以被 Redis 运行时调用。
- 模块命令处理: Redis 模块可以定义自己的命令,module.c 包含了用于处理模块命令的函数。例如,moduleCommand 函数用于处理模块命令的执行,根据不同的子命令调用相应的处理函数。
- 模块事件处理: 模块可以注册回调函数来处理 Redis 的事件,module.c 中包含了与模块事件处理相关的函数,如 moduleFireServerEvent 用于触发服务器事件。
- 模块数据类型支持: Redis 模块可以定义自己的数据类型,module.c 包含了与模块数据类型相关的函数,如 moduleTypeCreate 用于创建新的模块数据类型。
- 模块 API 支持:module.c 文件包含了大量用于与模块 API 交互的函数,开发者可以通过这些 API 实现模块的各种功能,包括键操作、数据类型操作、事件订阅等。
总体而言,module.c 是 Redis 模块系统的核心部分,提供了模块加载、命令处理、事件处理、数据类型支持等功能,使得 Redis 可以通过模块系统进行功能扩展和定制。
struct RedisModuleCtx {
/*通过这个指针,可以获取到多个 API 函数的指针。这样的设计可以使模块能够访问 Redis 提供的多个功能。*/
void *getapifuncptr; /* NOTE: Must be the first field. *///必须是结构体的第一个字段。它用于存储指向获取 API 函数指针的指针,通常用于访问 Redis 提供的 API 函数。
struct RedisModule *module; /* Module reference. *///指向与当前模块相关的模块的引用。
client *client; /* Client calling a command. *///指向调用命令的客户端的引用。
struct RedisModuleBlockedClient *blocked_client; /* Blocked client for//指向受阻塞的客户端的引用,用于支持线程安全的上下文。
thread safe context. */
struct AutoMemEntry *amqueue; /* Auto memory queue of objects to free. *///用于自动内存管理的对象队列,用于存储需要释放的对象。
int amqueue_len; /* Number of slots in amqueue. *///槽位的总数。
int amqueue_used; /* Number of used slots in amqueue. *///已经使用的槽位数。
int flags; /* REDISMODULE_CTX_... flags. *///用于指定上下文的一组标志,例如 REDISMODULE_CTX_... 标志。
void **postponed_arrays; /* To set with RM_ReplySetArrayLength(). *///用于在稍后通过 RM_ReplySetArrayLength() 设置的数组的指针数组。
int postponed_arrays_count; /* Number of entries in postponed_arrays. *///postponed_arrays 中的条目数量。
void *blocked_privdata; /* Privdata set when unblocking a client. *///在解除客户端的阻塞时设置的私有数据。
RedisModuleString *blocked_ready_key; /* Key ready when the reply callback//用于在键被客户端阻塞时为回复回调设置的准备好的键。
gets called for clients blocked
on keys. */
/* Used if there is the REDISMODULE_CTX_KEYS_POS_REQUEST flag set. */
int *keys_pos;//如果设置了 REDISMODULE_CTX_KEYS_POS_REQUEST 标志,则用于存储键的位置信息。
int keys_count;//存储在 keys_pos 中的键的数量。
struct RedisModulePoolAllocBlock *pa_head;//用于内存池分配的头部指针。
redisOpArray saved_oparray; /* When propagating commands in a callback
we reallocate the "also propagate" op
array. Here we save the old one to
restore it later. *///用于在回调中传播命令时保存旧的操作数组。
};
/* Load a module and initialize it. On success C_OK is returned, otherwise
* C_ERR is returned. */
/*Redis模块的加载和初始化过程。*/
int moduleLoad(const char *path, void **module_argv, int module_argc) {
int (*onload)(void *, void **, int);
void *handle;
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
ctx.client = moduleFreeContextReusedClient;
selectDb(ctx.client, 0);
struct stat st;
// 检查模块文件是否具有执行权限
if (stat(path, &st) == 0)
{ // this check is best effort
if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
serverLog(LL_WARNING, "Module %s failed to load: It does not have execute permissions.", path);
return C_ERR;
}
}
// 使用dlopen加载模块
handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
if (handle == NULL) {
serverLog(LL_WARNING, "Module %s failed to load: %s", path, dlerror());
return C_ERR;
}
// 获取模块的初始化函数指针
onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
if (onload == NULL) {
dlclose(handle);
serverLog(LL_WARNING,
"Module %s does not export RedisModule_OnLoad() "
"symbol. Module not loaded.",path);
return C_ERR;
}
// 调用模块的初始化函数
if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
if (ctx.module) {
moduleUnregisterCommands(ctx.module);
moduleUnregisterSharedAPI(ctx.module);
moduleUnregisterUsedAPI(ctx.module);
moduleFreeModuleStructure(ctx.module);
}
// 如果清理函数返回错误,则取消卸载
dlclose(handle);
serverLog(LL_WARNING,
"Module %s initialization failed. Module not loaded",path);
return C_ERR;
}
/* Redis module loaded! Register it. */
// 将加载的模块添加到模块字典中
dictAdd(modules,ctx.module->name,ctx.module);
ctx.module->blocked_clients = 0;
ctx.module->handle = handle;
serverLog(LL_NOTICE,"Module '%s' loaded from %s",ctx.module->name,path);
/* Fire the loaded modules event. */
// 触发模块加载事件
moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE,
REDISMODULE_SUBEVENT_MODULE_LOADED,
ctx.module);
moduleFreeContext(&ctx);
return C_OK;
}
模块的参数:
- path: 模块文件的路径,表示要加载的Redis模块的位置。
- module_argv: 模块初始化函数的参数,是一个指向指针数组的指针,用于传递参数给模块的初始化函数。
- module_argc: 模块初始化函数的参数个数,表示模块初始化函数期望接收的参数数量。
其中
handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
这句代码的目的是加载位于 path 路径下的共享库,并返回一个句柄,以便在程序运行时与该共享库进行交互,调用其中定义的函数或使用其中的变量。在这个特定的上下文中,它用于加载 Redis 模块,使得 Redis 可以动态地扩展其功能。
onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
这行代码的目的是将 RedisModule_OnLoad 函数的地址赋值给 onload 变量,以便后续可以通过 onload 调用 RedisModule_OnLoad 函数进行模块的初始化。这是在模块加载时执行的初始化函数,用于完成模块所需的各种准备工作。
onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR
这行代码的目的是调用模块的初始化函数,并检查初始化函数的返回值是否为 REDISMODULE_ERR。如果初始化失败,可能是由于模块自身的问题导致,此时会进行一些清理工作,并记录错误日志,然后返回 C_ERR 表示模块加载失败。如果初始化成功,返回值为 C_OK 表示模块加载成功。
dlclose(handle);
dlclose(handle) 用于关闭通过 dlopen 函数打开的共享库,并释放与该库关联的资源。
dlclose 并不会立即卸载共享库,而是在满足一定条件后卸载。具体来说,只有在所有使用 dlopen 打开的该共享库的句柄都被关闭后,共享库才会被卸载。这可以确保在仍然有其他代码使用该库时,库不会被提前卸载。
int moduleUnload(sds name) {
struct RedisModule *module = dictFetchValue(modules,name);
if (module == NULL) {
errno = ENOENT;
return REDISMODULE_ERR;
} else if (listLength(module->types)) {
errno = EBUSY;
return REDISMODULE_ERR;
} else if (listLength(module->usedby)) {
errno = EPERM;
return REDISMODULE_ERR;
} else if (module->blocked_clients) {
errno = EAGAIN;
return REDISMODULE_ERR;
}
/* Give module a chance to clean up. */
// 调用模块的清理函数(如果有的话)
int (*onunload)(void *);
onunload = (int (*)(void *))(unsigned long) dlsym(module->handle, "RedisModule_OnUnload");
if (onunload) {
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
ctx.module = module;
ctx.client = moduleFreeContextReusedClient;
int unload_status = onunload((void*)&ctx);
moduleFreeContext(&ctx);
if (unload_status == REDISMODULE_ERR) {
serverLog(LL_WARNING, "Module %s OnUnload failed. Unload canceled.", name);
errno = ECANCELED;
return REDISMODULE_ERR;
}
}
// 执行模块卸载前的清理工作
moduleFreeAuthenticatedClients(module);
moduleUnregisterCommands(module);
moduleUnregisterSharedAPI(module);
moduleUnregisterUsedAPI(module);
moduleUnregisterFilters(module);
/* Remove any notification subscribers this module might have */
// 移除模块的所有通知订阅者
moduleUnsubscribeNotifications(module);
moduleUnsubscribeAllServerEvents(module);
/* Unload the dynamic library. */
// 卸载动态库
if (dlclose(module->handle) == -1) {
char *error = dlerror();
if (error == NULL) error = "Unknown error";
serverLog(LL_WARNING,"Error when trying to close the %s module: %s",
module->name, error);
}
/* Fire the unloaded modules event. */
// 触发模块卸载事件
moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE,
REDISMODULE_SUBEVENT_MODULE_UNLOADED,
module);
/* Remove from list of modules. */
// 从模块字典中删除该模块
serverLog(LL_NOTICE,"Module %s unloaded",module->name);
dictDelete(modules,module->name);
module->name = NULL; /* The name was already freed by dictDelete(). */
moduleFreeModuleStructure(module);
return REDISMODULE_OK;
}
这段代码实现了 Redis 模块的卸载过程。具体来说,它的主要作用是将一个已加载的 Redis 模块从服务器中卸载,并进行相应的清理工作。
void moduleCommand(client *c) {
char *subcmd = c->argv[1]->ptr;
// 处理 MODULE HELP 命令
if (c->argc == 2 && !strcasecmp(subcmd,"help")) {
const char *help[] = {
"LIST -- Return a list of loaded modules.",
"LOAD <path> [arg ...] -- Load a module library from <path>.",
"UNLOAD <name> -- Unload a module.",
NULL
};
addReplyHelp(c, help);
} else
// 处理 MODULE LOAD 命令
if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
robj **argv = NULL;
int argc = 0;
if (c->argc > 3) {
argc = c->argc - 3;
argv = &c->argv[3];
}
// 调用 moduleLoad 函数加载模块
if (moduleLoad(c->argv[2]->ptr,(void **)argv,argc) == C_OK)
addReply(c,shared.ok);
else
addReplyError(c,
"Error loading the extension. Please check the server logs.");
} else if (!strcasecmp(subcmd,"unload") && c->argc == 3) {
// 处理 MODULE UNLOAD 命令
// 调用 moduleUnload 函数卸载模块
if (moduleUnload(c->argv[2]->ptr) == C_OK)
addReply(c,shared.ok);
else {
char *errmsg;
switch(errno) {
case ENOENT:
errmsg = "no such module with that name";
break;
case EBUSY:
errmsg = "the module exports one or more module-side data "
"types, can't unload";
break;
case EPERM:
errmsg = "the module exports APIs used by other modules. "
"Please unload them first and try again";
break;
case EAGAIN:
errmsg = "the module has blocked clients. "
"Please wait them unblocked and try again";
break;
default:
errmsg = "operation not possible.";
break;
}
addReplyErrorFormat(c,"Error unloading module: %s",errmsg);
}
} else if (!strcasecmp(subcmd,"list") && c->argc == 2) {
// 处理 MODULE LIST 命令
// 调用 addReplyLoadedModules 函数,向客户端回复已加载模块的信息
addReplyLoadedModules(c);
} else {
// 无效的子命令,返回错误信息
addReplySubcommandSyntaxError(c);
return;
}
}
这个函数根据输入的命令不同,来加载或卸载模块,或者是其他操作。
void moduleFireServerEvent(uint64_t eid, int subid, void *data) {
/* Fast path to return ASAP if there is nothing to do, avoiding to
* setup the iterator and so forth: we want this call to be extremely
* cheap if there are no registered modules. */
/*程序通过检查是否有已注册的模块来优化性能。如果没有已注册的模块,就没有必要执行一些开销较大的操作,*/
if (listLength(RedisModule_EventListeners) == 0) return;
listIter li;
listNode *ln;
listRewind(RedisModule_EventListeners,&li);
//遍历列表观察是否有指定事件
while((ln = listNext(&li))) {
RedisModuleEventListener *el = ln->value;
if (el->event.id == eid) {
//会创建一个 RedisModuleCtx 上下文对象,用于模块的事件处理。该上下文包含模块信息和客户端信息。
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;//初始化操作
ctx.module = el->module;
/*ModulesInHooks 可能是一个用于跟踪当前活动的 Redis 模块数量的计数器。
当模块被加载或卸载时,该计数器会相应地增加或减少。*/
if (ModulesInHooks == 0) {//函数递增了 ModulesInHooks 计数器,用于跟踪当前活动的 Redis 模块数量,并将事件所属的模块的 in_hook 计数器递增。
ctx.client = moduleFreeContextReusedClient;
} else {
/*如果存在其他模块,新创建的客户端将拥有 CLIENT_MODULE 标志,
表示它是一个模块客户端。并且,将用户字段 user 设置为 NULL,表示使用 root 用户。*/
ctx.client = createClient(NULL);
ctx.client->flags |= CLIENT_MODULE;
ctx.client->user = NULL; /* Root user. */
}
void *moduledata = NULL;
RedisModuleClientInfoV1 civ1;
RedisModuleReplicationInfoV1 riv1;
RedisModuleModuleChangeV1 mcv1;
/* Start at DB zero by default when calling the handler. It's
* up to the specific event setup to change it when it makes
* sense. For instance for FLUSHDB events we select the correct
* DB automatically. */
/*在调用模块事件处理函数之前,默认选择数据库 0。
针对特定的事件设置可能在合适时刻更改选择的数据库,例如对于 FLUSHDB 事件,会自动选择正确的数据库。*/
selectDb(ctx.client, 0);
/* Event specific context and data pointer setup. */
/*这段代码的主要目的是为不同类型的模块事件设置特定的上下文和数据指针,以便在调用模块事件处理函数时传递正确的信息*/
if (eid == REDISMODULE_EVENT_CLIENT_CHANGE) {//表示客户端变更的信息。
modulePopulateClientInfoStructure(&civ1,data,
el->event.dataver);
moduledata = &civ1;
} else if (eid == REDISMODULE_EVENT_REPLICATION_ROLE_CHANGED) {//表示主从角色切换的信息。
modulePopulateReplicationInfoStructure(&riv1,el->event.dataver);
moduledata = &riv1;
} else if (eid == REDISMODULE_EVENT_FLUSHDB) {//模块可以使用此事件来执行与数据库清空相关的操作。
moduledata = data;
RedisModuleFlushInfoV1 *fi = data;
if (fi->dbnum != -1)
selectDb(ctx.client, fi->dbnum);
} else if (eid == REDISMODULE_EVENT_MODULE_CHANGE) {//模块可以使用此事件来执行与模块加载或卸载相关的操作。
RedisModule *m = data;
if (m == el->module)
continue;
mcv1.version = REDISMODULE_MODULE_CHANGE_VERSION;
mcv1.module_name = m->name;
mcv1.module_version = m->ver;
moduledata = &mcv1;
} else if (eid == REDISMODULE_EVENT_LOADING_PROGRESS) {//可以触发此事件以跟踪加载进度。模块可以使用此事件来执行与加载进度相关的操作。
moduledata = data;
} else if (eid == REDISMODULE_EVENT_CRON_LOOP) {//模块可以使用此事件来执行周期性操作。
moduledata = data;
}
ModulesInHooks++;//使用的模块数量加加
el->module->in_hook++;//递增当前事件所属的模块的 in_hook 计数器。这个计数器用于跟踪当前活动的 Redis 模块在处理事件时的数量。
el->callback(&ctx,el->event,subid,moduledata);
el->module->in_hook--;
ModulesInHooks--;
/*如果当前活动的 Redis 模块数量不为零,释放模块上下文中的客户端资源。
这个操作可能是为了清理模块上下文中的资源,确保在所有模块处理完事件后进行清理。*/
if (ModulesInHooks != 0) freeClient(ctx.client);//这个客户端是主动生成的中间客户端
moduleFreeContext(&ctx);
}
}
}
这个函数是用于在Redis内部触发可被模块拦截的事件,并提供了参数 ‘data’、‘eid’ 和 ‘subid’ 以支持事件的处理和提供额外的信息给回调函数。这有助于模块在Redis内部事件发生时执行自定义操作。
在Redis中,普通上下文(Regular Context)和模块上下文(Module Context)是两种不同的执行上下文,用于处理不同类型的操作和命令。以下是它们之间的主要区别:
- 普通上下文(Regular Context):
- 用途:普通上下文通常用于处理普通的Redis命令,例如通过客户端连接传递的命令请求。
- 特点:普通上下文用于执行传统的Redis命令,这些命令可以是由Redis核心提供的内置命令,也可以是用户自定义的命令。
- 关联:与客户端连接(client)直接相关,普通上下文在处理普通的Redis命令时涉及到客户端的输入和输出。
- 模块上下文(Module Context):
- 用途:模块上下文用于执行由Redis模块注册的自定义命令和操作。
- 特点:模块上下文是为了支持Redis模块API而设计的,它允许模块开发者注册自定义命令、操作和事件处理函数,并以一种扩展Redis功能的方式与核心交互。
- 关联:与模块注册的命令和操作直接相关,模块上下文提供了API来访问模块特有的功能和数据。
总体来说,普通上下文用于处理传统的Redis命令,而模块上下文用于处理通过Redis模块注册的自定义命令和操作。这种分离允许模块在不影响Redis核心的情况下扩展Redis的功能。
RedisModuleCommandProxy *cp = (void*)(unsigned long)c->cmd->getkeys_proc;
struct RedisModuleCommandProxy {
struct RedisModule *module; // 指向注册该命令的模块
RedisModuleCmdFunc func; // 与该命令相关联的实际命令执行函数
struct redisCommand *rediscmd; // 与 Redis 服务器注册的实际 Redis 命令
};
cp->module = NULL; // 该值未在转换中设置
cp->func = (RedisModuleCmdFunc)c->cmd->getkeys_proc; // 将 getkeys_proc 转换为 RedisModuleCmdFunc 类型
cp->rediscmd = c->cmd; // 将 redisCommand 结构体赋值给 rediscmd
在C语言中,函数指针和指向结构体的指针都是一种指针类型。因为这两种类型的指针在底层都是存储内存地址的整数值,所以它们之间可以相互转换,尽管这可能会导致未定义的行为。这里的关键是两者的大小和内部表示方式要兼容。
在这段代码中,getkeys_proc 是一个函数指针类型,其指向的函数原型为 **int *redisGetKeysProc(struct redisCommand *cmd, robj argv, int argc, int *numkeys)。然后,通过将 c->cmd->getkeys_proc 强制类型转换为 RedisModuleCommandProxy 结构体指针,可以访问结构体的成员。
这种转换的有效性取决于两者的内存布局是否兼容。在这个特定的情况下,函数指针 redisGetKeysProc 的大小和布局可能与 RedisModuleCommandProxy 结构体的大小和布局相兼容,使得强制类型转换是合法的。但这种做法是依赖于特定平台和编译器的,可能会导致不可移植的代码。因此,这种类型的转换应该小心使用,并且最好通过更安全的方式来实现模块和命令的关联。