• Lighttpd1.4.20源码分析之fdevent系统(3) 使用


    前面讲了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函数的返回值)的处理,以及超时连接的处理。

  • 相关阅读:
    vagrant中配置多虚拟机
    更改centos的hostname
    静态变量设为nonpublic或者加final关键字
    随机数Random和SecureRandom
    C#技术栈入门到精通系列18——映射框架AutoMapper
    C#技术栈入门到精通系列13——日志框架Log4Net
    MFC 弹出窗口初始化
    MFC ListControl 智障总结
    Windows本地磁盘无法重命名
    C++ private类访问
  • 原文地址:https://www.cnblogs.com/kernel_hcy/p/1689387.html
Copyright © 2020-2023  润新知