Lighttpd 之 plugin 分析

Lighttpd 是 一个安全,快速,标准,且非常灵活的 Web 服务器,并对高性能环境做了最佳化。通过编写定制化的插件(plugin)可以增强服务器的功能,如提供高性能的 API 服务等。Lighttpd 实际上可以看作一个封装良好的网络库,利用它提供的基础框架,可以方便地在插件中实现自己的业务逻辑。我在工作中通过编写 Lighttpd 插件实现过几个高性能的 API 服务,现在想要系统地分析一下 Lighttpd 插件的工作机制。直接看源码吧,plugin.h 文件中定义了插件的数据结构:

typedef struct {
size_t version;
buffer *name;

void *data;
void *lib;

void *(* init) ();

handler_t (* set_defaults) (server *srv, void *p_d);
handler_t (* cleanup) (server *srv, void *p_d);
handler_t (* handle_trigger) (server *srv, void *p_d);
handler_t (* handle_sighup) (server *srv, void *p_d);

handler_t (* handle_uri_raw) (server *srv, connection *con, void *p_d);
...
} plugin;

可以看到,除了开头定义了 versionname 等成员变量,后面定义了大量的函数指针(上面贴的代码已经省略了很多),其实在创建插件的实例后,它们会被赋值为对应实现函数的指针,这样就实现了类似 C++ 中“多态”的功能:在不同插件的实例上调用相同的函数,会有不同的行为,这个行为取决于插件对该函数的特定实现。这些函数指针其实类似 C++ 中的“虚函数指针”,不过这里在编译期就确定了要调用哪个函数,并没有在运行时查询“虚函数表”的过程。
下面说一下前面定义的几个成员变量,version 表示该插件能够在哪个 Lighttpd 版本下工作,加载插件时会校验 version 和当前 Lighttpd 的版本是否一致,不一致则不会加载。name 是插件的名字(例如:proxy),data 是插件相关的数据,lib 是插件动态库(例如:mod_proxy.so)加载到内存中的地址,init 则是插件初始化函数的地址。这些成员变量的赋值会介绍,在这之前先看一个具体的例子,下面是 mod_proxy.c 文件中关于 proxy 插件的定义:

...
typedef struct {
PLUGIN_DATA;

buffer *parse_response;
buffer *balance_buf;

plugin_config **config_storage;

plugin_config conf;
} plugin_data;
...
int mod_proxy_plugin_init(plugin *p) {
p->version = LIGHTTPD_VERSION_ID;
p->name = buffer_init_string("proxy");

p->init = mod_proxy_init;
p->cleanup = mod_proxy_free;
p->set_defaults = mod_proxy_set_defaults;
p->connection_reset = mod_proxy_connection_close_callback;
p->handle_connection_close = mod_proxy_connection_close_callback;
p->handle_uri_clean = mod_proxy_check_extension;
p->handle_subrequest = mod_proxy_handle_subrequest;
p->handle_trigger = mod_proxy_trigger;

p->data = NULL;

return 0;
}

可以看到 versionname 是在 mod_proxy_plugin_init() 函数中被赋值的。注意,插件的各种函数指针也是在这个初始化函数中被赋值的,具体的实现都是在 mod_proxy.c 中定义的,上面省略了这部分内容。再看上面的 plugin_data,是插件相关的数据,函数参数中经常出现的 void *p_d 就是它,其实在 plugin.c 中也有一个 plugin_data 的定义:

typedef struct {
PLUGIN_DATA;
} plugin_data;

#define PLUGIN_DATA size_t id // plugin.h

这个定义相当于一个“父类”,而具体插件(例如: proxy)中的定义相当于“子类”,因为“子类”开始的部分和“父类”相同,可以保证:“子类”的指针转换成“父类”的指针,再转换回去时,数据不会丢失。其实 Lighttpd 看到的插件数据结构是 plugin.c 中定义的 plugin_data,调用插件的函数传入数据后,插件可以再把数据的类型还原回去。这样,Lighttpd 所面对的数据接口就只有一个,具体插件的数据可以不对 Lighttpd 暴露,很好地隐藏了数据,同时也简化了处理的复杂度,提高了程序的扩展性。
而各个插件的加载是在 Lighttpd 启动后的初始化阶段完成,具体是在 server.c 中调用的plugins_load() 函数中完成的,这个函数定义在 plugin.c 中:

