那谁的技术博客

感兴趣领域:高性能服务器编程,存储,算法,Linux内核
随笔 - 210, 文章 - 0, 评论 - 1183, 引用 - 0
数据加载中……

lighttpd1.4.18代码分析(四)--处理监听fd的流程

前面介绍了lighttpd使用的watcher-worker模型, 它对IO事件处理的封装, 现在可以把这些结合起来看看这大概的流程.

首先, 服务器创建监听socket, 然后在server.c中调用函数network_register_fdevents将监听socket注册到IO事件处理器中:
int network_register_fdevents(server *srv) {
    size_t i;

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

    
/* register fdevents after reset */
    
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_add(srv
->ev, &(srv_socket->fde_ndx), srv_socket->fd, FDEVENT_IN);
    }
    
return 0;
}
在这里, 调用函数fdevent_register注册fd到IO事件处理器中, 对于服务器监听fd而言,
它在fdnode中的回调函数handler是函数network_server_handle_fdevent, 而ctx则是srv_socket.
接着调用函数fdevent_event_add, 其中传入的第三个参数是FDEVENT_IN, 也就是当该fd上有可读数据时触发调用, 对于所有监听的fd而言,
有可读事件就意味着有新的连接到达.

然后服务器创建子进程worker, 服务器父进程自己成为watcher, 自此下面的工作由子进程进行处理,
每个子进程所完成的工作都是一样的.有的书上说有多个进程在等待accept连接的时候会造成所谓的惊群现象,在lighttpd的代码中,
没有看到在accaept之前进行加锁操作, 这是否会造成惊群不得而知.

现在, 在IO事件处理器中仅有一个fd等待触发, 就是前面注册的监听fd, 我们看看当一个连接到来的时候处理的流程, 首先看我们曾经说过的
轮询fd进行处理的主循环:
        // 轮询FD
        if ((n = fdevent_poll(srv->ev, 1000)) > 0) {
            
/* n is the number of events */
            
int revents;
            
int fd_ndx;

            fd_ndx 
= -1;
            
do {
                fdevent_handler handler;
                
void *context;
                handler_t r;

                
// 获得处理这些事件的函数指针 fd等

                
// 获得下一个fd在fdarray中的索引
                fd_ndx  = fdevent_event_next_fdndx (srv->ev, fd_ndx);
                
// 获得这个fd要处理的事件类型
                revents = fdevent_event_get_revent (srv->ev, fd_ndx);
                
// 获取fd
                fd      = fdevent_event_get_fd     (srv->ev, fd_ndx);
                
// 获取回调函数
                handler = fdevent_get_handler(srv->ev, fd);
                
// 获取处理相关的context(对server是server_socket指针, 对client是connection指针)
                context = fdevent_get_context(srv->ev, fd);

                
/* connection_handle_fdevent needs a joblist_append */
                
// 进行处理
                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 (--> 0);
当一个连接到来的时候, 调用fdevent_poll返回值是1, 因为这个函数的返回值表示的是有多少网络IO事件被触发了, 接着由于n>0, 进入循环中
获得被触发的fd, 回调函数, 以及ctx指针, 在这里由于是监听fd被触发, 那么返回的回调函数是前面提到的network_server_handle_fdevent,
接着就要调用这个函数处理IO事件了:
// 这个函数是处理server事件的函数, 与connection_handle_fdevent对应
handler_t network_server_handle_fdevent(void *s, void *context, int revents) {
    server     
*srv = (server *)s;
    server_socket 
*srv_socket = (server_socket *)context;
    connection 
*con;
    
int loops = 0;

    UNUSED(context);

    
if (revents != FDEVENT_IN) {
        log_error_write(srv, __FILE__, __LINE__, 
"sdd",
                
"strange event for server socket",
                srv_socket
->fd,
                revents);
        
return HANDLER_ERROR;
    }

    
/* accept()s at most 100 connections directly
     *
     * we jump out after 100 to give the waiting connections a chance 
*/
    
// 一次最多接受100个链接
    for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) {
        handler_t r;

        
// 这里马上进入状态机中进行处理仅仅对应状态为CON_STATE_REQUEST_START这一段
        
// 也就是保存连接的时间以及设置一些计数罢了
        connection_state_machine(srv, con);

        
switch(r = plugins_call_handle_joblist(srv, con)) {
        
case HANDLER_FINISHED:
        
case HANDLER_GO_ON:
            
break;
        
default:
            log_error_write(srv, __FILE__, __LINE__, 
"d", r);
            
break;
        }
    }
    
return HANDLER_GO_ON;
}
我给这段代码加了一些注释, 有几个地方做一些解释:
1)UNUSED(context)是一个宏, 扩展开来就是( (void)(context) ), 实际上是一段看似无用的代码, 因为没有起什么明显的作用, 是一句废话,
在这个函数中, 实际上没有使用到参数context, 如果在比较严格的编译器中, 这样无用的参数会产生一条警告, 说有一个参数没有使用到, 加上了
这么一句无用的语句, 就可以避免这个警告.那么, 有人就会问了, 为什么要传入这么一个无用的参数呢?回答是, 为了满足这个接口的需求,
来看看回调函数的类型定义:
typedef handler_t (*fdevent_handler)(void *srv, void *ctx, int revents);
这个函数指针要求的第二个参数是一个ctx指针, 对于监听fd的回调函数network_server_handle_fdevent而言, 它是无用的, 但是对于处理连接fd
的回调函数而言, 这个指针是有用的.

