前面讲了lighttpd的fdevent系统的初始化过程。这篇要看一看lighttpd是怎样使用fdevent系统的。讲解的过程中,会详细的分析fdevent的源代码。
首先还是从server.c的main函数入手。在程序的初始化过程中,当完成fdevent的初始化之后,第一个需要fdevent处理的事情就是将在初始化网络的过程中得到的监听fd(socket函数的返回值)注册的fdevent系统中。调用的是network_register_fdevents()函数,定义在network.c文件中:
1 /* *
2 * 在fd events系统中注册监听socket。
3 * 这个函数在子进程中被调用。
4 */
5 int network_register_fdevents(server * srv)
6 {
7 size_t i;
8 if ( - 1 == fdevent_reset(srv -> ev)){ return - 1 ;}
9 /*
10 * register fdevents after reset
11 */
12 for (i = 0 ; i < srv -> srv_sockets.used; i ++ )
13 {
14 server_socket * srv_socket = srv -> srv_sockets.ptr[i];
15 fdevent_register(srv -> ev, srv_socket -> fd, network_server_handle_fdevent, srv_socket);
16 fdevent_event_add(srv -> ev, & (srv_socket -> fde_ndx), srv_socket -> fd, FDEVENT_IN);
17 }
18 return 0 ;
19 }
函数的重点是for循环,它遍历所有的监听fd并将其注册到fdevent系统中。在初始化网络的过程中,调用socket函数之后,将其返回值(监听fd)保存在server结构体的srv_sockets成员中,这个成员是一个server_socket_array结构体,而server_socket_array结构体是server_socket结构体的指针数组。server_socket结构体定义如下:
1 typedef struct
2 {
3 sock_addr addr; // socket fd对应的的地址。
4 int fd; // socket()函数返回的监听fd
5 int fde_ndx; // 和fd相同。
6 buffer * ssl_pemfile;
7 buffer * ssl_ca_file;
8 buffer * ssl_cipher_list;
9 unsigned short ssl_use_sslv2;
10 unsigned short use_ipv6; // 标记是否使用ipv6
11 unsigned short is_ssl;
12 buffer * srv_token;
13 #ifdef USE_OPENSSL
14 SSL_CTX * ssl_ctx;
15 #endif
16 unsigned short is_proxy_ssl;
17 } server_socket;
这里我们主要看前三个成员,前两个成员很简单,对于第三个成员,作者的本意应该是保存fd对应的fdnode在fdevents结构体中fdarray数组中的下标,但是程序在存储fdnode时候是以fd最为下标存储的(后面的fdevent_register函数中),所以通常fde_ndx==fd。
下面看一看fdevent_register()函数,在fdevent.c中定义:
1 int fdevent_register(fdevents * ev, int fd, fdevent_handler handler, void * ctx)
2 {
3 fdnode * fdn;
4 fdn = fdnode_init();
5 fdn -> handler = handler;
6 fdn -> fd = fd;
7 fdn -> ctx = ctx;
8 ev -> fdarray[fd] = fdn; // 使用文件描述符作为数组的下标。可以将查询
9 // 的时间变为 O(1)
10 return 0 ;
11 }
在这个函数中,创建了一个fdnode结构体的实例,然后对其成员赋值。最后,以fd为下标将这个实例存如fdevents结构体中的fdarray数组中。关于第三个参数:fdevent_handler handler,这是一个函数指针,其定义为typedef handler_t(*fdevent_handler) (void *srv, void *ctx, int revents)。这个函数指针对应XXX_handle_fdevent()类型的函数。比如network.c/ network_server_handle_fdevent() ,connections.c/ connection_handle_fdevent()。这些函数的作用是在fdevent系统检测到fd有IO事件发生时,处理这些IO事件。比如,network_server_handle_fdevent()处理监听fd(socket函数的返回值)发生的IO事件,connection_handle_fdevent()处理连接fd(accept函数的返回值)发生的IO事件。除了上面的两个函数,还有stat_cacahe.c/stat_cache_handle_fdevent(),mod_cgi.c/cgi_handle_fdevent(),mod_fastcgi.c/ fcgi_handle_fdevent(),mod_proxy.c/ proxy_handle_fdevent()和mod_scgi.c/scgi_handle_fdevent()等。在后面的讲解中,主要围绕network_server_handle_fdevent()和connection_handle_fdevent(),其他的函数有兴趣的读者可以自行查看。
接着,在for循环中调用(fdevent.c)fdevent_event_add()函数:
1 int fdevent_event_add(fdevents * ev, int * fde_ndx, int fd, int events)
2 {
3 int fde = fde_ndx ? * fde_ndx : - 1 ;
4 if (ev -> event_add)
5 fde = ev -> event_add(ev, fde, fd, events)
6 if (fde_ndx)
7 * fde_ndx = fde;
8 return 0 ;
9 }
函数中调用了fdevents结构体中event_add函数指针对应的函数。前面我们已经假设系统使用epoll,那么我们就去看看fdevent_linux_sysepoll.c中的fdevent_linux_sysepoll_event_add()函数,这个函数的地址在初始化的时候被赋给fdevents中的event_add指针:
1 static int fdevent_linux_sysepoll_event_add(fdevents * ev, int fde_ndx, int fd, int events)
2 {
3 struct epoll_event ep;
4 int add = 0 ;
5 if (fde_ndx == - 1 ) // 描述符不在epoll的检测中,增加之。
6 add = 1 ;
7 memset( & ep, 0 , sizeof (ep));
8 ep.events = 0 ;
9 /* *
10 * 在ep中设置需要监听的IO事件。
11 * EPOLLIN : 描述符可读。
12 * EPOLLOUT :描述符可写。
13 * 其他的事件还有:EPOLLRDHUP , EPOLLPRI, EPOLLERR, EPOLLHUP, EPOLLET, EPOLLONESHOT等。
14 */
15 if (events & FDEVENT_IN)
16 ep.events |= EPOLLIN;
17 if (events & FDEVENT_OUT)
18 ep.events |= EPOLLOUT;
19 /*
20 * EPOLLERR :描述符发生错误。
21 * EPOLLHUP :描述符被挂断。通常是连接断开。
22 */
23 ep.events |= EPOLLERR | EPOLLHUP /* | EPOLLET */ ;
24 ep.data.ptr = NULL;
25 ep.data.fd = fd;
26 /*
27 * EPOLL_CTL_ADD : 增加描述符fd到ev->epoll_fd中,并关联ep中的事件到fd上。
28 * EPOLL_CTL_MOD : 修改fd所关联的事件。
29 */
30 if ( 0 != epoll_ctl(ev -> epoll_fd, add ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, & ep))
31 {
32 fprintf(stderr, " %s.%d: epoll_ctl failed: %s, dying\n " ,__FILE__,__LINE__, strerror(errno));
33 SEGFAULT();
34 return 0 ;
35 }
36 return fd;
37 }
首先看函数的第三个参数events,他是一个整型,其没以为对应一种IO事件。
上面fdevent_event_add()函数的额第三个参数是FDEVENT_IN,这是一个宏:
1 /*
2 * 用于标记文件描述符的状态
3 */
4 #define FDEVENT_IN BV(0) // 文件描述符是否可写
5 #define FDEVENT_PRI BV(1) // 不阻塞的可读高优先级的数据 poll
6 #define FDEVENT_OUT BV(2) // 文件描述符是否可读
7 #define FDEVENT_ERR BV(3) // 文件描述符是否出错
8 #define FDEVENT_HUP BV(4) // 已挂断 poll
9 #define FDEVENT_NVAL BV(5) // 描述符不引用一打开文件 poll
其中BV也是一个宏,定义在settings.c文件中:
1 #define BV(x) (1 << x)
其作用就是将一个整数变量第x位置1,其余为0。
那么上面FDEVENT_XX的宏定义就是定义了一系列者养的整数,通过这些宏的或操作,可以在一个整数中用不同的位表示不同的事件。这些宏和struct epoll_event结构体中的events变量的对应的宏定义对应(有点绕。。。)。说白了就是和epoll.h中的枚举EPOLL_EVENTS对应。
这个函数的主要工作就是设置一些值然后调用epoll_ctl函数。虽然函数的名称是event_add,但是如果第二个参数fde_ndx不等于-1(那肯定就等于后面的参数fd),那这个时候可以肯定fd已经在epoll的监测中了,这时候不再是将fd增加到epoll中,而是修改其要监听的IO事件,就是最后一个参数表示的事件。fdevent_linux_sysepoll_event_add()增加成功后返回fd,在fdevent_event_add中,将这个返回值赋给fde_ndx,所以,fde_ndx==fd。
至此,监听fd的注册流程已经全部结束了,由于当有连接请求是,监听fd的表现是有数据课读,因此,只监听其FDEVENT_IN事件。注册之后,监听fd就开始等待连接请求。
接着,进程执行到了下面的语句:
1 // 启动事件轮询。底层使用的是IO多路转接。
2 if ((n = fdevent_poll(srv -> ev, 1000 )) > 0 )
3 {
4 /*
5 * nn是事件的数量(服务请求啦,文件读写啦什么的。。。)
6 */
7 int revents;
8 int fd_ndx = - 1 ;
9 /* *
10 * 这个循环中逐个的处理已经准备好的请求,知道所有的请求处理结束。
11 */
12 do
13 {
14 fdevent_handler handler;
15 void * context;
16 handler_t r;
17
18 fd_ndx = fdevent_event_next_fdndx(srv -> ev, fd_ndx);
19 revents = fdevent_event_get_revent(srv -> ev, fd_ndx);
20 fd = fdevent_event_get_fd(srv -> ev, fd_ndx);
21 handler = fdevent_get_handler(srv -> ev, fd);
22 context = fdevent_get_context(srv -> ev, fd);
23 /*
24 * connection_handle_fdevent needs a joblist_append
25 */
26 /* *
27 * 这里,调用请求的处理函数handler处理请求!
28 */
29 switch (r = ( * handler) (srv, context, revents))
30 {
31 case HANDLER_FINISHED:
32 case HANDLER_GO_ON:
33 case HANDLER_WAIT_FOR_EVENT:
34 case HANDLER_WAIT_FOR_FD:
35 break ;
36 case HANDLER_ERROR:
37 SEGFAULT();
38 break ;
39 default :
40 log_error_write(srv, __FILE__, __LINE__, " d " , r);
41 break ;
42 }
43 } while ( -- n > 0 );
44 }
45 else if (n < 0 && errno != EINTR)
46 {
47 log_error_write(srv, __FILE__, __LINE__, " ss " , " fdevent_poll failed: " , strerror(errno));
48 }
这段语句是worker子进程的工作重心所在。首先调用fdevent_poll()函数等待IO事件发生,如果没有IO事件,程序会阻塞在这个函数中。如果有fd发生了IO事件,则从fdevent_poll函数中返回,返回值是发生了IO事件的fd的数量。接着,程序进入do-while循环,循环中对每个fd,调用一些列fdevent系统的接口函数,最后调用event_handler处理IO事件。
fdevent_poll()函数调用fdevents结构体中的poll,最终调用的是epoll_wait()函数。epoll_wait()函数将发生了IO事件的fd对应的epoll_evet结构体实例的存储在fdevents结构体的epoll_events数组成员中。fdevent_event_next_fdndx函数返回epoll_events数组中下一个元素的下标,fdevent_event_get_revent函数调用ev->event_get_revent()获得fd发生的IO事件,最终调用的是:
1 static int fdevent_linux_sysepoll_event_get_revent(fdevents * ev, size_t ndx)
2 {
3 int events = 0 , e;
4 e = ev -> epoll_events[ndx].events;
5 if (e & EPOLLIN)
6 events |= FDEVENT_IN;
7 if (e & EPOLLOUT)
8 events |= FDEVENT_OUT;
9 if (e & EPOLLERR)
10 events |= FDEVENT_ERR;
11 if (e & EPOLLHUP)
12 events |= FDEVENT_HUP;
13 if (e & EPOLLPRI) // 有紧急数据到达(带外数据)
14 events |= FDEVENT_PRI;
15 return e;
16 }
这个函数就做了一个转换。fdevent_get_handler和fdevent_get_context返回fd对应的fdnode中的handler和ctx。这几个函数都简单,这里不再列出源代码。
最后,在switch语句中调用fd对应的handler函数处理事件。对于监听fd,调用的函数为:
1 /* *
2 * 这个是监听socket的IO事件处理函数。
3 * 只要的工作就是建立和客户端的socket连接。只处理读事件。在处理过程中,
4 * 每次调用这个函数都试图一次建立100个连接,这样可以提高效率。
5 */
6 handler_t network_server_handle_fdevent( void * s, void * context, int revents)
7 {
8 server * srv = (server * ) s;
9 server_socket * srv_socket = (server_socket * ) context;
10 connection * con;
11 int loops = 0 ;
12 UNUSED(context);
13 /*
14 * 只有fd事件是FDEVENT_IN时,才进行事件处理。
15 */
16 if (revents != FDEVENT_IN)
17 {
18 log_error_write(srv, __FILE__, __LINE__, " sdd " , " strange event for server socket " , srv_socket -> fd, revents);
19 return HANDLER_ERROR;
20 }
21 /*
22 * accept()s at most 100 connections directly we jump out after 100 to give the waiting connections a chance
23 *一次监听fd的IO事件,表示有客户端请求连接,对其的处理就是建立连接。建立连接后并不急着退出函数,
24 * 而是继续尝试建立新连接,直到已经建立了100次连接。这样可以提高效率。
25 */
26 for (loops = 0 ; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops ++ )
27 {
28 handler_t r;
29 // 根据当前状态,改变con的状态机,并做出相应的动作。
30 connection_state_machine(srv, con);
31 switch (r = plugins_call_handle_joblist(srv, con))
32 {
33 case HANDLER_FINISHED:
34 case HANDLER_GO_ON:
35 break ;
36 default :
37 log_error_write(srv, __FILE__, __LINE__, " d " , r);
38 break ;
39 }
40 }
41 return HANDLER_GO_ON;
42 }
监听fd有IO事件,表示有客户端请求连接,对其的处理就是建立连接。在这个函数中,建立连接后并不急着退出,而是继续尝试建立新连接,直到已经建立了100次连接。这样可以提高效率。connection_accept()函数接受连接请求并返回一个connection结构体指针。接着对这个连接启动状态机(状态机可是很有意思的。。。)。然后把连接加到作业队列中。
这里有一个问题,如果连接迟迟达不到100个,那么程序就会阻塞在这个函数中,这样对于已经建立的连接也就没法进行处理。这怎么办?读者不要忘了,在将监听fd注册到fdevent系统时,其被设置成了非阻塞的,因此,如果在调用accept()函数时没有连接请求,那么accept()函数会直接出错返回,这样connection_accept就返回一个NULL,退出了for循环。
到这,fdevent系统对于监听fd的处理就完成了一个轮回。处理完IO事件以后fd接着在epoll中等待下一次事件。
下一篇中回介绍一些fdevent系统对连接fd(accept函数的返回值)的处理,以及超时连接的处理。