lighttpd 之十 I/O多路复用技术模型

1 概述     

       随着互联网的迅速发展,网络服务质量的逐渐提升,越来越多的网站开始面临严重的性能降低、服务质量变差的压力。作为提供网络服务的主要软件,Web服务器必须选择合适的策略来足以承受当前的高负载连接。前面章节曾提到过,Lighttpd服务器程序从一开始就把服务性能放在首位来考虑,其良好的结构设计不仅使得Lighttpd应用服务程序策略方案容易扩展,而且可以平稳地运行在当前大多数流行的UNIX/Linux系统上,并根据不同系统环境自动选择最优服务策略,因此关于实现此功能结构的Lighttpd源码必大有借鉴之处,本章将试图解析关于这方面的源码。
     本节相关部分源码:
      base.h
      server.c
      fdevent.c
      fdevent_select.c
      fdevent_linux_sysepoll.c

2 I/O模型基础知识      

      在解析Lighttpd中关于I/O多路复用相关源码之前,有必要先对UNIX/Linux下的I/O模型基础知识做个较为简单的介绍<sup>52</sup>,以便使得初次接触到这方面的读者也能顺利地继续阅读本章余下的内容。

2.1 I/O模型分类介绍

       目前UNIX/Linux下可用的I/O模型有5种,分别为阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O、异步I/O,而其中较为成熟且高效率、运行稳定的是I/O复用模型,因此当前众多网络服务程序几乎都是采用这种I/O操作策略,Lighttpd应用程序也不例外。

        当一个应用程序读写(以读为例)某端口数据时,选择不同I/O模型的应用程序,执行流程也将不同。下面将对选择这5种I/O模型的应用进程的各自执行情形进行分析,以便让读者理解Lighttpd应用程序选择使用I/O复用模型的大致运行情况以及性能优势。

      一个完整经典的应用程序的数据读取操作可以看作两步,一是等待数据准备好,二是将数据从内核复制到应用程序进程。

1.阻塞I/O模型

       最流行的I/O模型是阻塞I/O(blocking I/O)模型,几乎所有刚开始学习I/O操作的人员都是使用这个模型,虽然它存在一定的性能缺陷,但是它的确很简单。图8-1是利用该模型读取I/O端口数据的典型流程。在有些情况下,当系统调用发现用户请求的I/O操作不能立刻完成时(比如对I/O写操作,缓冲区没有空闲空间或者空闲空间少于待写的数据量;而对于读操作,缓冲区中没有数据可读或者可读数据少于用户请求的数据量),则当前的进程会进入睡眠,也就是进程被I/O读写阻塞。但是当数据可以写出或有数据可供读入时(其他进程或线程从缓冲区中读走了数据后或者向缓冲区写入了数据),系统将会产生中断,唤醒在缓冲区上等待相应事件的进程继续执行。

       

2.非阻塞I/O模型

       在有些时候,程序设计开发人员并不希望进程在I/O操作不能完成的时候睡眠,而是希望系统调用能立刻返回一个错误,以报告这一情况,然后进程可以根据需要在适当的时候再重新请求这个I/O操作。这就是所谓的非阻塞I/O模型。在图8-2中可以看到,应用程序前几次Read()系统调用时都没有数据可供返回,此时内核立即返回一个EAGAIN错误代号,程序并不睡眠而是继续调用Read(),当第四次调用Read()时数据准备好了,于是执行数据从内核到用户空间的复制并成功返回,应用程序接着处理数据。这种对一个非阻塞I/O端口反复调用Read()进行数据读取的动作称为轮询,即应用程序持续轮询内核数据是否准备好,这将导致耗费大量的CPU时间,因此该模型并不常见。

      3.

3.I/O复用模型

      上一节讲了非阻塞I/O模型的问题在于,尽管应用程序可以在当前I/O操作不能完成的时候迫使系统调用立刻返回而不至于睡眠,但是却无法知道什么时候再次请求I/O操作可以顺利完成。于是应用程序不得不每隔一段时间就重新请求一次系统调用,这种轮询策略极大浪费了CPU时间。I/O复用模型是在此之上的改进,它的好处在于使得应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以顺利完成,达到时间复用的目的。进程阻塞在类似于select或epoll这样的系统调用上,而不是阻塞在真正的I/O系统调用上。select或epoll使得进程可以在多个I/O端口上等待I/O事件(可读、可写、网络连接成功等)的发生 。当有事件发生时,程序再根据发生事件进行相应I/O操作。不过,可以看到应用进程在等待I/O事件发生的时候仍处于阻塞状态,因此如果希望应用进程在没有I/O事件可以处理的时候做其他的工作,这种模型是不可行的。

4.信号驱动I/O模型

     信号驱动I/O模型使得应用程序不需要阻塞在某一个或多个I/O端口上,它先利用系统调用Sigaction()来安装某个端口的事件信号处理函数,该系统调用Sigaction()执行成功后立即返回,进程继续往下工作而不被阻塞,当某I/O端口上可进行数据操作时,内核就为该进程产生一个SIGIO信号,进程收到该信号后相应地在信号处理函数里进行I/O操作,因此,这种机制能在I/O操作可以无阻塞地完成时异步地通知应用程序。

5.异步I/O模型

     异步I/O也是属于POSIX规范的一部分,类似信号驱动I/O的异步通知机制使它常常被与后者相混淆。与后者的区别在于,启用异步I/O意味着告知内核启动某个I/O操作,并让内核在整个操作(包括将数据从内核复制到用户空间的缓冲区)完成时通知我们。也就是说,信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而在异步I/O模型中,是由内核通知我们I/O操作何时完成,即实际的I/O操作也是异步的。

      以上对阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O、异步I/O这5种模型的执行流程(读取数据)做了最基本的介绍,读者先对它们有个基本的了解,对于Lighttpd应用程序采用的第三种模型即I/O复用,本章后面内容将详细讲解。

2.2 常见I/O多路复用实现技术

        Lighttpd1.4.20版本应用程序针对不同的UNIX/Linux平台采用了不同的I/O复用实现技术,除了包括最经典的select/poll,还有对Solaris的/dev/poll、BSD的kqueue、Linux的epoll以及实时信号驱动I/O(即rtsig)的支持,本小节就将先对这些多路复用实现技术做初步的介绍。

1.select/poll        

       传统的UNIX/Linux提供的select/poll实现了基于事件驱动的I/O多路复用模型,虽然其存储实现上的不完善而受到各种各样的限制,但是使用select/poll实现的I/O多路复用模型是使用最为广泛的事件驱动I/O模型。
select/poll系统调用的原型如清单8-1所示。
清单8-1 select/poll系统调用原型

 1.#include<sys/select.h>
2.int pselect(int nfds,fd_set*restrict readfds,fd_set*restrict writefds,
fd_set*restrict errorfds,
3.const struct timespec*restrict timeout,const sigset_t*restrict sigmask);
4.int select(int nfds,fd_set*restrict readfds,fd_set*restrict writefds,
fd_set*restrict errorfds,
5.struct timeval*restrict timeout);
6.void FD_CLR(int fd,fd_set*fdset);
7.int FD_ISSET(int fd,fd_set*fdset);
8.void FD_SET(int fd,fd_set*fdset);
9.void FD_ZERO(fd_set*fdset);
10.
11.#include<poll.h>
12.int poll(struct pollfd fds[],nfds_t nfds,int timeout);

首先来看select()函数,该函数包含有五个参数。其中中间三个参数数据类型为fd_set结构体,该结构体定义在/usr/include/sys/select.h头文件内,如清单8-2所示。

13.///usr/include/sys/select.h
14./*fd_set for select and pselect.*/
15.typedef struct
16.{
17./*XPG4.2 requires this member name.Otherwise avoid the name
18.from the global namespace.*/
19.#ifdef__USE_XOPEN
20.__fd_mask fds_bits[__FD_SETSIZE/__NFDBITS];
21.#define__FDS_BITS(set)((set)->fds_bits)
22.#else
23.__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
24.#define__FDS_BITS(set)((set)->__fds_bits)
25.#endif
26.}fd_set;
27.
/*Maximum number of file descriptors infd_set'.*/
/*指示最大描述符,__FD_SETSIZE宏在下面两个头文件中都有定义。*/
///usr/include/linux/posix_types.h
#undef__FD_SETSIZE
#define__FD_SETSIZE 1024
///usr/include/bits/typesizes.h
/*Number of descriptors that can fit in anfd_set'.*/
#define__FD_SETSIZE 1024
28.#define FD_SETSIZE__FD_SETSIZE

     可以看出,select监控的fd_set类型描述符集实际上就是包含若干位的数组,其中每个整数中的每一位对应一个描述符,提供的宏FD_CLR、FD_ISSET、FD_SET、FD_ZERO分别用来完成描述符集中的某位置0、测试描述符集中的某位是否置1、描述符集中的某位置1以及描述符集中的所有位置0,这些完成功能的实质都是对该数组的位操作。

      三个fd_set类型参数readfds、writefds和errorfds分别表示调用进程关注的可读、可写和异常的描述符集,比如如果进程希望某描述符在可读时接到通知,就将其加入readfds即可。如果对某一类描述符都不感兴趣则应将其设置为NULL,使用未经过初始化的描述符集其结果是不可预料的。

      select()函数第一个参数nfds为调用进程关心的描述符集中最大描述符(不是描述符集内的描述符个数)加1,比如三个描述符集中最大的描述符为5,那么nfds就为6。select()函数第五个参数timeout表示该函数最多等待的时间,该结构体有两个字段,分别记录秒数和微秒数,如清单8-3所示。该参数设置三种不同的值将导致该函数调用三种不同的运行结果。    

1)timeout设置为NULL,此时该调用永远等待下去,直到有描述符发生进程关心的事件而返回为止。
2)timeout设置为一个非零值,此时该调用等待一段固定时间返回或者在此之前有描述符发生进程关心的事件而返回。
3)timeout设置为零值(即结构体变量两个字段都为0),此时该调用检查描述符集后立即返回。

清单8-3 timeval数据结构定义

struct timeval{
long tv_sec;/*秒*/
long tv_usec;/*微秒(1/1000000)*/
};

select()函数返回所有描述符集中已就绪的总位数,超时则返回0,出错返回-1。

在循环使用select()函数时有三个地方值得注意:

第一,虽然在普遍情况下,参数timeout在select()函数返回时不会被修改,但是有的Linux版本系统却将会这个值修改成函数返回时剩余的等待秒数,因此从移植性上考虑,在每次重新调用select()函数前都得再次对参数timeout初始化。

第二,select()函数中间的三个参数(即描述符集)在select()函数返回时,其保存有指示哪些描述符已经进入就绪状态(此时其对应位为1,其他未就绪描述符对应的位置0),从而程序可以使用宏FD_ISSET来测试描述符集中的就绪描述符。因此,在每次重新调用select()函数前都得再次把所有描述符集中关注的位都要设置为1

第三,应注意到利用select()函数监控的最大描述符受到系统FD_SETSIZE宏的限制。

pselect()函数和select()函数相差不大,但是其有两点变化:
1)pselect()函数的第五个参数timeout为timespec类型而不是timeval类型。timespec和timeval两结构体类型仅第二个字段成员不同,即timespec的第二个字段指定纳秒,如清单8-4所示。
清单8-4 timespec数据结构定义

struct timespec{
time_t tv_sec;/*秒*/
long tv_nsec;/*纳秒(1/1000000000)*/
};

2)pselect()函数增加的第六个参数如果不为空,则pselect()函数先用它替换当前的信号掩码,然后执行同select()函数同样的功能后,重置原来的信号掩码。也即是这个参数可以允许程序先阻塞某些信号,在测试由这些当前被禁止信号的信号处理函数设置的全局变量后,再调用pselect()函数重新设置信号掩码。

清单8-5给出了利用select多路复用I/O的Web服务应用模型(请读者注意:这里给出的模型未考虑任何可能出现的错误,仅仅给出一个如何使用select的步骤,后面的模型也是如此)。
清单8-5 利用select多路复用I/O的Web服务应用模型

/*可读、可写、异常三种文件描述符集的声明和初始化。*/
fd_set readfds,writefds,exceptionfds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptionfds);
int max_fd;
/*socket配置和监听。*/
sock=socket(...);
bind(sock,...);
listen(sock,...);
/*对socket描述符上发生关心的事件进行注册。*/
FD_SET(&readfds,sock);
max_fd=sock;
while(1){
int i;
fd_set r,w,e;
/*为了重复使用readfds、writefds、exceptionfds,将它们复制到临时变量内。*/
memcpy(&r,&readfds,sizeof(fd_set));
memcpy(&w,&writefds,sizeof(fd_set));
memcpy(&e,&exceptionfds,sizeof(fd_set));
/*利用临时变量调用select()阻塞等待,等待时间为永远等待直到发生事件。*/
select(max_fd+1,&r,&w,&e,NULL);
/*测试是否有客户端发起连接请求,如果有则接受并把新建的描述符加入监控。*/
if(FD_ISSET(&r,sock)){
new_sock=accept(sock,...);
FD_SET(&readfds,new_sock);
FD_SET(&writefds,new_sock);
max_fd=MAX(max_fd,new_sock);
}
/*对其他描述符发生的事件进行适当处理。描述符依次递增,各系统的最大值有所不同(比如在作者系
统上最大为1024),在Linux可以用命令ulimit-a查看(用ulimit命令也对该值进行修改)。在freebsd下,用sysctl-a|grep kern.maxfilesperproc来查询和修改。*/
for(i=sock+1;i<max_fd+1;++i){
if(FD_ISSET(&r,i))
doReadAction(i);
if(FD_ISSET(&w,i))
doWriteAction(i);
}
}

poll()函数实现和select()函数类似的功能,前面已经给出原型,其第一个参数是指向一个pollfd结构体数组的第一个元素的指针,pollfd结构体如清单8-6所示,用于指定测试某个描述符的条件,其中的events和revents可能取值有POLLIN(普通或优先级带数据可读)、POLLRDNORM(普通数据可读)、POLLRDBAND(优先级带数据可读)、POLLPRI(高优先级数据可读)、POLLOUT(普通数据可写)、POLLWRNORM(普通数据可写)、POLLWRBAND(优先级带数据可写)、POLLERR(发生错误)、POLLHUP(发生挂起)、POLLNVAL(描述符不是打开文件)(后三个值不能在events中设置,主要用于在revents中返回表示发生事件)。第二个参数nfds表示第一个数组参数中元素的个数(即像SELECT模型中最大描述符必须小于FD_SETSIZE的限制在POLL模型里就不存在了,poll()函数直接利用参数nfds对所有关注事件进行监控)。第三个参数timeout指定poll()函数调用等待多久,取值为INFTIM(负数)则永远等待,为0则立即返回,大于0则等待指定时间段。

清单8-6 pollfd数据结构定义

struct pollfd{
int fd;/*指定的被测试描述符。*/
short events;/*关注的事件。*/
short revents;/*结果事件,正是因为利用这个字段来返回发生事件标识,避免了像SELECT模型那样的每次进行监控前都要重新设置关注事件的麻烦。该字段在调用函数poll()里会先自动清空为0,然后在轮询等待过程中根据events字段设置的关注事件是否已发生来设置对应的bit位,另外POLLHUP、POLLERR和POLLNVAL这三个标识(这三个标识无法用于events字段)也会被设置,只要它们各自对应的事件发生了。*/
};

如果不再关心某个特定的描述符,则只需把它对应的pollfd结构的fd字段设置为一个负值,poll函数将忽略这样的pollfd结构体的events成员检查,返回时将它的revents字段的值设置为0。
清单8-7给出了利用poll多路复用I/O的Web服务应用模型。
清单8-7 利用poll多路复用I/O的Web服务应用模型

/*新建并初始化文件描述符集。*/
struct pollfd fds[MAX_NUM_FDS];
int max_fd;
/*socket配置和监听。*/
sock=socket(...);
bind(sock,...);
listen(sock,...);
/*对socket描述符上发生关心的事件进行注册。*/
fds[0].fd=sock;
fds[0].events=POLLIN;
max_fd=1;
while(1){
int i;
/*调用poll()阻塞等待,等待时间为永远等待直到发生事件。*/
poll(fds,max_fd,-1);
/*测试是否有客户端发起连接请求,如果有则接受并把新建的描述符加入监控。*/
if(fds[0].revents&POLLIN){
new_sock=accept(sock,...);
fds[max_fd].fd=new_sock;
fds[max_fd].events=POLLIN|POLLOUT;
++max_fd;
}
/*对其他描述符发生的事件进行适当处理。*/
for(i=1;i<max_fd+1;++i){
if(fds[i].revents&POLLIN)
doReadAction(i);
if(fds[i].revents&POLLOUT)
doWriteAction(i);
}
}

      从上面基于select/poll多路复用I/O的Web服务应用模型可以看出:在大量的并发连接中,如果空闲连接(即无事件发生的连接)较多,select/poll的性能会因为并发数的线性上升而成平方速度下降,这是因为在每次select和poll返回时都要检测每个连接是否有事件发生(即最后那个循环检查),当连接数很大时,系统开销会异常大。另外select/poll每次返回时都要从内核向用户空间复制大量的数据,这样的开销也很大。使用select/poll实现的多路复用I/O模型是最稳定也是使用最为广泛的事件驱动I/O模型,但是其固有的一些缺点(如性能低下、伸缩性不强)使得各种更为先进的替代方案出现在各种平台下。

2.epoll

       epoll作为poll的变体在Linux内核2.5中被引入<sup>53</sup>。相比于select实现的多路复用I/O模型,epoll最大的好处在于它不会随着监控描述符数目的增长而效率急速下降。在内核中的select实现是采用轮询来处理的,轮询的描述符数目越多,自然耗时越多,而且在很多情况下,select能最多同时监听的描述符数目为1024个。

      epoll提供了三种系统调用,如清单8-8所示。
      清单8-8 epoll系统调用原型

    #include<sys/epoll.h>
     int epoll_create(int size);
     int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);
     int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
     int epoll_pwait(int epfd,struct epoll_event*events,int maxevents,int timeout,
     const sigset_t*sigmask);

        系统调用epoll_create()创建一个epoll的句柄,参数size用来告诉内核监听的数目一共有多少,请求内核为存储事件分配空间,并返回一个描述符(在使用完epoll后,必须调用close()关闭这个描述符,否则可能导致系统描述符被耗尽)

       函数epoll_ctl()用来向内核注册、删除或修改事件,其第一个参数epfd是函数epoll_create()的返回值,第二个参数op表示动作,分别为EPOLL_CTL_ADD(注册新的fd到epfd中)、EPOLL_CTL_MOD(修改已经注册的fd的监听事件)以及EPOLL_CTL_DEL(从epfd中删除一个fd),第三个参数fd表示需要监听的描述符,第四个参数event是epoll_event结构体类型,用于告诉内核需要监听什么事件,其中结构体epoll_event(如清单8-9所示)的字段events可能取值有EPOLLIN(普通数据可读)、EPOLLOUT(普通数据可写)、EPOLLPRI(高优先级数据可读)、EPOLLERR(发生错误)、EPOLLHUP(发生挂起)、EPOLLET(将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的)。该函数执行成功返回0,发生错误返回-1,同时设置错误标志errno。

        清单8-9 epoll_event、epoll_data_t数据结构定义

       typedef union epoll_data{
            void*ptr;
            int fd;
           __uint32_t u32;
           __uint64_t u64;
       }epoll_data_t;
      struct epoll_event{
           __uint32_t events;/*Epoll events*/
           epoll_data_t data;/*User data variable*/
       };

     

         函数epoll_wait()用来等待事件产生,其第一个参数epfd是函数epoll_create()的返回值;第二个参数events用来从内核接收发生事 件的集合;第三个参数maxevents指定最大事件数目,其值不能大于之前函数epoll_create()调用时的参数size,同时必须大于0;第四个参数timeout指定epoll_wait()函数调用等待多久(单位为毫秒),取值为-1则等待时间不确定,为0则立即返回,大于0则等待指定时间段。该函数执行成功将返回发生事件的描述符数目,当超时仍没有事件发生时返回0,发生错误返回-1,同时设置错误标志errno。

         函数epoll_pwait()和函数epoll_wait()的异同与函数pselect()和函数select()之间的异同类似,在此不再赘述。

        EPOLL事件的两种模型
        Level Triggered(LT)

       LT(level triggered,水平触发)是默认的工作方式,同时支持阻塞和非阻塞socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉进程该描述符有事件发生,之后如果进程一直不对这个就绪状态做出任何操作,则内核会继续通知,直到事件处理完成。以LT方式调用的epoll接口就相当于一个速度比较快的POLL模型。

       Edge Triggered(ET)

       ET(edge-triggered,边缘触发)是高速工作方式,只支持非阻塞socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉进程该描述符有事件发生,之后就算进程一直不对这个就绪状态做出任何操作,内核也不会再发送更多的通知,也就是说内核仅在描述符变化的那个突变边缘对进程做出一次通知。根据ET方式的特性,epoll工作在此模式时必须使用非阻塞文件描述符,以避免由于一个文件描述符的阻塞读/写操作把处理多个文件描述符的任务“饿死”。调用ET模式epoll接口的推荐步骤如下:

     1)基于非阻塞文件描述符。
      2)只有当read()或write()返回EAGAIN(对于面向包/令牌的文件,比如数据包套接口、规范模式的终端)或是read()/write()读到/写出的数据长度小于请求的数据长度(对于面向流的文件,比如pipe、FIFO、流套接口)时才需要挂起等待下一个事件。

      总的来说,在大并发的系统中,边缘触发模式比水平触发模式更有优势,但是对程序员的要求也更高。如果对于这两种模式想要了解得更为深入,那么建议读者阅读其源码。

      清单8-10给出了利用epoll多路复用I/O的Web服务应用模型。
      清单8-10 利用epoll多路复用I/O的Web服务应用模型

