标签 wifidog原理 下的文章

wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(1)---fdevents结构体和fdevent系统对外接口

前面讲了lighttpd的插件系统,这一篇将看一看lighttpd中的fdevent系统。fdevent系统主要是处理各种IO事件,在web服务器中,主要就是向socket写数据和从socket读数据。通常,web服务器是IO密集型程序,这就要求在数据的读写上,web服务器必须能够具有很好的性能,不会因为某个socket的阻塞而致使其他socket也被阻塞,否则会大大降低服务器的性能。因此,大部分的web服务器都采用非阻塞IO进行数据的读写。lighttpd通过fdevent系统,采用类似OO中面向对象的方式将对IO事件的处理进行封装,对于不同的IO系统,提供一个统一的接口。
lighttpd采用了所谓的Reactor模式,也就是非阻塞IO加多路复用(non-blocking IO + IO multiplexing)。在多路复用上,lighttpd通过fdevent将各种不同的实现进行封装。lighttpd使用的多路IO有如下几个:
1.png

下面看一下fdevent.h中fdevents结构体,这个结构体相当于是一个虚基类,其中的函数指针是纯虚函数。对于每种实现,则相当于是继承了这个基类并实现了其中的纯虚函数,也就是给函数指针赋一个函数地址值。下面是代码:

typedef struct fdevents
{
    fdevent_handler_t type; //多路IO类型
     fdnode **fdarray;     //文件描述符数组
     size_t maxfds;         //最大的文件描述符数

#ifdef USE_LINUX_SIGIO
    int in_sigio;
    int signum;
    sigset_t sigset;
    siginfo_t siginfo;
    bitset *sigbset;
 #endif

#ifdef USE_LINUX_EPOLL
    int epoll_fd;
    struct epoll_event *epoll_events;
 #endif

#ifdef USE_POLL
    struct pollfd *pollfds;     //描述符及其状态的结构体数组
     size_t size;                     //数组中数据的个数
     size_t used;                     //数组的大小
    //用于存储pollfds中为使用的位置。
    //由于可能的删除操作,会是pollfds中存在空档,将这些空档
    //的索引存在unused中,便于下次插入操作时直接使用这些空档
    //减少空间的浪费。
     buffer_int unused;     
 #endif

#ifdef USE_SELECT
    //三个文件描述符集合
     fd_set select_read;             //可读,对应FDEVENT_IN
     fd_set select_write;         //可写,对应FDEVENT_OUT
     fd_set select_error;         //处于异常条件,对应FDEVENT_ERR
    //由于select函数会修改上面的三个集合,
    //因此,在这里保存一个初始的副本。
     fd_set select_set_read;
    fd_set select_set_write;
    fd_set select_set_error;
    int select_max_fd;             //最大的文件描述符数。
 #endif

#ifdef USE_SOLARIS_DEVPOLL
    int devpoll_fd;
    struct pollfd *devpollfds;
 #endif

#ifdef USE_FREEBSD_KQUEUE
    int kq_fd;
    struct kevent *kq_results;
    bitset *kq_bevents;
 #endif

#ifdef USE_SOLARIS_PORT
    int port_fd;
 #endif

    //统一的操作接口,与后面的函数声明对应。
     int (*reset) (struct fdevents * ev);
    void (*free) (struct fdevents * ev);
    int (*event_add) (struct fdevents * ev, int fde_ndx, int fd, int events);
    int (*event_del) (struct fdevents * ev, int fde_ndx, int fd);
    int (*event_get_revent) (struct fdevents * ev, size_t ndx);
    int (*event_get_fd) (struct fdevents * ev, size_t ndx);
    int (*event_next_fdndx) (struct fdevents * ev, int ndx);
    int (*poll) (struct fdevents * ev, int timeout_ms);
    int (*fcntl_set) (struct fdevents * ev, int fd);
} fdevents;

可以看到这个结构体中使用很多宏,这是为了在编译的时候去掉那些没有使用到的变量,根据当前所使用的多路IO系统,对这个结构体进行定制。
结构体的第一个成员是一个枚举类型fdevent_handler_t,定义如下:

typedef enum
{
    FDEVENT_HANDLER_UNSET,                 //未定义
     FDEVENT_HANDLER_SELECT,             //select
     FDEVENT_HANDLER_POLL,                 //poll
     FDEVENT_HANDLER_LINUX_RTSIG,         //rtsig
     FDEVENT_HANDLER_LINUX_SYSEPOLL,     //sysepoll
     FDEVENT_HANDLER_SOLARIS_DEVPOLL,     //devpoll
     FDEVENT_HANDLER_FREEBSD_KQUEUE,     //kqueue
     FDEVENT_HANDLER_SOLARIS_PORT         //port
 } fdevent_handler_t;

这个枚举类型标记了所有可能用到的多路IO系统。
结构体中的第二个成员fdnode **fdarray;,是一个fdnode类型变量的数组。fdnode的定义如下:

typedef struct _fdnode
{
    fdevent_handler handler; //处理函数指针
    void *ctx;                //文件描述符的context
    int fd;                  //文件描述符
    struct _fdnode *prev, *next; //指针
} fdnode;

fdevent_handler handler是一个函数指针,用来存储这个描述符处理函数的地址。关于这个地址,后面的文章中将会有详细的介绍,其定义为typedef handler_t(*fdevent_handler) (void *srv, void *ctx, int revents);。从最后两个变量可以看出,这应该是一个链表的节点,但是,这个结构体是以数组的形式存储的,也就是fdevents中的fdarray变量,这样可以提高查询的效率。
后面由宏包裹的变量就是对于各个不同的多路IO系统定义的变量。我们着重看一看linux下的epoll所使用的变量:

#ifdef USE_LINUX_EPOLL
     int epoll_fd; //epoll_create返回的描述符
     struct epoll_event *epoll_events;//保存fd及对应的事件
#endif

这里要说明一下poll使用的变量buffer_init unused。这个变量的类型的定义如下:

typedef struct
{
    int *ptr;         //位置索引数组。
    size_t used;     //数组中数据个数。
    size_t size;     //数组长度。
} buffer_int;

其实就是一个int数组,只不过封装了一下,增加了两个属性。unused数组中存放的是pollfds的下标值。在后面的实现中我们可以看到,pollfds是一个struct pollfd类型数组,注意,不是这个类型的指针的数组。这个数组的大小是根据fdevents中的maxfds的值定的,并且在初始化的时候数组的空间也一次性分配好。由于对pollfds数组有删除元素的操作,因此,会在数组中留下“洞”,而ununsd就是存储这些“洞”的下标值,便于在插入元素时,快速的找到位置。这是一个很有用的技巧,在对数据进行反复的删除插入元素操作时,可以提高效率。大多数情况下使用栈链表来存储这些可用空间的下标,栈顶指向链表头。
其余的变量读者可自行分析。
接下来看看这些函数指针。这些函数指针对应与结构体定义后面的一系列函数声明。从名字中可以轻易的看出对应关系。

/*
 * 重置和释放fdevent系统。
 */
int fdevent_reset(fdevents * ev);
void fdevent_free(fdevents * ev);
/*
 * 将fd增加到fd event系统中。events是要对fd要监听的事件。
 * fde_ndx是fd对应的fdnode在ev->fdarray中的下标值的指针。
 * 如果fde_ndx==NULL,则表示在fd event系统中增加fd。如果不为NULL,则表示这个
 * fd已经在系统中存在,这个函数的功能就变为将对fd监听的事件变为events。
 */
int fdevent_event_add(fdevents * ev, int *fde_ndx, int fd, int events);
/*
 * 从fd event系统中删除fd。 fde_ndx的内容和上面的一致。
 */
int fdevent_event_del(fdevents * ev, int *fde_ndx, int fd);
/*
 * 返回ndx对应的fd所发生的事件。
 * 这里的ndx和上面的fde_ndx不一样,这个ndx是ev->epoll_events中epoll_event结构体的下标。
 * 第一次调用的时候,通常ndx为-1。
 * 这个ndx和其对应的fd没有关系。而fde_ndx等于其对应的fd。
 */
int fdevent_event_get_revent(fdevents * ev, size_t ndx);
/*
 * 返回ndx对应的fd。
 */
int fdevent_event_get_fd(fdevents * ev, size_t ndx);
/*
 * 返回下一个发生IO事件的fd。
 */
int fdevent_event_next_fdndx(fdevents * ev, int ndx);
/*
 * 开始等待IO事件。timeout_ms是超时限制。
 */
int fdevent_poll(fdevents * ev, int timeout_ms);
/**
 * 设置fd的状态,通常是设置为运行exec在子进程中关闭和非阻塞。
 */
int fdevent_fcntl_set(fdevents * ev, int fd);

