Lighttpd 是 一个安全,快速,标准,且非常灵活的 Web 服务器,并对高性能环境做了最佳化。通过编写定制化的插件(plugin)可以增强服务器的功能,如提供高性能的 API 服务等。Lighttpd 实际上可以看作一个封装良好的网络库,利用它提供的基础框架,可以方便地在插件中实现自己的业务逻辑。我在工作中通过编写 Lighttpd 插件实现过几个高性能的 API 服务,现在想要系统地分析一下 Lighttpd 插件的工作机制。直接看源码吧,plugin.h 文件中定义了插件的数据结构:
typedef struct { |
可以看到,除了开头定义了 version
、name
等成员变量,后面定义了大量的函数指针(上面贴的代码已经省略了很多),其实在创建插件的实例后,它们会被赋值为对应实现函数的指针,这样就实现了类似 C++ 中“多态”的功能:在不同插件的实例上调用相同的函数,会有不同的行为,这个行为取决于插件对该函数的特定实现。这些函数指针其实类似 C++ 中的“虚函数指针”,不过这里在编译期就确定了要调用哪个函数,并没有在运行时查询“虚函数表”的过程。
下面说一下前面定义的几个成员变量,version
表示该插件能够在哪个 Lighttpd 版本下工作,加载插件时会校验 version
和当前 Lighttpd 的版本是否一致,不一致则不会加载。name 是插件的名字(例如:proxy),data
是插件相关的数据,lib
是插件动态库(例如:mod_proxy.so)加载到内存中的地址,init
则是插件初始化函数的地址。这些成员变量的赋值会介绍,在这之前先看一个具体的例子,下面是 mod_proxy.c 文件中关于 proxy 插件的定义:
... |
可以看到 version
和 name
是在 mod_proxy_plugin_init()
函数中被赋值的。注意,插件的各种函数指针也是在这个初始化函数中被赋值的,具体的实现都是在 mod_proxy.c 中定义的,上面省略了这部分内容。再看上面的 plugin_data
,是插件相关的数据,函数参数中经常出现的 void *p_d
就是它,其实在 plugin.c 中也有一个 plugin_data
的定义:
typedef struct { |
这个定义相当于一个“父类”,而具体插件(例如: proxy)中的定义相当于“子类”,因为“子类”开始的部分和“父类”相同,可以保证:“子类”的指针转换成“父类”的指针,再转换回去时,数据不会丢失。其实 Lighttpd 看到的插件数据结构是 plugin.c 中定义的 plugin_data
,调用插件的函数传入数据后,插件可以再把数据的类型还原回去。这样,Lighttpd 所面对的数据接口就只有一个,具体插件的数据可以不对 Lighttpd 暴露,很好地隐藏了数据,同时也简化了处理的复杂度,提高了程序的扩展性。
而各个插件的加载是在 Lighttpd 启动后的初始化阶段完成,具体是在 server.c 中调用的plugins_load()
函数中完成的,这个函数定义在 plugin.c 中:
int plugins_load(server *srv) { |
为了突出主题,我去掉了一些不是特别重要的代码,简单概括一下上面函数做的事情:从 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) { |
在 server
中 plugin_slots
是一个 void 指针,而在 PLUGIN_TO_SLOT
宏中被转换为 plugin
结构体的三级指针,从前面申请内存的语句可以看出,plugin_slots
是一个存放 plugin
结构体二级指针的数组,数组的长度为 PLUGIN_FUNC_SIZEOF
。二级指针其实也是一个数组,数组的元素是 plugin
结构体的指针,指向具体的插件。先看一下 PLUGIN_FUNC_SIZEOF
是怎么定义的,它是在 plugin.c 中定义的枚举类型的最后一个枚举值:
typedef enum { |
这个枚举定义了 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
宏实现的:其实就是拿到 xxx
在 plugin_slots
表格中对应的行,然后把该行中所有插件的 xxx
函数分别运行一遍,该宏定义在 plugin.c 中:
|
注意,plugin.c 文件中定义了两个 PLUGIN_SLOT
宏,分别针对两类不同的函数,一类有 3 个参数,另一类有 2 个参数,文件中的注释写得很清楚。
最后总结一下,由于插件的实现细节是对 Lighttpd 隐藏的,它不知道哪些插件是做什么的,所以针对一个请求,Lighttpd 并不知道应该使用哪个插件进行处理,它会逐一调用所有插件的处理函数,由插件自己确定是否需要处理当前的请求。因为每次都调用了很多无用的函数,就会影响服务器的效率,但这却有助于服务器的扩展,这也是一个折中的问题。好了,关于 Lighttpd 插件的运行机制,就先分析这么多吧,再挖下去就没完没了了。