/*新建并初始化文件描述符集。*/
struct epoll_event ev;
struct epoll_event events[MAX_EVENTS];
/*创建epoll句柄。*/
int epfd=epoll_create(MAX_EVENTS);
/*socket配置和监听。*/
sock=socket(...);
bind(sock,...);
listen(sock,...);
/*对socket描述符上发生关心的事件进行注册。*/
ev.events=EPOLLIN;
ev.data.fd=sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev);
while(1){
int i;
/*调用epoll_wait()阻塞等待,等待时间为永远等待直到发生事件。*/
int n=epoll_wait(epfd,events,MAX_EVENTS,-1);
for(i=0;i<n;++i){
/*测试是否有客户端发起连接请求,如果有则接受并把新建的描述符加入监控。*/
if(events[i].data.fd==sock){
if(events[i].events&POLLIN){
new_sock=accept(sock,...);
ev.events=EPOLLIN|POLLOUT;
ev.data.fd=new_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}
}else{
/*对其他描述符发生的事件进行适当处理。*/
if(events[i].events&POLLIN)
doReadAction(i);
if(events[i].events&POLLOUT)
doWriteAction(i);
}
}
}

       从清单8-10所示的模型代码中可以明显地看出:与select/poll相比,epoll的最大优点就是每次只返回有事件发生的文件描述符信息,这样调用者不用遍历整个文件描述符队列,而且使用epoll使得系统不用从内核向用户空间复制数据,因为它是利用mmap使内核和用户空间共享一块内存<sup>54</sup>。另外epoll可以设置不同的事件触发方式,包括边缘触发和电平触发两种,为用户使用epoll提供了灵活性。

       由于本人大部分都在linux环境工作,因此其它实现方式不再关心。

       3.kqueue

       4./dev/poll

       5.rtsig

3 Lighttpd中多路复用技术模型应用

      前面几小节对目前较为流行的I/O多路复用实现技术作了介绍,本节开始将解析它们在Lighttpd源码中的具体实现。Lighttpd提供的I/O事件处理模型如表8-1所示。

      

3.1 整合多种复用技术模型的数据结构封装

      

       打开各实现文件,读者不难发现有一个fdevents的结构体频频出现,该结构体是Lighttpd实现对不同平台多种I/O事件处理模型支持的基础,其封装所有模型所必需的数据和操作,定义在头文件fdevent.h内,如清单8-19所示。
       清单8-19 fdevents数据结构定义

29.//fdevent.h
30./**
31.*fd-event handler for select(),poll()and rt-signals on Linux 2.4
32.*
33.*/
34.typedef struct fdevents{
35.fdevent_handler_t type;
36.fdnode**fdarray;
37.size_t maxfds;
38.
39.#ifdef USE_LINUX_SIGIO
40.int in_sigio;
41.int signum;
42.sigset_t sigset;
43.siginfo_t siginfo;
44.bitset*sigbset;
45.#endif
46.#ifdef USE_LINUX_EPOLL
47.int epoll_fd;
48.struct epoll_event*epoll_events;
49.#endif
50.#ifdef USE_POLL
51.struct pollfd*pollfds;
52.size_t size;
53.size_t used;
54.buffer_int unused;
55.#endif
56.#ifdef USE_SELECT
57.fd_set select_read;
58.fd_set select_write;
59.fd_set select_error;
60.fd_set select_set_read;
61.fd_set select_set_write;
62.fd_set select_set_error;
63.int select_max_fd;
64.#endif
65.#ifdef USE_SOLARIS_DEVPOLL
66.int devpoll_fd;
67.struct pollfd*devpollfds;
68.#endif
69.#ifdef USE_FREEBSD_KQUEUE
70.int kq_fd;
71.struct kevent*kq_results;
72.bitset*kq_bevents;
73.#endif
74.#ifdef USE_SOLARIS_PORT
75.int port_fd;
76.#endif
77.int(*reset)(struct fdevents*ev);
78.void(*free)(struct fdevents*ev);
79.int(*event_add)(struct fdevents*ev,int fde_ndx,int fd,int events);
80.int(*event_del)(struct fdevents*ev,int fde_ndx,int fd);
81.int(*event_get_revent)(struct fdevents*ev,size_t ndx);
82.int(*event_get_fd)(struct fdevents*ev,size_t ndx);
83.int(*event_next_fdndx)(struct fdevents*ev,int ndx);
84.int(*poll)(struct fdevents*ev,int timeout_ms);
85.int(*fcntl_set)(struct fdevents*ev,int fd);
86.}fdevents;
87.
88.typedef enum{FDEVENT_HANDLER_UNSET,
89.FDEVENT_HANDLER_SELECT,
90.FDEVENT_HANDLER_POLL,
91.FDEVENT_HANDLER_LINUX_RTSIG,
92.FDEVENT_HANDLER_LINUX_SYSEPOLL,
93.FDEVENT_HANDLER_SOLARIS_DEVPOLL,
94.FDEVENT_HANDLER_FREEBSD_KQUEUE,
95.FDEVENT_HANDLER_SOLARIS_PORT
96.}fdevent_handler_t;
97.
98./**
99.*array of unused fd's
100.*/
101.typedef struct_fdnode{
/*函数指针字段,后面会讲到。*/
102.fdevent_handler handler;
103.void*ctx;
104.int fd;
105.struct_fdnode*prev,*next;
106.}fdnode;
107.
108.typedef struct{
109.int*ptr;
110.
111.size_t used;
112.size_t size;
113.}buffer_int;

该结构体初看上去有很多字段,但是不难发现根据编译系统的环境不同有不少用不着的字段会被去掉,比如如果当前系统是Linux系统,其不支持/dev/poll和kqueue,那么与这两种事件处理模型相关的字段(devpoll_fd、devpollfds、kq_fd、kq_results和kq_bevents)就不会存在,具体原因如下所示。

根据编译环境改变fdevents数据结构字段
           在编译Lighttpd源码生成应用程序之前,由于Lighttpd源码包里并没有提供Makefile文件,因此除了必须要先安装Lighttpd编译依赖的其他开发库外,还需在进行make之前先configure,configure会根据系统环境(比如包含的头文件)信息生成相应的config.h头文件,该头文件内包含有一些宏定义,以作者的Ubuntu系统为例,生成的config.h内能找到这样的几行代码(通过这些宏读者不难看出这个头文件config.h的真正作用,其保存的是通过configure检查出来的用于判断系统编译环境的宏,这些宏方便应用程序根据不同环境作出不同的编译选项选择):

//config.h
/*Define to 1 if you have the<stdint.h>header file.*/
#define HAVE_STDINT_H 1
/*Define to 1 if you have theepoll_ctl'function.*/
#define HAVE_EPOLL_CTL 1
/*Define to 1 if you have the<sys/epoll.h>header file.*/
#define HAVE_SYS_EPOLL_H 1
/*Define to 1 if you have thekqueue'function.*/
/*#undef HAVE_KQUEUE*/
/*Define to 1 if you have the<sys/event.h>header file.*/
/*#undef HAVE_SYS_EVENT_H*/

再来看看头文件fdevent.h内的几行宏操作和定义:

//fdevent.h
#ifdef HAVE_CONFIG_H
#include"config.h"
#endif
#if defined(HAVE_EPOLL_CTL)&&defined(HAVE_SYS_EPOLL_H)
#if defined HAVE_STDINT_H
#include<stdint.h>
#endif
#define USE_LINUX_EPOLL
#include<sys/epoll.h>
#endif
#if defined HAVE_SYS_EVENT_H&&defined HAVE_KQUEUE
#define USE_FREEBSD_KQUEUE
#include<sys/event.h>
#endif

前面第一个宏判断和操作为当定义有宏HAVE_CONFIG_H时,将头文件config.h引入本文件(即fdevent.h头文件)。如果引入了头文件config.h,那么需要的这几个宏也就引入进来了,于是下面的两个宏操作以及定义结果就不言而喻了,从而在fdevents结构体中与事件处理模型kqueue相关的字段(kq_fd、kq_results、kq_bevents)就不存在了,因为其存在的前提判断USE_FREEBSD_KQUEUE是否已经定义为否。

头文件config.h是否能引入到fdevent.h头文件中就看宏HAVE_CONFIG_H是否有定义了,在生成的Makefile文件内可以找到这么一句:

//Makefile
DEFS=-DHAVE_CONFIG_H-DLIBRARY_DIR="\"$(libdir)\""
-DSBIN_DIR="\"$(sbindir)\""

DEFS变量($(DEFS))在Makefile文件的其他所有文件编译连接语句的选项里几乎都有出现,也就是说给它们都加上了这三个选项,而其中的第一个选项-DHAVE_CONFIG_H就是为当前编译文件提前定义一个HAVE_CONFIG_H宏,-D选项在GCC手册里的说明是-DMACRO表示以字符串“1”定义MACRO宏<sup>68</sup>,这里给出一个小例子,如下所示:

        //a.c