在fdevent.c文件中,这些函数的实现基本上都是简单的调用fdevents结构体中对应的函数指针。对于lighttpd,通过调用上面的这些函数完成IO事件的处理,对于具体到底是谁处理了这些事件,lighttpd并不知道,也不关心。
剩下的函数声明:

/*
 * 返回fd对应的事件处理函数地址。也就是fdnode中handler的值。
 */
fdevent_handler fdevent_get_handler(fdevents * ev, int fd);
/*
 * 返回fd对应的环境。也就是fdnode中ctx的值。
 */
void *fdevent_get_context(fdevents * ev, int fd);

/*
 * 注册和取消注册fd。
 * 就是生成一个fdnode,然后保存在ev->fdarray中。或者删除之。
 */
int fdevent_register(fdevents * ev, int fd, fdevent_handler handler, void *ctx);
int fdevent_unregister(fdevents * ev, int fd);
/**
 * 初始化各种多路IO。
 */
int fdevent_select_init(fdevents * ev);
int fdevent_poll_init(fdevents * ev);
int fdevent_linux_rtsig_init(fdevents * ev);
int fdevent_linux_sysepoll_init(fdevents * ev);
int fdevent_solaris_devpoll_init(fdevents * ev);
int fdevent_freebsd_kqueue_init(fdevents * ev);

下面总结一下:
文件fdevent.h中声明的一系列函数就是fdevent系统对外的接口,这相当与类的公有函数。lighttpd通过调用这些函数来实现IO事件的处理。在这些函数的具体实现仅仅是简单的调用了fdevents结构体中的函数指针。而这写函数指针所对应的函数分别定义在以fdevent_开头的.c文件中。从这些文件的名字可以看出其所对应的IO系统。在这些文件中,函数大多是static,这就行当与类的私有函数,起到隐藏具体实现的效果。后面的问装中我们会具体的分析一个多路IO系统的使用。
lighttpd作者对fdevent系统封装相当的出彩,对于理解在C中使用面向对象的方式编程具有很好的帮助。
下一篇中将看一看fdevent系统的初始化和使用。

本文章由 http://www.wifidog.pro/2015/04/20/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E5%AF%B9%E5%A4%96%E6%8E%A5%E5%8F%A3.html 整理编辑,转载请注明出处

wifidog源码分析Lighttpd1.4.20源码分析之插件系统(3)---PLUGIN_TO_SLOT宏

前面讲了lighttpd插件系统的加载和初始化,这一篇中,将介绍一下plugin.c中的宏PLUGIN_TO_SLOT。
在将PLUGIN_TO_SLOT宏之前,我们先来看看lighttpd中插件系统的对外接口。这个接口所对的“外”指的是lighttpd服务器。前面已经提到,在运行的过程中,lighttpd不知道所加载的插件都是干什么用的,只知道这些插件所实现的接口,也就是在plugin结构体中那些函数指针有哪些对于某个插件是NULL,哪些是具体的函数地址。
既然lighttpd只知道这些,那么它又是怎样调用这些插件的呢?
答案就在plugin.h文件中的下面一系列函数声明:

handler_t plugins_call_handle_uri_raw(server * srv, connection * con);
handler_t plugins_call_handle_uri_clean(server * srv,connection * con);
handler_t plugins_call_handle_subrequest_start(server * srv,connection * con);
handler_t plugins_call_handle_subrequest(server * srv,connection * con);
handler_t plugins_call_handle_request_done(server * srv,connection * con);
handler_t plugins_call_handle_docroot(server * srv,connection * con);
handler_t plugins_call_handle_physical(server * srv,connection * con);
handler_t plugins_call_handle_connection_close(server * srv,connection * con);
handler_t plugins_call_handle_joblist(server * srv,connection * con);
handler_t plugins_call_connection_reset(server * srv,connection * con);
handler_t plugins_call_handle_trigger(server * srv);
handler_t plugins_call_handle_sighup(server * srv);
handler_t plugins_call_init(server * srv);
handler_t plugins_call_set_defaults(server * srv);
handler_t plugins_call_cleanup(server * srv);

这些函数就是插件系统对外的接口。在运行过程中,lighttpd靠调用上面的这些函数调用插件。比如:在server.c的main函数中,就调用了plugins_call_set_defaults函数:

if (HANDLER_GO_ON != plugins_call_set_defaults(srv))
    {
        log_error_write(srv, __FILE__, __LINE__, "s",
                "Configuration of plugins failed. Going down.");
        plugins_free(srv);
        network_close(srv);
        server_free(srv);
        return -1;
    }

如果你使用ctags+vim看代码,当在这些函数的调用处想跳转到定义时发现ctags没有找到这些函数的定义。难道这些函数没有实现?这显然是不会的。其实,这正是由于本文的重点───PLUGIN_TO_SLOT宏造成的。
打开plugin.c文件,会发现这几行代码:

PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_RAW, handle_uri_raw)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_REQUEST_DONE,handle_request_done)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_CONNECTION_CLOSE,handle_connection_close)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SUBREQUEST, handle_subrequest)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SUBREQUEST_START,handle_subrequest_start)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_JOBLIST, handle_joblist)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_DOCROOT, handle_docroot)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_PHYSICAL, handle_physical)
PLUGIN_TO_SLOT(PLUGIN_FUNC_CONNECTION_RESET, connection_reset)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_TRIGGER, handle_trigger)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SIGHUP, handle_sighup)
PLUGIN_TO_SLOT(PLUGIN_FUNC_CLEANUP, cleanup)
PLUGIN_TO_SLOT(PLUGIN_FUNC_SET_DEFAULTS, set_defaults)

再看看PLUGIN_SLOT宏的前两行:

#define PLUGIN_TO_SLOT(x, y) \
    handler_t plugins_call_##y(server *srv, connection *con) {\

这下明白了吧。上面那些函数是由这些宏调用模板化生成的。由于这些函数的代码具有很高的相似度(仅仅是调用的插件函数不同),通过宏模板进行生成,可以节约大量的代码,同时又不容易出错。这类似于C++中的模板。注:C语言预处理器运算符##为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与##相邻,则改参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果重新扫描。(摘自:C语言程序设计 K&R)在这里,plugins_call_和实参y拼接成函数名。
下面我们着重分析一下PLUGIN_SLOT宏的内容:

#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;\
    }

根据后面的宏调用可以看出,参数x是plugin_t枚举类型,y是plugin结构体中函数指针成名的名字。在宏调用中,x和y是相对应的。
PLUGIN_SLOT宏首先通过参数y拼接出函数名。如:这个宏调用
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean),拼接得到的函数名为plugins_call_handle_uri_clean,加上参数和返回值,正好是plugin.h中的函数handler_t plugins_call_handle_uri_clean(server * srv, connection * con)。其他的以此类推。
这条语句slot = ((plugin ***)(srv->plugin_slots))[x];通过宏参数x得到plugin_slots的第x列。plugin_slots的结构在前面的文章中已经讲解过了。不熟悉的读者可以再回头看看。这列中包含所有具有参数x所对应的功能的插件的指针。也就是,plugin结构体的成员变量y不为NULL的所有plugin实例的指针。接着,在for循环中,调用这些插件的y函数,就是这句:switch(r = p->y(srv, con, p->data))。后面就是一些返回值和错误处理了。
读者也许早就发现在plugin.c文件中有两个PLUGIN_SLOT宏。猛地一看没有任何差别。确实,着两个宏基本上都一样,只有一点不同:第二个宏的switch语句中调用y函数时,参数少了一个con:switch(r = p->y(srv, p->data))。这是他们唯一的差别。读者可以看看plugin结构体中的函数指针,有四个是两个参数的(server * srv, void *p_d),其他都是三个参数(server * srv, connection * con, void *p_d)。
在这里有一个很有意思的问题。我们注意到,在所有plugins_call_XXX函数中,由于都是通过上面的PLUGIN_SLOT宏生成的。那么,这些函数在被lighttpd进行调用的时候,无论来的请求想要干什么,lighttpd都会逐一调用所有插件对应的函数。这就有一个问题了:如果所调用的第一个插件所具有的功能不是这个连接所想要的功能,这不就出错了么?但是反过来想想,既然要隐藏插件的所有细节,那么lighttpd也就无从知道哪些插件是干什么的。因此,对与一个连接,lighttpd也就不会知道使用哪个插件进行处理。因此,这样做也是没办法的事情。虽然这样会影响服务器的效率,毕竟每次都调用了很多无用的函数,但却有助于服务器的扩展。效率换扩展性,关键要把握一个度。
那么lighttpd无法确定连接该用哪个插件来处理,那么插件自己就必须知道哪些连接是自己该处理的。这涉及到连接的细节处理,我们先暂且放一放。从下一篇开始,将讲解一下fd event系统,在分析具体的连接的处理时,在讲解这个问题。
下一篇,介绍lighttpd中的fdevents结构体和fd eve