int plugins_load(server *srv) {
plugin *p;
int (*init)(plugin *pl);
const char *error;
size_t i, j;

for (i = 0; i < srv->srvconf.modules->used; i++) {
data_string *d = (data_string *)srv->srvconf.modules->data[i];
char *modules = d->value->ptr;
...
buffer_copy_string_buffer(srv->tmp_buf, srv->srvconf.modules_dir);
buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN("/"));
buffer_append_string(srv->tmp_buf, modules);
buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN(".so"));

p = plugin_init();
if (NULL == (p->lib = dlopen(srv->tmp_buf->ptr, RTLD_NOW|RTLD_GLOBAL))) {
...
plugin_free(p);
return -1;
}

buffer_reset(srv->tmp_buf);
buffer_copy_string(srv->tmp_buf, modules);
buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN("_plugin_init"));

init = (int (*)(plugin *))(intptr_t)dlsym(p->lib, srv->tmp_buf->ptr);
if ((error = dlerror()) != NULL) {
...
plugin_free(p);
return -1;
}

if ((*init)(p)) {
...
plugin_free(p);
return -1;
}

plugins_register(srv, p);
}

return 0;
}

为了突出主题,我去掉了一些不是特别重要的代码,简单概括一下上面函数做的事情:从 Lighttpd 配置中读取要加载的插件列表,针对每个插件:调用 dlopen() 将动态库.so加载到内存,再使用 dlsym() 获取插件初始化函数的地址,并调用它初始化插件(例如:mod_proxy_plugin_init() ,最后调用 plugins_register() 将插件注册到 server 的插件列表 plugins 中。之后,server.c 中调用了 plugins_call_init() 函数填充 server 的 plugin_slots:

handler_t plugins_call_init(server *srv) {
size_t i;
plugin **ps;

ps = srv->plugins.ptr;
srv->plugin_slots = calloc(PLUGIN_FUNC_SIZEOF, sizeof(ps));

for (i = 0; i < srv->plugins.used; i++) {
size_t j;
plugin *p = ps[i];

#define PLUGIN_TO_SLOT(x, y) \
if (p->y) { \
plugin **slot = ((plugin ***)(srv->plugin_slots))[x]; \
if (!slot) { \
slot = calloc(srv->plugins.used, sizeof(*slot));\
((plugin ***)(srv->plugin_slots))[x] = slot; \
} \
for (j = 0; j < srv->plugins.used; j++) { \
if (slot[j]) continue;\
slot[j] = p;\
break;\
}\
}

PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean);
...
#undef PLUGIN_TO_SLOT

if (p->init) {
if (NULL == (p->data = p->init())) {
...
return HANDLER_ERROR;
}

((plugin_data *)(p->data))->id = i + 1;

if (p->version != LIGHTTPD_VERSION_ID) {
...
return HANDLER_ERROR;
}
} else {
p->data = NULL;
}
}

return HANDLER_GO_ON;
}

serverplugin_slots 是一个 void 指针,而在 PLUGIN_TO_SLOT 宏中被转换为 plugin 结构体的三级指针,从前面申请内存的语句可以看出,plugin_slots 是一个存放 plugin 结构体二级指针的数组,数组的长度为 PLUGIN_FUNC_SIZEOF。二级指针其实也是一个数组,数组的元素是 plugin 结构体的指针,指向具体的插件。先看一下 PLUGIN_FUNC_SIZEOF 是怎么定义的,它是在 plugin.c 中定义的枚举类型的最后一个枚举值:

typedef enum {
PLUGIN_FUNC_UNSET,
PLUGIN_FUNC_HANDLE_URI_CLEAN,
PLUGIN_FUNC_HANDLE_URI_RAW,
PLUGIN_FUNC_HANDLE_REQUEST_DONE,
PLUGIN_FUNC_HANDLE_CONNECTION_CLOSE,
PLUGIN_FUNC_HANDLE_TRIGGER,
PLUGIN_FUNC_HANDLE_SIGHUP,
PLUGIN_FUNC_HANDLE_SUBREQUEST,
PLUGIN_FUNC_HANDLE_SUBREQUEST_START,
PLUGIN_FUNC_HANDLE_JOBLIST,
PLUGIN_FUNC_HANDLE_DOCROOT,
PLUGIN_FUNC_HANDLE_PHYSICAL,
PLUGIN_FUNC_CONNECTION_RESET,
PLUGIN_FUNC_INIT,
PLUGIN_FUNC_CLEANUP,
PLUGIN_FUNC_SET_DEFAULTS,

PLUGIN_FUNC_SIZEOF
} plugin_t;