2) 在函数的前面, 首先要判断传入的event事件是否是FDEVENT_IN, 也就是说, 只可能在fd有可读数据的时候才触发该函数, 其它的情况都是错误.

3)函数在最后进入一个循环, 循环的最多次数是100次, 并且当connection_accept函数返回NULL时也终止循环, 也就是说, 当监听fd被触发时,
服务器尽量的去接收新的连接, 最多接收100个新连接, 这样有一个好处, 假如服务器监听fd是每次触发只接收一个新的连接, 那么效率是比较低的,
不如每次被触发的时候"尽力"的去接收, 一直到接收了100个新的连接或者没有可接收的连接之后才返回.接着来看看负责接收新连接的函数
connection_accept做了什么:
// 接收一个新的连接
connection *connection_accept(server *srv, server_socket *srv_socket) {
    
/* accept everything */

    
/* search an empty place */
    
int cnt;
    sock_addr cnt_addr;
    socklen_t cnt_len;
    
/* accept it and register the fd */

    
/**
     * check if we can still open a new connections
     *
     * see #1216
     
*/

    
// 如果正在使用的连接数大于最大连接数 就返回NULL
    if (srv->conns->used >= srv->max_conns) {
        
return NULL;
    }

    cnt_len 
= sizeof(cnt_addr);

    
if (-1 == (cnt = accept(srv_socket->fd, (struct sockaddr *&cnt_addr, &cnt_len))) {
        
switch (errno) {
        
case EAGAIN:
#if EWOULDBLOCK != EAGAIN
        
case EWOULDBLOCK:
#endif
        
case EINTR:
            
/* we were stopped _before_ we had a connection */
        
case ECONNABORTED: /* this is a FreeBSD thingy */
            
/* we were stopped _after_ we had a connection */
            
break;
        
case EMFILE:
            
/* out of fds */
            
break;
        
default:
            log_error_write(srv, __FILE__, __LINE__, 
"ssd""accept failed:", strerror(errno), errno);
        }
        
return NULL;
    } 
else {
        connection 
*con;

        
// 当前使用的fd数量+1
        srv->cur_fds++;

        
/* ok, we have the connection, register it */
        
// 打开的connection+1(这个成员貌似没有用)
        srv->con_opened++;

        
// 获取一个新的connection
        con = connections_get_new_connection(srv);

        
// 保存接收到的fd
        con->fd = cnt;
        
// 索引为-1
        con->fde_ndx = -1;
#if 0
        gettimeofday(
&(con->start_tv), NULL);
#endif
        
// 注册函数指针和connection指针
        fdevent_register(srv->ev, con->fd, connection_handle_fdevent, con);

        
// 状态为可以接收请求
        connection_set_state(srv, con, CON_STATE_REQUEST_START);

        
// 保存接收连接的时间
        con->connection_start = srv->cur_ts;
        
// 保存目标地址
        con->dst_addr = cnt_addr;
        buffer_copy_string(con
->dst_addr_buf, inet_ntop_cache_get_ip(srv, &(con->dst_addr)));
        
// 保存server_socket指针
        con->srv_socket = srv_socket;

        
// 设置一下接收来的FD, 设置为非阻塞
        if (-1 == (fdevent_fcntl_set(srv->ev, con->fd))) {
            log_error_write(srv, __FILE__, __LINE__, 
"ss""fcntl failed: ", strerror(errno));
            
return NULL;
        }
#ifdef USE_OPENSSL
        
/* connect FD to SSL */
        
if (srv_socket->is_ssl) {
            
if (NULL == (con->ssl = SSL_new(srv_socket->ssl_ctx))) {
                log_error_write(srv, __FILE__, __LINE__, 
"ss""SSL:",
                        ERR_error_string(ERR_get_error(), NULL));

                
return NULL;
            }

            SSL_set_accept_state(con
->ssl);
            con
->conf.is_ssl=1;

            
if (1 != (SSL_set_fd(con->ssl, cnt))) {
                log_error_write(srv, __FILE__, __LINE__, 
"ss""SSL:",
                        ERR_error_string(ERR_get_error(), NULL));
                
return NULL;
            }
        }
#endif
        
return con;
    }
}
抛开出错处理这部分不解释, 一旦出错, 就返回NULL指针, 这时可以终止上面那个循环接收新连接的过程,下面重点看看接收了一个新的连接之后需要
做哪些事情, 在上面的代码中我加了一些简单的注释, 下面加一些更加详细些的解释:
1)要将服务器已经接收的fd数量(成员cur_fds)加一, 这个数量用于判断是否可以接收新的连接的, 超过一定的数量时, 服务器就暂停接收,
等一些fd释放之后才能继续接收