本文章由 http://www.wifidog.pro/2015/04/17/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E6%8F%92%E4%BB%B6%E5%AE%8F%E5%AE%9A%E4%B9%89.html 整理编辑,转载请注明出处

wifidog认证源码分析Lighttpd1.4.20源码分析之插件系统(1)---plugin结构体和插件接口

在lighttpd中,使用插件的形式来增加服务的功能。同时,lighttpd提供了一个插件的公共接口给开发者,方便第三方提供额外的插件。Lighttpd的插件接口主要提供在plugin.h文件中。其中,plugin结构体是最核心的部分。
plugin结构体的定义如下:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 
typedef struct 
{
    size_t version;

    buffer *name;                /* name of the plugin */

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

    /*
     * is called ... 纯虚函数,在子类中要予以赋值。
     */
    handler_t(*handle_trigger) (server * srv, void *p_d);    /* once a second */
    handler_t(*handle_sighup) (server * srv, void *p_d);    /* at a signup */
    handler_t(*handle_uri_raw) (server * srv, connection * con, void *p_d); /* after uri_raw is set */
    handler_t(*handle_uri_clean) (server * srv, connection * con, void *p_d);/* after uri is set */
    handler_t(*handle_docroot) (server * srv, connection * con, void *p_d);    /* getting the document-root */
    handler_t(*handle_physical) (server * srv, connection * con, void *p_d);    /* mapping url to physical path */
    handler_t(*handle_request_done) (server * srv, connection * con, void *p_d);    /* at the end of a request */
    handler_t(*handle_connection_close) (server * srv, connection * con, void *p_d);    /* at the end of a connection */
    handler_t(*handle_joblist) (server * srv, connection * con, void *p_d);    /* after all events are handled */
    handler_t(*handle_subrequest_start) (server * srv, connection * con, void *p_d);

    /*
     * when a handler for the request has to be found 
     */
    handler_t(*handle_subrequest) (server * srv, connection * con, void *p_d);    /* */
    handler_t(*connection_reset) (server * srv, connection * con, void *p_d);    /* */
    void *data;

    /*
     * dlopen handle 
     */
    void *lib;
} plugin;

可以看出,在结构体plugin的设计中,作者使用了面向对象的思想。plugin结构体就是一个虚基类,其中的数据成员,如name,version等都是子类公有的。而随后的一系列函数指针则是虚函数,这些函数指针在plugin结构体中并没有进行赋值,要求所有的子类必须对其进行赋值。不同的子类对这些函数指针赋不同的值,在进行函数调用的时候就可以实现多态。
另外,c语言毕竟不支持面向对象,因此,在通过c实现面向对象的时候大多情况先是要靠人的理解,而不是语言上的约束。如,这里说plugin结构体是一个虚基类,实际上所有的子类都是这个结构体的实例,而子类的实例只有一个,也就是他自己。这就和C++中的子类不同了。
在plugin结构体中,version成员比较重要。很明显,这个成员标记这个插件的版本。在plugin结构体中定义的那一系列函数指针是插件的对外接口,也就是插件对lighttpd的接口,lighttpd只知道这些接口,通过调用这些接口来完成工作。随着lighttpd的不断改进,这些接口可能满足不了服务器的要求,因此要对其进行改进,这样就有可能造成以前开发的插件无法使用。通过version成员,在加载插件的时候判断这个插件是否符合当前服务器的版本,也就是接口是否相符。如果不相符,则不加载插件,这样就可以避免由于接口的不相符造成服务器的崩溃等问题。
这些函数指针在lighttpd的文档中被称作'hooks'。分为serverwide hooks和connectionwide hooks,serverwide hooks是有服务器调用的,主要处理一些初始化等辅助的工作,包括:init,cleanup, set_defaults, handle_trigger和handle_sighup。connectionwide hooks主要是面向连接的,在处理连接的时候调用这些hooks完成相应的工作。这些hooks大部分在函数http_response_prepare()中被调用。
至于这些hooks是在哪被调用,都完成哪些功能,在后面分析具体的插件的时候会详细介绍。有兴趣的读者可以阅读lighttpd源码包中doc文件夹下的plugins文件。
在plugin.h中,plugin结构体的定义后面还有一堆的函数声明:

int plugins_load(server * srv);
void plugins_free(server * srv);

这两个很明显是加载和释放插件函数。

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 handler_t plugins_call_handle_uri_raw(server * srv, connection * con);
handler_t plugins_call_handle_uri_clean(server * srv, connection * con);
handler_t plugins_call_handle_subrequest_start(server * srv, connection * con);
handler_t plugins_call_handle_subrequest(server * srv, connection * con);
handler_t plugins_call_handle_request_done(server * srv, connection * con);
handler_t plugins_call_handle_docroot(server * srv, connection * con);
handler_t plugins_call_handle_physical(server * srv, connection * con);
handler_t plugins_call_handle_connection_close(server * srv, connection * con);
handler_t plugins_call_handle_joblist(server * srv, connection * con);
handler_t plugins_call_connection_reset(server * srv, connection * con);

handler_t plugins_call_handle_trigger(server * srv);
handler_t plugins_call_handle_sighup(server * srv);

handler_t plugins_call_init(server * srv);
handler_t plugins_call_set_defaults(server * srv);
handler_t plugins_call_cleanup(server * srv);

这一系列的plugins_call_XXXXX函数则是插件对外的接口。也就是说,lighttpd服务器通过这些函数,调用插件进行工作。lighttpd在调用插件的时候并不知道到底调用的是哪些插件,而仅仅调用上面的函数。这些函数再调用相应的插件的函数,从而完成工作。具体怎么调用插件的函数,放在后面的文章中介绍。
最后面的config_XXXXXX函数是处理一些配置问题,暂不讨论。
在plugin.h文件中还定义了一些宏:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/--> 1 #define SERVER_FUNC(x) \
        static handler_t x(server *srv, void *p_d)
#define CONNECTION_FUNC(x) \
    static handler_t x(server *srv, connection *con, void *p_d)
#define INIT_FUNC(x)  static void *x()

#define FREE_FUNC          SERVER_FUNC
#define TRIGGER_FUNC       SERVER_FUNC
#define SETDEFAULTS_FUNC   SERVER_FUNC
#define SIGHUP_FUNC        SERVER_FUNC
#define SUBREQUEST_FUNC    CONNECTION_FUNC
#define JOBLIST_FUNC       CONNECTION_FUNC
#define PHYSICALPATH_FUNC  CONNECTION_FUNC
#define REQUESTDONE_FUNC   CONNECTION_FUNC
#define URIHANDLER_FUNC    CONNECTION_FUNC

前面的三个宏(SERVER_FUNC, CONNECTION_FUNC和INIT_FUNC)定义了函数签名的模板。后面的一系列宏和plugin结构体中的函数指针对应,确定这些函数指针所对应的函数签名。
在进行插件开发的时候,插件中的函数签名要使用上面的宏来生成。这样可以保证接口的统一。
  最后,还要提一下plugin.c文件中的结构体:

typedef struct 
{
    PLUGIN_DATA;
} plugin_data;

PLUGIN_DATA是一个宏,定义为:#define PLUGIN_DATA size_t id。
这个结构体用来存放插件所需要使用的数据,这个结构体作为plugin结构体中函数指针的最后一个参数:void p_d传入对应的函数中。在plugin.c结构体中plugin_data的定义很简单,仅仅包含一个数据成员id。在mod_.c/h文件中,同样也包含有plugin_data结构体的定义。如:mod_cgi.c中,

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 typedef struct {
    PLUGIN_DATA;
    buffer_pid_t cgi_pid;
    buffer *tmp_buf;
    buffer *parse_response;
    plugin_config **config_storage;
    plugin_config conf;
} plugin_data;

在mod_cml.h中:

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->1 typedef struct {
    PLUGIN_DATA;
    buffer *basedir;
    buffer *baseurl;
    buffer *trigger_handler;
    plugin_config **config_storage;
    plugin_config conf;
} plugin_data;

等等。
这些定义有一个共通的特点,那就是第一个成员都是PLUGIN_DATA。这又是一个技巧。所有这些plugin_data相当于是plugin.c中plugin_data的子类。这些子类开始的部分和父类相同,这就允许子类的指针转换成父类指针,然后再转换回去,并保证数据不会丢失。这样,lighttpd所面对的插件数据接口是plugin.c中定义的plugin_data,当lighttpd在调用插件中的函数,并把数据传进去的时候,插件可以再把数据的类型还原回去。这样,对于lighttpd,所面对的数据接口就只有一个,插件所需要的数据可以不对lighttpd公开,这就很好的隐藏了数据。同时也简化了lighttpd的复杂度,提高了程序的扩展性。

  下一篇中,将解释lighttpd中插件的加载和初始化。