#include<stdio.h>
int main(){
#if defined(HAVE)
printf("Hello world");
#else
printf("World hello");
#endif
}

请读者分别以"gcc-o a a.c"和"gcc-DHAVE-o a a.c"编译运行上述文件代码,看得到的结果是怎样的?

当然,有些字段是必定会存在的,比如结构体最开始定义的三个字段和最后定义的几个函数指针字段,而中间的字段就会有选择地加入而形成“新”的结构体。类比之前3.4节讲过的data_xxxxx数据结构,同样可以从fdevents结构体的设计这里看出一些面向对象的思想,只是采取的途径各有不同。在这里可以将结构体内宏外的字段组合看成一个基类,其他的是根据系统环境的不同而生成的不同子类,图8-6中给出的是假定系统环境仅支持其中一种I/O模型的UML图,如果系统环境支持多种I/O模型,则各子类的字段应增加。

       fdevents结构体大部分字段都是对应各自I/O模型出现的并且根据前面讲解意思也很清晰,尚未提到的字段只剩下几个,其中第一个字段的fdevent_handler_t是枚举类型,与各种I/O事件处理模型一一对应;第二个字段的fdnode是一个结构体类型,该结构体可以通过其prev和next两字段形成链表,fd表示与该结构体关联的描述符,ctx是一个表示与该结构体关联的上下文环境,根据不同的fd描述符有不同的取值,最后一个介绍的字段handler非常重要,其是一个回调函数,在fd描述符上有事件触发时进行回调处理。第三个字段的maxfds是size_t类型用于计数存放处理的描述符最大数量。

        在fdevents结构体的最后是一组函数指针,这组指针根据具体选用的I/O事件处理模型会指向各自不同的实现函数,如果把每种具体I/O事件处理模型看作子类的实例,那么这里也就是实现了所谓的多态效果。各I/O事件处理模型的具体实现分布在不同的源文件内,比如与SELECT模型相关的接口实现在源文件fdevent_select.c内,与poll模型相关的接口实现在源文件fdevent_poll.c内等,并且这些函数被关键字static修饰,这意味着这些函数的作用域仅局限于各自本文件内(即内部函数,不能被其他文件内函数所调用),是各自模型的私有实现成员,而对外公开的接口都定义在源文件fdevent.c内并在头文件fdevent.h中声明。

         下面再来看看统一的公开接口和各I/O模型私有的实现是如何关联起来的。首先查看定义在源文件fdevent.c的fdevent_init()函数,表面上来看,该函数就做了一件事情,它申请并初始化了一个fdevents结构体ev并将它作为函数返回值返回,当然它的初始化工作借助了其他函数,通过判断参数type得知当前选择的是何种I/O事件处理模型(这有可能是用户在配置文件里指定的,也有可能是自动选择默认的),从而调用各模型的初始化函数对ev做进一步的初始化。各模型的初始化函数基本都是对ev结构体的函数指针字段赋值,将自身实现的私有函数赋值给ev结构体的相应函数指针字段,这样一来就完成了可以通过结构体ev的函数指针字段对各模型私有实现函数调用的功能;有的I/O模型的初始化函数还会根据需要对是否能建立相应的监听句柄进行测试,比如/dev/poll,如清单8-20所示。

          清单8-20 函数fdevent_init

     //fdevent.c
114.fdevents*fdevent_init(size_t maxfds,fdevent_handler_t type){
115.fdevents*ev;
116.ev=calloc(1,sizeof(*ev));
117.ev->fdarray=calloc(maxfds,sizeof(*ev->fdarray));
118.ev->maxfds=maxfds;
119.switch(type){
120.case FDEVENT_HANDLER_POLL:
121.if(0!=fdevent_poll_init(ev)){
122.fprintf(stderr,"%s.%d:event-handler poll failed\n",
123.__FILE__,__LINE__);
124.return NULL;
125.}
126.break;
127.case FDEVENT_HANDLER_SELECT:
128.if(0!=fdevent_select_init(ev)){
129.fprintf(stderr,"%s.%d:event-handler select failed\n",
130.__FILE__,__LINE__);
131.return NULL;
132.}
133.break;
134.case FDEVENT_HANDLER_LINUX_RTSIG:
135.if(0!=fdevent_linux_rtsig_init(ev)){
136.fprintf(stderr,"%s.%d:event-handler linux-rtsig failed,
137.try to set server.event-handler=\"poll\"or\"select\"\n",
138.__FILE__,__LINE__);
139.return NULL;
140.}
141.break;
142.case FDEVENT_HANDLER_LINUX_SYSEPOLL:
143.if(0!=fdevent_linux_sysepoll_init(ev)){
144.fprintf(stderr,"%s.%d:event-handler linux-sysepoll failed,
145.try to set server.event-handler=\"poll\"or\"select\"\n",
146.__FILE__,__LINE__);
147.return NULL;
148.}
149.break;
150.case FDEVENT_HANDLER_SOLARIS_DEVPOLL:
151.if(0!=fdevent_solaris_devpoll_init(ev)){
152.fprintf(stderr,"%s.%d:event-handler solaris-devpoll failed,
153.try to set server.event-handler=\"poll\"or\"select\"\n",
154.__FILE__,__LINE__);
155.return NULL;
156.}
157.break;
158.case FDEVENT_HANDLER_FREEBSD_KQUEUE:
159.if(0!=fdevent_freebsd_kqueue_init(ev)){
160.fprintf(stderr,"%s.%d:event-handler freebsd-kqueue failed,
161.try to set server.event-handler=\"poll\"or\"select\"\n",
162.__FILE__,__LINE__);
163.return NULL;
164.}
165.break;
166.default:
167.fprintf(stderr,"%s.%d:event-handler is unknown,
168.try to set server.event-handler=\"poll\"or\"select\"\n",
169.__FILE__,__LINE__);
170.return NULL;
171.}
172.return ev;
173.}
174.
//fdevent_select.c
175.int fdevent_select_init(fdevents*ev){
176.ev->type=FDEVENT_HANDLER_SELECT;
/*下面这个宏用到了两个连接符。字符'\'表示下一行也是该宏语句,字符'\'后不能再有任何字符(空格、注释等都不能有)。“##”则表示将其前后的两个子串连接起来组成新的字符串,与此类似作用的宏符号还有“#@”、“#”,它们的说明列表如下:

*/
177.#define SET(x)\
178.ev->x=fdevent_select_##x;
179.SET(reset);
180.SET(poll);
181.SET(event_del);
182.SET(event_add);
183.SET(event_next_fdndx);
184.SET(event_get_fd);
185.SET(event_get_revent);
186.return 0;
187.}
188.
//fdevent_poll.c
189.int fdevent_poll_init(fdevents*ev){
190.ev->type=FDEVENT_HANDLER_POLL;
191.#define SET(x)\
192.ev->x=fdevent_poll_##x;
193.SET(free);
194.SET(poll);
195.SET(event_del);
196.SET(event_add);
197.SET(event_next_fdndx);
198.SET(event_get_fd);
199.SET(event_get_revent);
200.return 0;
201.}
202.
//fdevent_linux_sysepoll.c
203.int fdevent_linux_sysepoll_init(fdevents*ev){
204.ev->type=FDEVENT_HANDLER_LINUX_SYSEPOLL;
205.#define SET(x)\
206.ev->x=fdevent_linux_sysepoll_##x;
207.SET(free);
208.SET(poll);
209.SET(event_del);
210.SET(event_add);
211.SET(event_next_fdndx);
212.SET(event_get_fd);
213.SET(event_get_revent);
214.if(-1==(ev->epoll_fd=epoll_create(ev->maxfds))){
215.fprintf(stderr,"%s.%d:epoll_create failed(%s),
216.try to set server.event-handler=\"poll\"or\"select\"\n",
217.__FILE__,__LINE__,strerror(errno));
218.return-1;
219.}
220.if(-1==fcntl(ev->epoll_fd,F_SETFD,FD_CLOEXEC)){
221.fprintf(stderr,"%s.%d:epoll_create failed(%s),
222.try to set server.event-handler=\"poll\"or\"select\"\n",
223.__FILE__,__LINE__,strerror(errno));
224.close(ev->epoll_fd);
225.return-1;
226.}
227.ev->epoll_events=malloc(ev->maxfds*sizeof(*ev->epoll_events));
228.return 0;
229.}
230.
//fdevent_freebsd_kqueue.c
231.int fdevent_freebsd_kqueue_init(fdevents*ev){
232.ev->type=FDEVENT_HANDLER_FREEBSD_KQUEUE;
233.#define SET(x)\
234.ev->x=fdevent_freebsd_kqueue_##x;
235.SET(free);
236.SET(poll);
237.SET(reset);
238.SET(event_del);
239.SET(event_add);
240.SET(event_next_fdndx);
241.SET(event_get_fd);
242.SET(event_get_revent);
243.ev->kq_fd=-1;
244.ev->kq_results=calloc(ev->maxfds,sizeof(*ev->kq_results));
245.ev->kq_bevents=bitset_init(ev->maxfds);
246./*check that kqueue works*/
247.if(-1==(ev->kq_fd=kqueue())){
248.fprintf(stderr,"%s.%d:kqueue failed(%s),
249.try to set server.event-handler=\"poll\"or\"select\"\n",
250.__FILE__,__LINE__,strerror(errno));
251.return-1;
252.}
253.close(ev->kq_fd);
254.ev->kq_fd=-1;
255.return 0;
256.}
257.
//fdevent_solaris_devpoll.c
258.int fdevent_solaris_devpoll_init(fdevents*ev){
259.ev->type=FDEVENT_HANDLER_SOLARIS_DEVPOLL;
260.#define SET(x)\
261.ev->x=fdevent_solaris_devpoll_##x;
262.SET(free);
263.SET(poll);
264.SET(reset);
265.SET(event_del);
266.SET(event_add);
267.SET(event_next_fdndx);
268.SET(event_get_fd);
269.SET(event_get_revent);
270.ev->devpollfds=malloc(sizeof(*ev->devpollfds)*ev->maxfds);
271.if((ev->devpoll_fd=open("/dev/poll",O_RDWR))<0){
272.fprintf(stderr,"%s.%d:opening/dev/poll failed(%s),
273.try to set server.event-handler=\"poll\"or\"select\"\n",
274.__FILE__,__LINE__,strerror(errno));
275.return-1;
276.}
277./*we just wanted to check if it works*/
278.close(ev->devpoll_fd);
279.ev->devpoll_fd=-1;
280.return 0;
281.}
282.
//fdevent_linux_rtsig.c
283.int fdevent_linux_rtsig_init(fdevents*ev){
284.ev->type=FDEVENT_HANDLER_LINUX_RTSIG;
285.#define SET(x)\
286.ev->x=fdevent_linux_rtsig_##x;
287.SET(free);
288.SET(poll);
289.SET(event_del);
290.SET(event_add);
291.SET(event_next_fdndx);
292.SET(fcntl_set);
293.SET(event_get_fd);
294.SET(event_get_revent);
295.ev->signum=SIGRTMIN+1;
296.sigemptyset(&(ev->sigset));
297.sigaddset(&(ev->sigset),ev->signum);
298.sigaddset(&(ev->sigset),SIGIO);
299.if(-1==sigprocmask(SIG_BLOCK,&(ev->sigset),NULL)){
300.fprintf(stderr,"%s.%d:sigprocmask failed(%s),
301.try to set server.event-handler=\"poll\"or\"select\"\n",
302.__FILE__,__LINE__,strerror(errno));
303.return-1;
304.}
305.ev->in_sigio=1;
306.ev->sigbset=bitset_init(ev->maxfds);
307.return 0;
308.}

       将结构体ev的函数指针字段与各模型私有实现函数关联起来以后,其他公共调用接口的实现就简单了,只需调用结构体ev对应的函数指针即可,ev根据初始化时的函数地址赋值,自动调用预期的函数,而各I/O模型的具体实现都被隐藏不外露,这样形成的对外统一公开函数调用接口,使得将多种复用技术模型整合在一起的实现变得容易而优雅,如清单8-21所示。