2) 调用函数connections_get_new_connection返回一个connection指针, 用于保存新到的连接, 获得这个指针之后要保存接收这个连接的时间
(成员connection_start中), 保存新到连接的地址(成员dst_addr和dst_addr_buf中), 此外还要保存一个server指针, 并且调用函数fdevent_fcntl_set
将该fd设置为非阻塞的, 最后别忘了要调用fdevent_register函数将该fd注册到IO事件处理器中, 另外该fd当前的状态通过connection_set_state设置为
CON_STATE_REQUEST_START, 这是后面进入状态机处理连接的基础.

了解了这个函数的处理过程, 回头看看上面的循环:
    for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) {
        handler_t r;

        
// 这里马上进入状态机中进行处理仅仅对应状态为CON_STATE_REQUEST_START这一段
        
// 也就是保存连接的时间以及设置一些计数罢了
        connection_state_machine(srv, con);

        
switch(r = plugins_call_handle_joblist(srv, con)) {
        
case HANDLER_FINISHED:
        
case HANDLER_GO_ON:
            
break;
        
default:
            log_error_write(srv, __FILE__, __LINE__, 
"d", r);
            
break;
        }
    }
我们已经分析完了函数connection_accept, 当一个新的连接调用这个函数成功返回的时候, 这个循环执行函数connection_state_machine
进行处理.这是一个非常关键的函数, 可以说, 我们后面讲解lighttpd的很多笔墨都将花费在这个函数上, 这也是我认为lighttpd实现中最精妙的
地方之一, 在这里我们先不进行讲解, 你所需要知道的是, 在这里, connection_state_machine调用了函数fdevent_event_add, 传入的事件参数仍然是
FDEVENT_IN, 也就是说, 对于新加入的fd, 它所首先关注的IO事件也是可读事件.

我们大体理一理上面的流程, 省略去对watcher-worker模型的描述:
创建服务器监听fd-->
调用fdevent_register函数将监听fd注册到IO事件处理器中-->
调用fdevent_event_add函数添加FDEVENT_IN到监听fd所关注的事件中-->

当一个新的连接到来时:
IO事件处理器轮询返回一个>0的值-->
IO事件处理返回被触发的fd, 回调函数, ctx指针,在这里就是监听fd,回调函数则是network_server_handle_fdevent->
调用监听fd注册的回调函数network_server_handle_fdevent-->
network_server_handle_fdevent函数尽力接收新的连接, 除非已经接收了100个新连接, 或者没有新连接到来-->
对于新到来的连接, 同样是调用fdevent_register函数将它注册到IO事件处理器中, 同样调用fdevent_event_add函数添加该fd所关注的事件是FDEVENT_IN

以上, 就是lighttpd监听fd处理新连接的大体流程.
我们知道, fd分为两种:一种是服务器自己创建的监听fd, 负责监听端口, 接收新到来的连接;
另一种, 就是由监听fd调用accept函数返回的连接fd, 这两种fd在处理时都会注册到IO事件处理器中(调用fdevent_register函数),
同时添加它们所关注的IO事件(可读/可写等)(调用fdevent_event_add函数).

也就是说,对IO事件处理器而言, 它并不关注所处理的fd是什么类型的, 你要使用它, 那么就把你的fd以及它的回调函数注册到其中, 同时添加你所关注的IO事件是什么, 当一个fd所关注的IO事件被触发时, IO事件处理器自动会根据你所注册的回调函数进行回调处理, 这是关键点, 如果你没有明白, 请回头看看前面提到的IO事件处理器.

这些的基础就是我们前面提到IO事件处理器, 前面我们提到过, lighttpd对IO事件处理的封装很漂亮, 每个具体实现都按照接口的规范进行处理.
我们在讲解时, 也没有涉及到任何一个具体实现的细节, 这也是因为lighttpd的封装很好, 以至于我们只需要了解它们对外的接口不需要深入细节就
可以明白其运行的原理.在本节中, 我们结合IO事件处理器, 对上面提到的第一种fd也就是监听fd的处理流程做了介绍, 在后面的内容中, 将重点讲解对
连接fd的处理.


posted on 2008-09-03 11:17 那谁 阅读(3742) 评论(3)  编辑 收藏 引用 所属分类: 网络编程服务器设计Linux/Unixlighttpd

评论

# re: lighttpd1.4.18代码分析(四)--处理监听fd的流程  回复  更多评论   

mark 有时间好好研究!
2008-09-03 18:43 | 浪迹天涯

# re: lighttpd1.4.18代码分析(四)--处理监听fd的流程  回复  更多评论   

写得不错
补充一下,在c++中无作用的语句(像上面那个拙劣的UNUSED宏)是会有警告的(C不会?),一个可选的做法是把形参的名字注释掉,如
void foo( int /*arg*/ ){
}
甚至可以
void foo( int /*arg*/ = 0 ){
}
一个实用的例子可以参见SGI版STL的std::allocator<T>::dealloate()的实现
2008-09-08 00:48 | 踏雪赤兔

# re: lighttpd1.4.18代码分析(四)--处理监听fd的流程  回复  更多评论   

@踏雪赤兔
兄台为什么说那个是拙劣的宏呢?
2011-04-19 16:27 | zhanglistar

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理