本文章由 http://www.wifidog.pro/2015/04/16/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90Lighttpd-plugin%E7%BB%93%E6%9E%84%E4%BD%93%E5%92%8C%E6%8F%92%E4%BB%B6%E6%8E%A5%E5%8F%A3-1.html 整理编辑,转载请注明出处

wifidog认证源码分析lighttpd1.4.20源码分析 -----工作模型

lighttpd的工作模型很简单──一个主进程加多个工作进程的多进程模型,也就是所谓的watcher-worker模型。
  整个程序的入口(main函数)在server.c文件中。在main函数的开始部分必然是处理参数和各种繁杂的初始化工作。其中有两个地方要重点看一起。第一个是下面的语句:

if (test_config) //没有进行任何测试。。。
{
     printf("Syntax OK\n");
}

这个If语句是为了判断配置文件的语法是否合法。但是,明显,没有进行任何的测试,而仅仅是输出了一句话。
  第二个是函数daemonize()。这个函数的作用是使程序进入daemon。函数的详细内容如下:

static void daemonize(void)
{
        /*
         * 忽略和终端读写有关的信号
         */
#ifdef SIGTTOU
        signal(SIGTTOU, SIG_IGN);
#endif    
#ifdef SIGTTIN
        signal(SIGTTIN, SIG_IGN);
#endif
#ifdef SIGTSTP
        signal(SIGTSTP, SIG_IGN);
#endif
        if (0 != fork()) /* 产生子进程,父进程退出 */
            exit(0);
        if (-1 == setsid())/* 设置子进程的设置ID */
            exit(0);
        signal(SIGHUP, SIG_IGN);/* 忽略SIGHUP信号 */
        if (0 != fork())/* 再次产生子进程,父进程退出 */
            exit(0);
        if (0 != chdir("/"))/* 更改工作目录为根目录 */
            exit(0);
}

  这里作者使用了标准的产生*nix daemon的方法。两次调用fork并退出父进程。具体的原因读者可以参阅《Unix环境高级编程》(APUE)中有关daemon的讲解部分。
  顺着main函数继续向下走,沿途的各种初始化工作尽可忽略。下面的语句是本文的重点!

* 下面程序将产生多个子进程。这些子进程成为worker,
     * 也就是用于接受处理用户的连接的进程。而当前的主进程将
     * 成为watcher,主要工作就是监视workers的工作状态,
     * 当有worker因为意外而退出时,产生新的worker。
     * 在程序退出时,watcher负责停止所有的workers并清理资源。
     */
        int child = 0; //用来标记这个进程是子进程还是父进程。
           //当子进程返回到这个while循环的开始的时候,由于标记
        //进程是子进程,流程直接跳出while循环执行后面的程序。
        //而对于父进程,则继续产生子进程。
        while (!child && !srv_shutdown && !graceful_shutdown)
        {
            if (num_childs > 0) //watcher继续产生worker
            {
                switch (fork())
                {
                case -1:    //出错
                    return -1;
                case 0:     //子进程进入这个case
                    child = 1;
                    break;
                default:    //父进程进入这个case
                    num_childs--;
                    break;
                }
            } 
            else         //watcher
            {
                /**
                 * 当产生了足够的worker时,watcher就在这个while
                 * 中不断的循环。
                 * 一但发现有worker退出(进程死亡),立即产生新的worker。
                 * 如果发生错误并接受到SIGHUP信号,向所有的进程
                 *(父进程及其子进程)包括自己发送SIGHUP信号。
                 * 并退出。
                 */
                int status;

                if (-1 != wait(&status))
                {
                    /** 
                     * one of our workers went away 
                     */
                    num_childs++;
                } 
                else
                {
                    switch (errno)
                    {
                    case EINTR:
                        /**
                         * if we receive a SIGHUP we have to close our 
                         * logs ourself as we don't 
                         * have the mainloop who can help us here
                         */
                        if (handle_sig_hup)
                        {
                            handle_sig_hup = 0;
                            log_error_cycle(srv);
                            /**
                             * forward to all procs in the process-group
                             * 向所有进程发送SIGHUP信号。(父进程及其子进程)
                             * we also send it ourself
                             */
                            if (!forwarded_sig_hup)
                            {
                                forwarded_sig_hup = 1;
                                kill(0, SIGHUP);
                            }
                        }
                        break;
                    default:
                        break;
                    }end of switch (errno)...
                }//end of if (-1 != wait(&status)) ...
            }//end of if (num_childs > 0)...
        }// end of while(!child...