清单8-21 I/O模型接口封装

       //fdevent.c
/*重置某个fdevents。*/
309.int fdevent_reset(fdevents*ev){
310.if(ev->reset)return ev->reset(ev);
311.return 0;
312.}
/*释放fdevents指针。*/
313.void fdevent_free(fdevents*ev){
314.size_t i;
315.if(!ev)return;
316.if(ev->free)ev->free(ev);
317.for(i=0;i<ev->maxfds;i++){
318.if(ev->fdarray[i])free(ev->fdarray[i]);
319.}
320.free(ev->fdarray);
321.free(ev);
322.}
/*向fdevents中添加一个描述符fd,events表示对这个描述符关心的事件。*/
323.int fdevent_event_add(fdevents*ev,int*fde_ndx,int fd,int events){
324.int fde=fde_ndx?*fde_ndx:-1;
325.if(ev->event_add)fde=ev->event_add(ev,fde,fd,events);
326.if(fde_ndx)*fde_ndx=fde;
327.return 0;
328.}
/*从fdevents中删除一个描述符fd。*/
329.int fdevent_event_del(fdevents*ev,int*fde_ndx,int fd){
330.int fde=fde_ndx?*fde_ndx:-1;
331.if(ev->event_del)fde=ev->event_del(ev,fde,fd);
332.if(fde_ndx)*fde_ndx=fde;
333.return 0;
334.}
/*根据描述符fd在fdevents中的fdarray中的索引index,获取该描述符目前关心的事件。*/
335.int fdevent_event_get_revent(fdevents*ev,size_t ndx){
336.if(ev->event_get_revent==NULL)SEGFAULT();
337.return ev->event_get_revent(ev,ndx);
338.}
/*根据描述符fd在fdevents中的fdarray中的索引index,获取要进行处理的描述符。*/
339.int fdevent_event_get_fd(fdevents*ev,size_t ndx){
340.if(ev->event_get_fd==NULL)SEGFAULT();
341.return ev->event_get_fd(ev,ndx);
342.}
/*获取下一个需要进行处理的描述符fd在fdarray中的索引index。*/
343.int fdevent_event_next_fdndx(fdevents*ev,int ndx){
344.if(ev->event_next_fdndx)return ev->event_next_fdndx(ev,ndx);
345.return-1;
346.}
/*轮询等待,超时时间由参数timeout_ms指定,单位是微秒。*/
347.int fdevent_poll(fdevents*ev,int timeout_ms){
348.if(ev->poll==NULL)SEGFAULT();
349.return ev->poll(ev,timeout_ms);
350.}
/*这个接口为实时信号驱动I/O模型所特有,其将信号和描述符关联起来等操作以便对描述符是否发生事件进行监控。其他I/O模型该函数都为NULL(即其他模型没有特别的任务放在该接口内实现),但是对描述符fd会统一的设置一些属性。*/
351.int fdevent_fcntl_set(fdevents*ev,int fd){
352.#ifdef FD_CLOEXEC
353./*close fd on exec(cgi)*/
/*FD_CLOEXEC用来设置文件的close-on-exec状态标志。在exec()调用后,close-on-exec标志为0的情况下,此文件不被关闭。非零则在exec()后被关闭。默认close-on-exec状态为0,需要通过FD_CLOEXEC设置。*/
354.fcntl(fd,F_SETFD,FD_CLOEXEC);
355.#endif
356.if((ev)&&(ev->fcntl_set))return ev->fcntl_set(ev,fd);
357.#ifdef O_NONBLOCK
/*非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open、read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。对于一个给定的描述符有两种方法对其指定非阻塞I/O:1)如果是调用open以获得该描述符,则可指定O_NONBLOCK标志。2)对于已经打开的一个描述符,则可调用fcntl,对其打开O_NONBOCK文件状态标志。*/
358.return fcntl(fd,F_SETFL,O_NONBLOCK|O_RDWR);
359.#else
360.return 0;
361.#endif
362.}

          另外,为了对所有I/O模型的事件进行统一,Lighttpd开发者对描述符事件标识进行了自己的定义,这是必需的,如select、kqueue这两种模型没有对POLLIN、POLLOUT这样的宏来指代事件,还有另外几种模型的这些同类宏值(如POLLIN和EPOLLIN)未必就完全相等,所以需要将它们统一标识(事实上,从各模型的实现文件里可以看出,Lighttpd里是以POLLIN这类宏为基准)。如清单8-22所示。值得注意的是,在对事件是否已经发生进行判断的时,采用的方法是掩码(与)操作,而不是单纯的数字比较,这点在后面的代码分析时可以看到。
清单8-22 I/O模型事件标识

//Settings.h
363.#define BV(x)(1<<x)
364.
365.//fdevent.h
366.#define FDEVENT_IN BV(0)
367.#define FDEVENT_PRI BV(1)
368.#define FDEVENT_OUT BV(2)
369.#define FDEVENT_ERR BV(3)
370.#define FDEVENT_HUP BV(4)
371.#define FDEVENT_NVAL BV(5)
372.
///usr/include/bits/poll.h
373./*Event types that can be polled for.These bits may be set inevents'
374.to indicate the interesting event types;they will appear inrevents'
375.to indicate the status of the file descriptor.*/
376.#define POLLIN 0x001/*There is data to read.*/
377.#define POLLPRI 0x002/*There is urgent data to read.*/
378.#define POLLOUT 0x004/*Writing now will not block.*/
379.#ifdef__USE_XOPEN
380./*These values are defined in XPG4.2.*/
381.#define POLLRDNORM 0x040/*Normal data may be read.*/
382.#define POLLRDBAND 0x080/*Priority data may be read.*/
383.#define POLLWRNORM 0x100/*Writing now will not block.*/
384.#define POLLWRBAND 0x200/*Priority data may be written.*/
385.#endif
386.#ifdef__USE_GNU
387./*These are extensions for Linux.*/
388.#define POLLMSG 0x400
389.#define POLLREMOVE 0x1000
390.#define POLLRDHUP 0x2000
391.#endif
392./*Event types always implicitly polled for.These bits need not be set in
393.events',but they will appear inrevents'to indicate the status of
394.the file descriptor.*/
395.#define POLLERR 0x008/*Error condition.*/
396.#define POLLHUP 0x010/*Hung up.*/
397.#define POLLNVAL 0x020/*Invalid polling request.*/
398.
///usr/include/sys/epoll.h
/*在Ubuntu上,这两组定义值大部分一致。*/
399.enum EPOLL_EVENTS
400.{
401.EPOLLIN=0x001,
402.#define EPOLLIN EPOLLIN
403.EPOLLPRI=0x002,
404.#define EPOLLPRI EPOLLPRI
405.EPOLLOUT=0x004,
406.#define EPOLLOUT EPOLLOUT
407.EPOLLRDNORM=0x040,
408.#define EPOLLRDNORM EPOLLRDNORM
409.EPOLLRDBAND=0x080,
410.#define EPOLLRDBAND EPOLLRDBAND
411.EPOLLWRNORM=0x100,
412.#define EPOLLWRNORM EPOLLWRNORM
413.EPOLLWRBAND=0x200,
414.#define EPOLLWRBAND EPOLLWRBAND
415.EPOLLMSG=0x400,
416.#define EPOLLMSG EPOLLMSG
417.EPOLLERR=0x008,
418.#define EPOLLERR EPOLLERR
419.EPOLLHUP=0x010,
420.#define EPOLLHUP EPOLLHUP
421.EPOLLRDHUP=0x2000,
422.#define EPOLLRDHUP EPOLLRDHUP
423.EPOLLONESHOT=(1<<30),
424.#define EPOLLONESHOT EPOLLONESHOT
425.EPOLLET=(1<<31)
426.#define EPOLLET EPOLLET
427.};
428.

3.2 I/O多路复用技术模型的使用

       有了整合多个I/O模型的优美结构设计,本节将开始讲述Lighttpd源码是如何使用这个封装结构来搭建起完美的支持多平台多选择的I/O事件处理器。

       首先还是跟随server.c源文件中的main()函数执行过程进行跟踪,与I/O事件处理器使用相关的第一个函数调用为fdevent_init(),如清单8-23所示,该函数调用传递了两个参数分别为最大描述符和当前选择的I/O事件处理模型,这两个值都是根据用户配置和系统环境允许两重条件而得到的值。
清单8-23 I/O事件处理器初始化

