陈硕的《Linux 多线程服务端编程》一书中有这样一段描述:
在高性能的网络程序中,使用得最广泛的编程模型当属 “non-blocking IO + IO multiplexing”,即 Reactor 模式。在这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑。Reactor 模型的有优点很明显,编程不难,效率也不错,对于 IO 密集型应用是个不错的选择。Lighttpd 就是这样,它内部的 fdevents 结构十分精妙,值得学习。
就我了解,在高性能 Web 服务器中,Lighttpd 的应用并不是特别广泛,占据统治地位的是 Nginx。工作中基于 Lighttpd 开发很大程度上是由于历史技术选型的原因。不过,看过它的源码之后,发现在设计方面的确十分精妙,之前分析过它的 plugin 机制,这次就来分析一下它的 fdevents
结构。
先看 fdevents
结构体的定义,在 fdevent.h 中:
typedef struct _fdnode { |
注意,因为 IO 多路复用的机制有很多种,如 poll、select、epoll、kqueue 等,fdevent 使用预编译宏控制是否包含支持某些多路复用机制的成员变量,例如代码中的 #ifdef USE_LINUX_EPOLL ... # endif
块,这里为了清晰起见,只保留了 epoll 相关的宏及成员变量。类似之前分析的 plugin 结构体的定义,不同的多路复用机制可以为后面的函数指针赋予自己的实现。
接下来看 fdevents
是如何被初始化的,在 server.c 中调用了 fdevent_init()
函数,下面追踪一下源码:
// fdevent.c |
同样对源码做了一定的删改,只保留 epoll 部分,用户可以在配置文件中指定事件处理器(IO 多路复用)的类型,如果不指定则根据 srv->event_handler = event_handlers[0].et;
可知会使用 epoll。先看 fdevent_init()
函数,只是为变量申请内存,然后调用了 fdevent_linux_sysepoll_init()
函数。在后者中,主要是为 fdevents
结构体中的函数指针赋予了 epoll 自己的实现,这些实现都定义在 fdevent_linux_sysepoll.h 文件中。
现在结构已经初始化完毕,接下来看 Lighttpd 是怎么使用它的,还是回到 server.c 中,一直跟踪代码发现在 network_init()
函数中完成了 server_socket
的创建,然后在 network_register_fdevents()
中把 server_socket
注册到 fdevents
:
// network.c |
上面的代码给 server_socket
注册了一个叫作 network_server_handle_fdevent
的回调函数,然后监听 FDEVENT_IN 事件,这个函数暂且放一放,我们回到 server.c 看 Lighttpd 的事件循环:
int main (int argc, char **argv) { |
外层的 while 循环是 worker 进程的事件循环,fdevent_poll()
函数等待 IO 事件发生,如果没有 IO 事件,程序会阻塞在这个函数中。如果有 fd 发生了 IO 事件,则从该函数中返回,返回值是发生了 IO 事件的 fd 的数量。接着,程序进入 do-while 循环,循环中对每个 fd,调用一系列 fdevents
的接口函数获取 revents、fd、handler 和 context,最后调用 handler 处理 IO 事件。前面给 server_socket
注册了一个叫作 network_server_handle_fdevent
的回调函数,现在我们来看这个函数是怎么处理连接建立事件的:
static handler_t network_server_handle_fdevent(server *srv, void *context, int revents) { |
在这个函数中有个循环,尝试连续建立了 100 个连接,这样可以提高效率。connection_accept()
函数接受连接请求并返回一个 connection
结构体指针,接着对这个连接启动状态机,即进入 connection_state_machine()
函数。如果可以建立的连接少于 100 个,进程也不会阻塞,因为 server_socket
是非阻塞式的,在调用 accept()
函数时如果没有连接请求会直接返回错误出,这时 connection_accept()
会返回 NULL,从而退出 for 循环。到这里,建立连接的事件循环就完成了,状态机等细节以后再做分析。