在正常的运行过程中,watcher进程是不会退出上面的while循环。一旦退出了这个循环,那么也就意为着整个程序退出了。
另外,woker的数量可以在配置文件中进行配置。
子进程,也就是worker退出了上面的while循环后就开始处理连接请求等各种工作。
在子进程的一开始,还是各种初始化工作,包括fd时间处理器的初始化(fdevent_init(srv->max_fds + 1, srv->event_handler)),stat cache初始化(stat_cache_init())等。子进程工作在一个大while循环中。
while的工作流程如下:
1、判断连接是否断开。如果断开,则调用处理程序进行处理并重新开始新一轮的日志记录。
2、判断是否接受到了alarm函数发出的信号。接受到信号后,判断服务器记录的时间是否和当前时间相同。如果相同,说明时间还没有过一秒,继续处理连接请求。如果不相同,则时间已经过了一秒。那么,服务器则触发插件,清理超时连接,清理stat-cache缓存。这理里面最重要的是处理超时连接。程序中通过一个for循环查询所有的连接,比较其idle的时间和允许的最大idle时间来判断连接是否超时。如果连接超时,则让连接进入出错的状态(connection_set_state(srv, con, CON_STATE_ERROR);)。
3、判断服务器socket连接是否失效。如果失效了,则在不是服务器过载的情况下将所有连接重新加入的fdevent中。为什么服务器socket会失效呢?可以看到,在后面判断出服务器过载后,即标记了socket连接失效。srv->sockets_disabled = 1;
4、如果socket没有失效,判断服务器是否过载。如果过载了,则关闭所有连接,清理服务器并退出服务器。
5、分配文件描述符。
6、启动事件轮询。等待各种IO时间的发生。包括文件读写,socket请求等。
7、一旦有事件发生,调用相应的处理函数进行处理。
8、最后,检查joblist中是否有未处理的job并处理之。
至此,一次循环结束了。然后,从头开始继续循环直到服务器关闭。

在处理IO事件的时候,程序进入下面的循环:

do
{
        fdevent_handler handler; //事件处理函数指针
        void *context;
        handler_t r;
        //获得下一个事件的标号
        fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
        //获得这个事件的具体类型。
revents = fdevent_event_get_revent(srv->ev, fd_ndx);
        //获得事件对应的文件描述符
fd = fdevent_event_get_fd(srv->ev, fd_ndx);
        //获得事件处理函数的指针
handler = fdevent_get_handler(srv->ev, fd);
        //获得事件的环境
context = fdevent_get_context(srv->ev, fd);
        /**
         * 这里,调用请求的处理函数handler处理请求!
         * 这才是重点中的重点!!
         */
        switch (r = (*handler) (srv, context, revents))
        {
        case HANDLER_FINISHED:
        case HANDLER_GO_ON:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_WAIT_FOR_FD:
            break;
        case HANDLER_ERROR:
            /*
             * should never happen 
             */
            SEGFAULT();
            break;
        default:
            log_error_write(srv, __FILE__, __LINE__, "d", r);
            break;
        }
    }while (—n > 0);

这个循环是worker进程的核心部分。这里由于作者对IO系统的出色的封装。我们不须要理解这些函数的作用就可知道连接的处理流程。在程序中,作者使用回调函数轻松的解决掉了处理工种事件时判断处理函数的问题。代码优雅而高效。在lighttpd中,作者使用了大量的回调函数,这使得代码清晰易懂,效率也很高。
  有一点值得注意。程序在处理连接超时的时候是每一秒中轮询所有的连接,判断其是否超时。这必然会降低服务器的效率。在实际的运行中,确实会对lighttpd的效率造成一定的影响。
  lighttpd使用的watcher-worker模型虽然简单,但是在实际的运行过程中也非常的高效。有的时候,简单并不一定就不高效。

本文章由 http://www.wifidog.pro/2015/04/16/wifidog%E8%AE%A4%E8%AF%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%9E%8B.html 整理编辑,转载请注明出处