//server.c
/*第一个参数取值为srv->max_fds+1而不就是srv->max_fds是因为文件描述符从0开始,所以当最大描述符为n的时候,给它分配的空间必须为n+1,这样才可以做到利用描述符fd作为数组空间索引访问元素(效率为O(1)),这可以接合fdevent_init()函数源码中内存空间分配语句:
ev->fdarray=calloc(maxfds,sizeof(*ev->fdarray));来理解。*/
429.if(NULL==(srv->ev=fdevent_init(srv->max_fds+1,srv->event_handler))){
430.log_error_write(srv,__FILE__,__LINE__,
431."s","fdevent_init failed");
432.return-1;
433.}

fdevent_init()函数进行的是fdevents结构体初始化,这在上一小节已经讲过。随着server.c源文件中的main()函数往下看,接着fdevent_init()函数调用马上就是另一个与I/O事件处理器密切相关的network_register_fdevents()函数调用,该函数功能很明显,用于将所有网络监听描述符(是指服务器端建立的监听Socket,用于接受客户端连接请求,读者先不用理会这些描述符具体是怎么来的,后面章节将统一讲解)注册到事件处理器中,如清单8-24所示。
清单8-24 网络监听描述符注册事件处理器

//server.c
434./*
435.*kqueue()is called here,select resets its internals,
436.*all server sockets get their handlers
437.*
438.**/
439.if(0!=network_register_fdevents(srv)){
440.plugins_free(srv);
441.network_close(srv);
442.server_free(srv);
443.return-1;
444.}
//network.c
445.int network_register_fdevents(server*srv){
446.size_t i;
447.if(-1==fdevent_reset(srv->ev)){
448.return-1;
449.}
450./*register fdevents after reset*/
451.for(i=0;i<srv->srv_sockets.used;i++){
452.server_socket*srv_socket=srv->srv_sockets.ptr[i];
453.fdevent_register(srv->ev,srv_socket->fd,network_server_handle_fdevent,
srv_socket);
454.fdevent_event_add(srv->ev,&(srv_socket->fde_ndx),srv_socket->fd,
FDEVENT_IN);
455.}
456.return 0;
457.}

       network_register_fdevents()函数先调用函数fdevent_reset()对fdevents类型变量ev进行重置,然后调用了另外两个函数来完成将各个网络监听描述符注册到事件处理器中的任务,如清单8-25所示。fdevent_register()函数实现的功能很容易理解,就是新建一个fdnode并利用传入的参数初始化后存入ev的fdarray数组字段。这里有两点值得注意。第一,fdnode存入fdarray数组时是直接插入到以fd为索引的元素空间,这样在事件触发处理函数知道其fd的情况下可以快速地找到与之相关的ctx等信息。第二,传入的第三个参数handler为fdevent_handler类型,这是一个函数指针类型,这里传递的是函数network_server_handle_fdevent()的地址,赋值给fdnode的handler字段后,在fd描述符上有事件触发时会进行回调处理(后面章节统一讨论该回调函数的具体内容)。函数fdevent_event_add()前面已经讨论过了,它间接地调用各I/O模型的私有函数向fdevents中添加一个描述符fd,并且对这个描述符关心的事件是FDEVENT_IN(可读事件),即当有客户端发送请求连接导致网络监听描述符可读时事件触发,另外注意它的第二个参数传的是地址,因此srv_socket->fde_ndx有可能在函数调用中被修改,各模型略有不同,留做后面具体分析。

清单8-25 事件注册与添加

//fdevent.c
458.int fdevent_register(fdevents*ev,int fd,fdevent_handler handler,void*ctx){
459.fdnode*fdn;
/*
//fdevent.c
fdnode*fdnode_init(){
fdnode*fdn;
fdn=calloc(1,sizeof(*fdn));
fdn->fd=-1;
return fdn;
}
//fdevent.h
typedef handler_t(*fdevent_handler)(void*srv,
void*ctx,int revents);
*/
460.fdn=fdnode_init();
461.fdn->handler=handler;
462.
463.fdn->fd=fd;
464.fdn->ctx=ctx;
465.ev->fdarray[fd]=fdn;
466.return 0;
467.}
468.
469.
470.int fdevent_event_add(fdevents*ev,int*fde_ndx,int fd,int events){
471.int fde=fde_ndx?*fde_ndx:-1;
472.if(ev->event_add)fde=ev->event_add(ev,fde,fd,events);
473.if(fde_ndx)*fde_ndx=fde;
474.return 0;
475.}

        初始化和注册都完成了,返回到server.c源文件往下查阅,遇到fdevent_fcntl_set()函数,该函数用于设置网络监听描述符的属性(比如FD_CLOEXEC以及O_NONBLOCK标记),对于RTSIG模型还有更实际的作用(后面内容会讲到),其他模型都是空执行,如清单8-26所示。
清单8-26 调用函数fdevent_fcntl_set设置网络监听描述符属性

//server.c
476.for(i=0;i<srv->srv_sockets.used;i++){
477.server_socket*srv_socket=srv->srv_sockets.ptr[i];
478.if(-1==fdevent_fcntl_set(srv->ev,srv_socket->fd)){
479.log_error_write(srv,__FILE__,__LINE__,"ss","fcntl failed:",
strerror(errno));
480.return-1;
481.}
482.}
483.

如果顺着main()函数继续往下看,在一个大的while循环内(事实上这也就是工作进程的服务主循环,后面章节将会详细讲解)看到对fdevent_poll()函数的调用,如代码段8-27所示,终于轮到I/O事件处理器的主角出场了,下面就来分析这段代码。前面已经说过,函数fdevent_poll()用于轮询等待事件发生,等待的时间1000微秒,在这段时间内如果有关心的事件发生,则函数返回事件数目到变量n,此时if为真执行其下代码,否则判断如果调用不是被信号所中断则记录失败错误信息。在等待的时间内有事件(一个或多个)发生,则利用do{}while(--n>0);对它们逐个进行处理,这个处理过程已经直接详细地注释在清单8-27所示的代码里。

清单8-27 循环检测描述符事件发生

//server.c
484.if((n=fdevent_poll(srv->ev,1000))>0){
485./*n is the number of events*/
486.int revents;
487.int fd_ndx;
488.#if 0
489.if(n>0){
490.log_error_write(srv,__FILE__,__LINE__,"sd",
491."polls:",n);
492.}
493.#endif
494.fd_ndx=-1;/*待处理事件初始索引。*/
495.do{
496.fdevent_handler handler;
497.void*context;
498.handler_t r;
/*获得下一个(如果是循环内第一次调用则获取第一个)待处理事件。*/
499.fd_ndx=fdevent_event_next_fdndx(srv->ev,fd_ndx);
/*获得待处理事件的类型,主要是将各模型的不同事件宏统一到作者自定义的FDEVENT_IN、FDEVENT_OUT、FDEVENT_ERR等这些宏。*/
500.revents=fdevent_event_get_revent(srv->ev,fd_ndx);
/*获得与待处理事件相关的描述符。*/
501.fd=fdevent_event_get_fd(srv->ev,fd_ndx);
/*获得与待处理事件相关的回调函数。*/
502.handler=fdevent_get_handler(srv->ev,fd);
/*获得与待处理事件相关的上下文context。*/
503.context=fdevent_get_context(srv->ev,fd);
504.
505./*connection_handle_fdevent needs a joblist_append*/
506.#if 0
507.log_error_write(srv,__FILE__,__LINE__,"sdd",
"event for",fd,revents);
508.#endif
/*调用回调函数对待处理事件进行处理。*/
509.switch(r=(*handler)(srv,context,revents)){
510.case HANDLER_FINISHED:
511.case HANDLER_GO_ON:
512.case HANDLER_WAIT_FOR_EVENT:
513.case HANDLER_WAIT_FOR_FD:
514.break;
515.case HANDLER_ERROR:
516./*should never happen*/
517.SEGFAULT();
518.break;
519.default:
520.log_error_write(srv,__FILE__,__LINE__,"d",r);
521.break;
522.}
523.}while(--n>0);/*对所有发生事件逐个处理。*/
524.}else if(n<0&&errno!=EINTR){
525.log_error_write(srv,__FILE__,__LINE__,"ss",
526."fdevent_poll failed:",
527.strerror(errno));
528.}

如果某工作进程结束或者Lighttpd程序退出(此时所有工作进程结束),各工作进程结束前要进行一些清理工作,当然也包括对I/O事件处理器内事件的注销和存储空间的释放。在main()函数末尾有对network_close()调用,该函数内调用函数fdevent_event_del()和fdevent_unregister()对I/O事件进行删除和注销,如清单8-28所示。在main()函数最后通过server_free()函数间接调用fdevent_free()函数释放I/O事件处理器存储空间。

清单8-28 描述符监控事件注销

529.//network.c
530.int network_close(server*srv){
531.size_t i;
532.for(i=0;i<srv->srv_sockets.used;i++){
533.server_socket*srv_socket=srv->srv_sockets.ptr[i];
534.if(srv_socket->fd!=-1){
535./*check if server fd are already registered*/
536.if(srv_socket->fde_ndx!=-1){
537.fdevent_event_del(srv->ev,&(srv_socket->fde_ndx),srv_socket->fd);
538.fdevent_unregister(srv->ev,srv_socket->fd);
539.}
540.close(srv_socket->fd);
541.}
542.buffer_free(srv_socket->srv_token);
543.free(srv_socket);
544.}
545.free(srv->srv_sockets.ptr);
546.return 0;
547.}

       对于Lighttpd应用程序使用I/O事件处理器对描述符进行的事件监听管理基本就是以上分析的这个流程,当然这里只是以服务器端建立的网络监听描述符(并且未考虑系统资源环境的影响,比如如果资源不足则有些监听描述符要提前关闭等)进行的事件管理为例子,对于在Lighttpd应用程序里其他类型的描述符(如与FAM相关的描述符、接受客户端请求后建立的描述符以及有些插件相关描述符等)上使用I/O事件处理器也采用同样的流程,而且它们之间大部分已经重叠(即在对事件进行处理的那部分都在工作进程的主循环内)在一起。图8-7给出了网络监听描述符上使用I/O事件处理器的整个流程。

        