这个枚举定义了 plugin 结构体中函数指针的类型,而最后一个类型 PLUGIN_FUNC_SIZEOF 的值刚好是上面所定义的所有枚举类型的数量。这是一个很常用的技巧,可以保证在上面增加类型的时候,保证程序中可以得到正确的类型数量,而不要去改动那些需要类型数量的代码。然后,再回到 PLUGIN_TO_SLOT 宏,它有两个参数,第一个参数 x 是枚举类型 plugin_t,第二个参数 x 对应的在 plugin 结构体中函数指针的名字。从宏定义代码来看,它的作用是:判断插件是否定义了名字为 y 的函数,如果有定义则将指针 p 添加到数组 plugin_slots 的第 x 行的末尾:if 语句判断如果 plugin_slots 的第 x 行不存在则创建出来,for 循环是为了将 p 添加到 plugin_slots 的第 x 行的末尾。PLUGIN_TO_SLOT 应用完成之后实际上得到了一个逻辑上类似下表的表格,能够很容易知道定义了某个函数的插件都有哪些(其中 p1-p4 分别是指向不同 plugin 结构体的指针):

Index 0 1 2
XX_UNSET p1 p2 p3
XX_URI_RAW p1 p3 -
XX_SET_DEFAULTS p3 p4 -

现在,这个 PLUGIN_TO_SLOT 的作用基本搞明白了,继续看 plugins_call_init() 函数,之后调用了插件 plugin 结构体中的 init() 函数并检查 version 和 Lighttpd 版本是否一致,至此 plugins_call_init() 函数就结束了。而 plugin_slots 这样一个表格有什么用呢?其实 Lighttpd 想做到更好的抽象,希望实现一种机制:在需要调用某一类函数的时候,能够方便地调用所有插件中的该类函数的实现。我们先看 plugin.h 中最后声明的一推 plugins_call_xxx 函数,这才是插件对 Lighttpd 暴露的接口,这些函数是通过另外一个 PLUGIN_TO_SLOT 宏实现的:其实就是拿到 xxxplugin_slots 表格中对应的行,然后把该行中所有插件的 xxx 函数分别运行一遍,该宏定义在 plugin.c 中:

#define PLUGIN_TO_SLOT(x, y) \
handler_t plugins_call_##y(server *srv, connection *con) {\
plugin **slot;\
size_t j;\
if (!srv->plugin_slots) return HANDLER_GO_ON;\
slot = ((plugin ***)(srv->plugin_slots))[x];\
if (!slot) return HANDLER_GO_ON;\
for (j = 0; j < srv->plugins.used && slot[j]; j++) { \
plugin *p = slot[j];\
handler_t r;\
switch(r = p->y(srv, con, p->data)) {\
case HANDLER_GO_ON:\
break;\
case HANDLER_FINISHED:\
case HANDLER_COMEBACK:\
case HANDLER_WAIT_FOR_EVENT:\
case HANDLER_WAIT_FOR_FD:\
case HANDLER_ERROR:\
return r;\
default:\
log_error_write(srv, __FILE__, __LINE__, "sbs", #x, p->name, "unknown state");\
return HANDLER_ERROR;\
}\
}\
return HANDLER_GO_ON;\
}

PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean)
...

注意,plugin.c 文件中定义了两个 PLUGIN_SLOT 宏,分别针对两类不同的函数,一类有 3 个参数,另一类有 2 个参数,文件中的注释写得很清楚。
最后总结一下,由于插件的实现细节是对 Lighttpd 隐藏的,它不知道哪些插件是做什么的,所以针对一个请求,Lighttpd 并不知道应该使用哪个插件进行处理,它会逐一调用所有插件的处理函数,由插件自己确定是否需要处理当前的请求。因为每次都调用了很多无用的函数,就会影响服务器的效率,但这却有助于服务器的扩展,这也是一个折中的问题。好了,关于 Lighttpd 插件的运行机制,就先分析这么多吧,再挖下去就没完没了了。

0%