Lighttpd 之 fdevent 分析

陈硕的《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 {
fdevent_handler handler; // 回调函数
void *ctx;
void *handler_ctx;
int fd; // 文件描述符
int events; // 关注的事件
} fdnode;

typedef struct fdevents {
struct server *srv;
fdevent_handler_t type; // 多路复用的类型

fdnode **fdarray; // 文件描述符列表
size_t maxfds; // 最大 fd 数目

#ifdef USE_LINUX_EPOLL
int epoll_fd; // epoll 实例的 fd
struct epoll_event *epoll_events;
#endif

int (*reset)(struct fdevents *ev);
void (*free)(struct fdevents *ev);

int (*event_set)(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 多路复用的机制有很多种,如 poll、select、epoll、kqueue 等,fdevent 使用预编译宏控制是否包含支持某些多路复用机制的成员变量,例如代码中的 #ifdef USE_LINUX_EPOLL ... # endif 块,这里为了清晰起见,只保留了 epoll 相关的宏及成员变量。类似之前分析的 plugin 结构体的定义,不同的多路复用机制可以为后面的函数指针赋予自己的实现。
接下来看 fdevents 是如何被初始化的,在 server.c 中调用了 fdevent_init() 函数,下面追踪一下源码:

// fdevent.c
fdevents *fdevent_init(server *srv, size_t maxfds, fdevent_handler_t type) {
fdevents *ev;

ev = calloc(1, sizeof(*ev));
ev->srv = srv;
ev->fdarray = calloc(maxfds, sizeof(*ev->fdarray));
ev->maxfds = maxfds;

if (0 != fdevent_linux_sysepoll_init(ev)) {
...
return NULL;
}

return ev;
}

// fdevent_linux_sysepoll.h
int fdevent_linux_sysepoll_init(fdevents *ev) {
ev->type = FDEVENT_HANDLER_LINUX_SYSEPOLL;
#define SET(x) \
ev->x = fdevent_linux_sysepoll_##x;

SET(free);
SET(poll);

SET(event_del);
SET(event_set);

SET(event_next_fdndx);
SET(event_get_fd);
SET(event_get_revent);

if (-1 == (ev->epoll_fd = epoll_create(ev->maxfds))) {
...
return -1;
}

if (-1 == fcntl(ev->epoll_fd, F_SETFD, FD_CLOEXEC)) {
...
close(ev->epoll_fd);
return -1;
}

ev->epoll_events = malloc(ev->maxfds * sizeof(*ev->epoll_events));

return 0;
}

同样对源码做了一定的删改,只保留 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
int network_register_fdevents(server *srv) {
size_t i;

if (-1 == fdevent_reset(srv->ev)) {
return -1;
}

for (i = 0; i < srv->srv_sockets.used; i++) {
server_socket *srv_socket = srv->srv_sockets.ptr[i];
fdevent_register(srv->ev, srv_socket->fd, network_server_handle_fdevent, srv_socket);
fdevent_event_set(srv->ev, &(srv_socket->fde_ndx), srv_socket->fd, FDEVENT_IN);
}

return 0;
}

// fdevent.c
int fdevent_register(fdevents *ev, int fd, fdevent_handler handler, void *ctx) {
fdnode *fdn;

fdn = fdnode_init();
fdn->handler = handler;
fdn->fd = fd;
fdn->ctx = ctx;
fdn->handler_ctx = NULL;
fdn->events = 0;

ev->fdarray[fd] = fdn;

return 0;
}

// fdevent.c
int fdevent_event_set(fdevents *ev, int *fde_ndx, int fd, int events) {
int fde = fde_ndx ? *fde_ndx : -1;

if (ev->event_set) fde = ev->event_set(ev, fde, fd, events);
ev->fdarray[fd]->events = events;

if (fde_ndx) *fde_ndx = fde;

return 0;
}

// fdevent_linux_sysepoll.h
static int fdevent_linux_sysepoll_event_set(fdevents *ev, int fde_ndx, int fd, int events) {
struct epoll_event ep;
int add = 0;

if (fde_ndx == -1) add = 1;

memset(&ep, 0, sizeof(ep));

ep.events = 0;

if (events & FDEVENT_IN) ep.events |= EPOLLIN;
if (events & FDEVENT_OUT) ep.events |= EPOLLOUT;

ep.events |= EPOLLERR | EPOLLHUP;

ep.data.ptr = NULL;
ep.data.fd = fd;

if (0 != epoll_ctl(ev->epoll_fd, add ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, &ep)) {
...
SEGFAULT();
return 0;
}

return fd;
}

上面的代码给 server_socket 注册了一个叫作 network_server_handle_fdevent 的回调函数,然后监听 FDEVENT_IN 事件,这个函数暂且放一放,我们回到 server.c 看 Lighttpd 的事件循环:

int main (int argc, char **argv) {
...
while (!srv_shutdown) {
...
if ((n = fdevent_poll(srv->ev, 1000)) > 0) {
int revents;
int fd_ndx;
fd_ndx = -1;

do {
fdevent_handler handler;
void *context;
handler_t r;

fd_ndx = fdevent_event_next_fdndx (srv->ev, fd_ndx);
if (-1 == fd_ndx) break;

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);

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:
SEGFAULT();
break;
default:
...
break;
}
} while (--n > 0);
}
...
}
...
}

外层的 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) {
server_socket *srv_socket = (server_socket *)context;
connection *con;
int loops = 0;

UNUSED(context);

if (0 == (revents & FDEVENT_IN)) {
...
return HANDLER_ERROR;
}

for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) {
handler_t r;

connection_state_machine(srv, con);

switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
...
break;
}
}

return HANDLER_GO_ON;
}

在这个函数中有个循环,尝试连续建立了 100 个连接,这样可以提高效率。connection_accept() 函数接受连接请求并返回一个 connection 结构体指针,接着对这个连接启动状态机,即进入 connection_state_machine() 函数。如果可以建立的连接少于 100 个,进程也不会阻塞,因为 server_socket 是非阻塞式的,在调用 accept() 函数时如果没有连接请求会直接返回错误出,这时 connection_accept() 会返回 NULL,从而退出 for 循环。到这里,建立连接的事件循环就完成了,状态机等细节以后再做分析。

0%