3.3 六种I/O多路复用技术模型的实现

        3.1 节曾经讲过,Lighttpd里利用fdevents结构体将六种I/O多路复用技术模型(select、poll、epoll、kqueue、/dev/poll、rtsig)的具体实现进行了统一的封装与信息隐藏,对外仅提供统一的公开调用接口函数,上一节讲的就是这些公开接口的调用流程,本小节将透过这些接口而对各I/O模型在Lighttpd的具体实现进行解析(仍以常规的网络监听描述符上多路复用为例)。

       1.SELECT模型实现

        在清单8-29中的函数执行流程中,最先对SELECT模型有实质作用的是函数fdevent_reset()和fdevent_event_add(),前者调用SELECT模型实现里的fdevent_select_reset()函数,后者调用fdevent_select_event_add()函数,这两个函数具体代码如清单8-29所示。fdevent_select_reset()函数实现对三个描述符集的重置功能;fdevent_select_event_add()函数先要保证描述符fd不大于FD_SETSIZE,而后根据关注事件类型将描述符加入到对应的描述符集中。

        清单8-29 函数fdevent_reset、fdevent_event_add

        //fdevent_select.c
548.static int fdevent_select_reset(fdevents*ev){
549.FD_ZERO(&(ev->select_set_read));
550.FD_ZERO(&(ev->select_set_write));
551.FD_ZERO(&(ev->select_set_error));
552.ev->select_max_fd=-1;
553.return 0;
554.}
555.
556.static int fdevent_select_event_add(fdevents*ev,int fde_ndx,int fd,int events){
557.UNUSED(fde_ndx);
558./*we should be protected by max-fds,but you never know*/
559.assert(fd<FD_SETSIZE);/*保证描述符fd不大于FD_SETSIZE。*/
560.if(events&FDEVENT_IN){/*关注描述符可读事件。*/
561.FD_SET(fd,&(ev->select_set_read));/*加入到可读事件描述符集。*/
562.FD_CLR(fd,&(ev->select_set_write));/*从可写描述符集中清除。*/
563.}
564.if(events&FDEVENT_OUT){/*关注描述符可写事件。*/
565.FD_CLR(fd,&(ev->select_set_read));/*从可读描述符集中清除。*/
566.FD_SET(fd,&(ev->select_set_write));/*加入到可写事件描述符集。*/
567.}
/*不管关注可读事件还是可写事件,都将描述符加入到异常描述符集。*/
568.FD_SET(fd,&(ev->select_set_error));
/*如有必要,更新最大描述记录。*/
569.if(fd>ev->select_max_fd)ev->select_max_fd=fd;
/*返回文件描述符,其将记录在server_socket结构体的fde_ndx字段内。*/
570.return fd;
571.}

     按图8-7的函数执行流程来看,第二个对SELECT模型有作用的是函数fdevent_poll(),该函数调用的fdevent_select_poll()函数,这个函数就不用多说了,其调用select()系统函数执行SELECT模型的事件等待过程,如清单8-30所示。
清单8-30 函数fdevent_poll

       //fdevent_select.c
572.static int fdevent_select_poll(fdevents*ev,int timeout_ms){
573.struct timeval tv;/*超时时间。*/
574.tv.tv_sec=timeout_ms/1000;
575.tv.tv_usec=(timeout_ms%1000)*1000;
/*根据SELECT模型的特点(8.2.2节),需要保留原来的描述符集而使用临时变量,避免每次都要重新添加关注。这里采用的是结构体赋值,可能在许多读者的印象中,C语言里的结构体的赋值通过成员逐个赋值或memcpy()等内存复制函数来完成的,事实上ANSI C标准支持这种结构体的直接赋值,ANSI C对K&R C作了很多的修订,使得C语言逐步走向完善和成熟。*/
576.ev->select_read=ev->select_set_read;
577.ev->select_write=ev->select_set_write;
578.ev->select_error=ev->select_set_error;
/*第一个参数为最大描述符加1。*/
579.return select(ev->select_max_fd+1,&(ev->select_read),&(ev->select_write),
580.&(ev->select_error),&tv);
581.}

     如果在等待的时间过程中有事件发生,那么将相应的调用函数fdevent_select_event_next_fdndx()、fdevent_select_event_get_revent()、fdevent_select_event_get_fd()三个函数来获取事件信息,然后利用回调函数handler()进行处理,如清单8-31所示。
清单8-31 获取发生事件信息

//fdevent_select.c
582.static int fdevent_select_event_next_fdndx(fdevents*ev,int ndx){
583.int i;
/*获得下一个(第一次调用时,参数为-1,以获取第一个)待处理事件,由于SELECT模型的特殊性可以知道这个值也就是发生事件的对应描述符。*/
584.i=(ndx<0)?0:ndx+1;
585.for(;i<ev->select_max_fd+1;i++){
586.if(FD_ISSET(i,&(ev->select_read)))break;
587.if(FD_ISSET(i,&(ev->select_write)))break;
588.if(FD_ISSET(i,&(ev->select_error)))break;
589.}
590.return i;
591.}
592.
593.static int fdevent_select_event_get_revent(fdevents*ev,size_t ndx){
594.int revents=0;
/*获得待处理事件的类型,各模型的不同事件宏已经统一,因此这里将对应事件转换为FDEVENT_IN、FDEVENT_OUT、FDEVENT_ERR宏。*/
595.if(FD_ISSET(ndx,&(ev->select_read))){
596.revents|=FDEVENT_IN;/*可读事件。*/
597.}
598.if(FD_ISSET(ndx,&(ev->select_write))){
599.revents|=FDEVENT_OUT;/*可写事件。*/
600.}
601.if(FD_ISSET(ndx,&(ev->select_error))){
602.revents|=FDEVENT_ERR;/*异常事件。*/
603.}
604.return revents;
605.}
606.
607.static int fdevent_select_event_get_fd(fdevents*ev,size_t ndx){
608.UNUSED(ev);
/*由于SELECT模型的特殊性可以知道ndx就是发生事件的对应描述符,直接返回。*/
609.return ndx;
610.}

在最后,如果进程退出则会调用fdevent_select_event_del()函数进行清理工作,源码如清单8-32所示。
清单8-32 监控事件清理

//fdevent_select.c
611.static int fdevent_select_event_del(fdevents*ev,int fde_ndx,int fd){
612.if(fde_ndx<0)return-1;
613.FD_CLR(fd,&(ev->select_set_read));
614.FD_CLR(fd,&(ev->select_set_write));
615.FD_CLR(fd,&(ev->select_set_error));
/*该函数执行成功也返回-1,该值返回之后将被赋值给结构体server_socket的fde_ndx字段,表示监听描述符未加入事件管理器监控中。*/
616.return-1;
617.}

实现SELECT模型的完整函数执行流程如图8-8所示。

2.POLL模型实现

POLL模型的处理比SELECT模型复杂一点,因为它使用了一个pollfd结构体来记录关注事件和返回发生事件。对于POLL模型没有额外的初始化和重置操作,一开始就调用函数fdevent_poll_event_add()用于将关注描述符及事件作为一个添加元素到pollfd结构体数组内,如清单8-33所示。
清单8-33 函数fdevent_poll_event_add

//fdevent_poll.c
618.static int fdevent_poll_event_add(fdevents*ev,int fde_ndx,int fd,int events){
619./*known index*/
/*已经知道该描述符对应元素在数组中的索引,比如在对一个已经添加到监控的描述符关注事件进行修改的时候。*/
620.if(fde_ndx!=-1){
621.if(ev->pollfds[fde_ndx].fd==fd){
622.ev->pollfds[fde_ndx].events=events;
623.return fde_ndx;
624.}
625.fprintf(stderr,"%s.%d:add:(%d,%d)\n",__FILE__,__LINE__,
626.fde_ndx,ev->pollfds[fde_ndx].fd);
627.SEGFAULT();
628.}
/*新加入监控,则需判断是否要分配存储内存。*/
629.if(ev->unused.used>0){
630.int k=ev->unused.ptr[--ev->unused.used];
631.ev->pollfds[k].fd=fd;
632.ev->pollfds[k].events=events;
633.return k;
634.}else{
635.if(ev->size==0){
636.ev->size=16;
637.ev->pollfds=malloc(sizeof(*ev->pollfds)*ev->size);
638.}else if(ev->size==ev->used){
639.ev->size+=16;
640.ev->pollfds=realloc(ev->pollfds,sizeof(*ev->pollfds)*ev->size);
641.}
642.ev->pollfds[ev->used].fd=fd;
643.ev->pollfds[ev->used].events=events;
644.return ev->used++;
645.}
646.}

      关注事件添加好之后,接下来就要进入事件等待发生过程,处理这个过程的是函数fdevent_poll_poll(),如清单8-34所示。其内的函数fdevent_poll_event_compress()由于恒假宏的作用永远也不会被调用,事实上这个函数在整个Lighttpd源码里都没有被调用过,是个已经废弃了的函数。
清单8-34 函数fdevent_poll_poll

     //fdevent_poll.c
647.static int fdevent_poll_poll(fdevents*ev,int timeout_ms){
648.#if 0
649.fdevent_poll_event_compress(ev);
650.#endif
651.return poll(ev->pollfds,ev->used,timeout_ms);
652.}

       如果等待超时则下一个工作循环继续等待,只要有事件发生那么将调用相应的函数获取发生事件相关信息并进行处理,如清单8-35所示。
清单8-35 获取发生事件相关信息

//fdevent_poll.c
653.static int fdevent_poll_event_next_fdndx(fdevents*ev,int ndx){
654.size_t i;
/*获得下一个(第一次调用时,参数为-1,以获取第一个)待处理事件。只要revents不为0就表示对应的描述符上发生了关注事件。*/
655.i=(ndx<0)?0:ndx+1;
656.for(;i<ev->used;i++){
657.if(ev->pollfds[i].revents)break;
658.}
659.return i;
660.}
661.
662.static int fdevent_poll_event_get_revent(fdevents*ev,size_t ndx){
663.int r,poll_r;
664.if(ndx>=ev->used){/*越界异常错误。*/
665.fprintf(stderr,"%s.%d:dying because:event:%zd>=%zd\n",__FILE__,
666.__LINE__,ndx,ev->used);
667.SEGFAULT();
668.return 0;
669.}
670.if(ev->pollfds[ndx].revents&POLLNVAL){/*文件未打开异常错误。*/
671./*should never happen*/
672.SEGFAULT();
673.}
/*Lighttpd开发者在这写了一段无用的代码,最后应该是return r;,这样才完成所谓的POLL*到FDEVEN_*的映射转换,但是此函数仍能正常工作说明POLL*和FDEVEN_*的值刚好一一恒等对应,即是y=f(x)=x函数的映射关系。*/
674.r=0;
675.poll_r=ev->pollfds[ndx].revents;
676./*map POLL*to FDEVEN_**/
677.if(poll_r&POLLIN)r|=FDEVENT_IN;
678.if(poll_r&POLLOUT)r|=FDEVENT_OUT;
679.if(poll_r&POLLERR)r|=FDEVENT_ERR;
680.if(poll_r&POLLHUP)r|=FDEVENT_HUP;
681.if(poll_r&POLLNVAL)r|=FDEVENT_NVAL;
682.if(poll_r&POLLPRI)r|=FDEVENT_PRI;
683.return ev->pollfds[ndx].revents;
684.}
/*获取发生事件的对应描述符,描述符存储在结构体pollfd的fd字段内。*/
685.static int fdevent_poll_event_get_fd(fdevents*ev,size_t ndx){
686.return ev->pollfds[ndx].fd;
687.}

         最后再来看看POLL模型的清理工作实现函数,对某个监控描述符的注销工作通过将对应结构体pollfd的fd字段设置为-1来完成,这样poll()函数自动忽略这样的结构体,如清单8-36所示。
清单8-36 监控事件清理

  //fdevent_poll.c
688.static int fdevent_poll_event_del(fdevents*ev,int fde_ndx,int fd){
689.if(fde_ndx<0)return-1;
690.if((size_t)fde_ndx>=ev->used){
691.fprintf(stderr,"%s.%d:del!out of range%d%zd\n",__FILE__,__LINE__,
692.fde_ndx,ev->used);
693.SEGFAULT();
694.}
695.if(ev->pollfds[fde_ndx].fd==fd){
696.size_t k=fde_ndx;
697.ev->pollfds[k].fd=-1;/*描述符设置为-1。*/
/*这些工作由poll()函数自动完成,无须程序显示设置。*/
698./*ev->pollfds[k].events=0;*/
699./*ev->pollfds[k].revents=0;*/
700.if(ev->unused.size==0){
701.ev->unused.size=16;
702.ev->unused.ptr=malloc(sizeof(*(ev->unused.ptr))*ev->unused.size);
703.}else if(ev->unused.size==ev->unused.used){
704.ev->unused.size+=16;
705.ev->unused.ptr=realloc(ev->unused.ptr,
706.sizeof(*(ev->unused.ptr))*ev->unused.size);
707.}
/*加入到未使用队列,便于重复利用空间。*/
708.ev->unused.ptr[ev->unused.used++]=k;
709.}else{
710.SEGFAULT();
711.}
712.return-1;
713.}
714.
715.static void fdevent_poll_free(fdevents*ev){
716.free(ev->pollfds);/*申请的存储空间需要释放,防止内存泄漏。*/
717.if(ev->unused.ptr)free(ev->unused.ptr);
718.}
719.

实现POLL模型的完整函数执行流程如图8-9所示。

图 8-9 实现POLL模型的完整函数执行流程

3.EPOLL模型实现

       与EPOLL模型相关的系统调用函数有三个,分别为epoll_create()、epoll_ctl()以及epoll_wait(),如何合理地将这三个系统调用分配到EPOLL模型实现代码中是本小节讨论的重点

        首先看到,在EPOLL模型的初始化实现函数fdevent_linux_sysepoll_init()(该函数源码已经在前面章节列出)内不仅只是对结构体fdevents的赋值初始化,函数epoll_create()也在该函数内被调用,用于生成一个epoll专用的文件描述符,并且调用函数fcntl()对该描述符的FD_CLOEXEC标记进行设置,这样在进行exec()调用后,epoll文件描述符自动被关闭。

         EPOLL模型中将描述符事件添加到关注列表是通过系统调用epoll_ctl()函数,它在函数fdevent_linux_sysepoll_event_add()执行以完成关注事件添加功,如清单8-37所示。

          清单8-37 函数fdevent_linux_sysepoll_event_add

//fdevent_linux_sysepoll.c
720.static int fdevent_linux_sysepoll_event_add(fdevents*ev,int fde_ndx,int
fd,int events){
721.struct epoll_event ep;
722.int add=0;
723.if(fde_ndx==-1)add=1;/*没有指定存储索引即表示是新增关注描述符。*/
724.memset(&ep,0,sizeof(ep));/*初始化为全0。*/
725.ep.events=0;
/*事件类型映射到统一宏处理。*/
726.if(events&FDEVENT_IN)ep.events|=EPOLLIN;
727.if(events&FDEVENT_OUT)ep.events|=EPOLLOUT;
728./**
729.*with EPOLLET we don't get a FDEVENT_HUP
730.*if the close is delay after everything has
731.*sent.
732.*/
/*使用默认的水平触发模式而不是边缘触发模式,使用EPOLLET无法判断socket连接客户端半断开情况。EPOLLET触发模式遇到的问题是在客户端半关闭TCP连接时,某些情况下服务器端无法获得这个连接断开事件。比如在客户端发送出(write())少量数据并且执行半关闭(shutdown())操作时,服务器端仅获取一个EPOLLIN事件(即将客户端发送的少量数据和连接终止看成一个可读事件)而无法获知连接终止发生。针对这个情况,在kernel 2.6.17里提供了一个新的标识符EPOLLRDHUP,不过由于在Lighttpd源码里并没有将这个标识符统一加入进来,因此Lighttpd开发者直接使用水平触发模式。*/
733.ep.events|=EPOLLERR|EPOLLHUP/*|EPOLLET*/;
734.ep.data.ptr=NULL;
735.ep.data.fd=fd;
/*添加或修改事件监控描述符。*/
736.if(0!=epoll_ctl(ev->epoll_fd,add?EPOLL_CTL_ADD:EPOLL_CTL_MOD,fd,&ep)){
737.fprintf(stderr,"%s.%d:epoll_ctl failed:%s,dying\n",__FILE__,__LINE__,
738.strerror(errno));
739.SEGFAULT();
740.return 0;
741.}
742.return fd;
743.}

        EPOLL模型中等待事件发生的系统调用epoll_wait()直接包装在函数fdevent_linux_sysepoll_poll内,发生的事件后,同样先要获取发生事件的相关信息并进行处理,如清单8-38所示。
清单8-38 获取发生事件的相关信息

         //fdevent_linux_sysepoll.c
744.static int fdevent_linux_sysepoll_poll(fdevents*ev,int timeout_ms){
745.return epoll_wait(ev->epoll_fd,ev->epoll_events,ev->maxfds,timeout_ms);
746.}
747.
748.static int fdevent_linux_sysepoll_event_next_fdndx(fdevents*ev,int ndx){
749.size_t i;
750.UNUSED(ev);
/*获得下一个(第一次调用时,参数为-1,以获取第一个)待处理事件。由于EPOLL模型仅返回发生事件了的描述符,因此逐个增1即可。*/
751.i=(ndx<0)?0:ndx+1;
752.return i;
753.}
754.
755.static int fdevent_linux_sysepoll_event_get_revent(fdevents*ev,size_t ndx){
756.int events=0,e;
757.e=ev->epoll_events[ndx].events;/*事件。*/
758.if(e&EPOLLIN)events|=FDEVENT_IN;/*事件映射到统一宏值。*/
759.if(e&EPOLLOUT)events|=FDEVENT_OUT;
760.if(e&EPOLLERR)events|=FDEVENT_ERR;
761.if(e&EPOLLHUP)events|=FDEVENT_HUP;
762.if(e&EPOLLPRI)events|=FDEVENT_PRI;
763.return e;
764.}
765.
766.static int fdevent_linux_sysepoll_event_get_fd(fdevents*ev,size_t ndx){
767.#if 0
768.fprintf(stderr,"%s.%d:%d,%d\n",__FILE__,__LINE__,
769.ndx,ev->epoll_events[ndx].data.fd);
770.#endif
771.return ev->epoll_events[ndx].data.fd;
772.}

      监控描述符的注销也要利用epoll_ctl()函数,不过传给它的操作类型值为EPOLL_CTL_DEL,如清单8-39所示。
     清单8-39 监控事件清理

//fdevent_linux_sysepoll.c
773.static int fdevent_linux_sysepoll_event_del(fdevents*ev,int fde_ndx,int fd){
774.struct epoll_event ep;
775.if(fde_ndx<0)return-1;
776.memset(&ep,0,sizeof(ep));
777.ep.data.fd=fd;
778.ep.data.ptr=NULL;
779.if(0!=epoll_ctl(ev->epoll_fd,EPOLL_CTL_DEL,fd,&ep)){
/*保存删除监控描述符。*/
780.fprintf(stderr,"%s.%d:epoll_ctl failed:%s,dying\n",__FILE__,__LINE__,
781.strerror(errno));
782.SEGFAULT();
783.return 0;
784.}
785.return-1;
786.}
787.
788.static void fdevent_linux_sysepoll_free(fdevents*ev){
789.close(ev->epoll_fd);
/*在使用完epoll后,必须调用close()关闭这个描述符,否则可能导致系统描述符被耗尽。*/
790.free(ev->epoll_events);/*释放存储空间。*/
791.}

实现EPOLL模型的完整函数执行流程如图8-10所示。

图 8-10 实现EPOLL模型的完整函数执行流程

4.KQUEUE模型实现

5./DEV/POLL模型实现

6.RTSIG模型实现

4 本章总结

     本章首先介绍了目前UNIX/Linux下可用的I/O 5种模型分别为阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O、异步I/O;接着对UNIX/Linux平台常用的I/O复用实现技术包括select/poll、/dev/poll、kqueue、epoll以及实时信号驱动I/O(即rtsig)的使用做了较为详细的使用讲解;在本章最后,对Lighttpd中支持多平台多选择的I/O事件处理器的设计、实现与使用结合源码给出了详细的解析。下一章将解析Lighttpd的网络服务响应请求流程的具体细节内容。

参考文档 lighttpd源码分析 高群凯著          

下载路径:https://download.csdn.net/download/caofengtao1314/10576306 

lighttpd源码下载 :https://download.csdn.net/download/caofengtao1314/10560484

猜你喜欢

转载自blog.csdn.net/caofengtao1314/article/details/82876057