lighttpd 之八 文件状态缓存器

1 概述

     在web server中,最重要的是保存在服务器里供客户端请求的文件资源,包括静态页面(如HTML文件、

文本文件、world文件等)、动态文件(如PHP、JSP、ASP等)还有图片文件、声音视频多媒体文件等。存在于

服务器上的这些文件又是动态变化的,比如某一时刻文件被删除或被修改,Web Server必须时刻知道这些变化

的发生,才能对客户端的请求作出正确的响应。举个例子,客户端请求访问一个服务器上并不存在的文件资料,

Web Server必须返回一个错误。Web server如何知道这个文件不存在呢?Web Server可以去文件系统对应路径查找,

但是对每次客户端的请求,Web Server都去磁盘查找,其效率当然不会理想,因此,文件缓存器的作用就是客户端

请求过的文件资源的信息缓存在内存中,对应客户端的文件资源请求先查询缓存器,因为有了这个设计,Web Server

性能也就能有一定的提升。另外,文件状态缓存器里保存的文件资源信息必须是正确的,也就是要和文件系统同步(

某文件资源之前存在,但是此刻却被Web站点管理员删除了,这个变化必须被实时的反应到缓存器中),即文件系统

里文件资源的变化必须要及时地更新到文件状态缓存器中。Lighttpd源码里设计的文件状态缓存器对于这些问题已经

考虑到了,下面就开始解析这部分以及相关内容。

          本节相关部分源码:

           base.h

           etag.h

           stat_cache.h

           etag.c

           stat_cache.c

2 ETag知识

    在开始分析文件状态缓存器源码之前,先来讲解两个Lighttpd源码文件etag.h和etag.c。虽然这两个文件代码量不多,但是其

涉及关于HTTP协议中的一个很重要的设计,这个设计应该在所有的成熟Web Server中都实现,在Lighttpd中也不例外。

2.1 Etag的定义 

      ETag的定义出现在RFC 2616文档的14.19 ETag一节。原文如下(第一句):The ETag response-header field provides the current value of the entity tag for the requested variant.(ETag响应头域提供了请求对应变量(variant)的当前实体标签。)我们可以简单地认为ETag是一个与某一时刻的Web资源相关联的记号(token)。ETag存在的意义就在于能够通过比较某Web资源前后两次对应ETag值让Web服务器获知该Web资源在这段间隔时间内是否有变化发生,而具体如何做到这点就看ETag怎么与某一时刻的Web资源去相对应,即如何由某一时刻的Web资源来生成其对应的ETag(RFC 2616对ETag并没有具体值的定义,对于Lighttpd里面如何生成ETag,请待后面的源码分析)。

2.2 Etag的功能

          让Web服务器获知Web资源是否有变化发生有什么作用?答案就是提高Web服务器性能,节约网络带宽。既然在客户端前后两次资源请求之间的这段时间内被请求的Web资源都没有发生变化,Web服务器当然可以直接返回一个状态码告诉客户端:“该资源自上次传送给你之后未发生任何变化,你可以直接使用本地缓存的副本。”从而Web服务器简简单单的一个状态码返回就完成了一次客户端请求。
再完整地看一下带有ETag的Web服务器端和客户端之间请求响应流程,如下所示。

                                   第一次请求
1)客户端发起一个HTTP GET请求(假设请求一个文件A)。
2)Web服务器处理请求并返回响应头和文件内容,其中响应头里就包括有ETag(例如"6a3d8e-47c-6ba-587624053"),状态码200。
3)客户端收到Web服务器响应,除了展示内容给最终用户外还会将文件内容、ETag等响应信息缓存起来。
                            第二次请求

1)客户端第二次发起HTTP GET请求(当然仍然请求的是文件A),此次客户端除了发送常规请求头外,还会发送一个If-None-Match头,其内容就是第一次请求时服务器返回的ETag"6a3d8e-47c-6ba-587624053"。
2)Web服务器判断客户端发送过来的ETag和此刻重新计算出来的ETag是否匹配,如果匹配则表示自客户端第一次请求文件A之后,文件A未发生任何有影响的(之所以这么说,看了之后的内容就懂了)变化,因此If-None-Match为False,于是Web服务器直接返回304。

3)客户端收到Web服务器304状态码响应,知道Web服务器上的文件A未发生变化,于是继续使用本地缓存。

          下图是利用Lighttpd做Web服务器时某客户端对站点上同一index.html文件请求两次的具体请求响应消息内容,我们可以看到在该客户端第二次请求index.html文件内容时,Lighttpd服务器并没有发送任何实体数据到客户端,而客户端直接使用本地缓存数据。

2.3 Etag的优势    

       在HTTP协议里,与ETag非常类似的另一个头域为Last-Modified头域。Last-Modified头域(结合If-Modified-Since头域使用)也能完成ETag上面介绍的功能,但是ETag更灵活(即ETag提供更灵活的验证模式)。首先,Last-Modified对文件的新旧检查只能到秒一级。如果文件修改非常频繁,比如在秒以下的时间内进行修改,这种修改Last-Modified无法判断。其次,如果文件的更改对用户的查阅影响并不大(比如一些文件周期性的更改,但是它的内容并不改变而仅仅改变修改时间),也没必要把文件重发给客户端,此时Last-Modified无法做到判断,而使用ETag只需不验证修改时间就可以满足需求(像Apache,可由FileETag指令配置ETag值是由文件的inode(索引节点)、大小、最后修改时间之一或它们的组合来确定)。另外,有些服务器并不能精确地得到文件的最后修改时间。
       当一个请求里ETag和Last-Modified都存在时,只有两者各自判断都满足返回304状态码的情况下,Web服务器才能返回304状态码。另外,因为ETag的生成计算也是需要发费Web服务器时间的,所以在实际的Web服务器使用中需要权衡处理。

2.4 Lighttpd中ETag的实现

        从上面叙述可知ETag就是一个字符串,因此Lighttpd中并没有关于ETag的结构化定义,直接采用buffer数据结构体来存储ETag。Lighttpd中关于ETag的实现源码也很简单,仅包含三个函数。

1.int etag_is_equal(buffer*etag,const char*matches)         

        该函数(如清单6-1所示)比较一个非空ETag是否与指定字符串相匹配,匹配返回1,否则返回0。
       清单6-1 函数etag_is_equal       

//etag.c
/*比较一个非空ETag是否与指定字符串相匹配,匹配返回1,否则返回0。*/
1.int etag_is_equal(buffer*etag,const char*matches){
2.if(etag&&!buffer_is_empty(etag)&&0==strcmp(etag->ptr,matches))return 1;
3.return 0;
4.}

2.int etag_create(buffer*etag,struct stat*st,etag_flags_t flags)

该函数(如清单6-2所示)用于根据指定的某文件状态信息创建对应的ETag(事实上此处创建的ETag还不是发送到客户端的最终值,其通过散列之后的值才是实际当作ETag响应头域域值发送到客户端)。通过判断参数flags值来获取文件状态参数st内指定的数据信息创建ETag并保存到buffer类型参数etag内隐性传出,函数返回0。etag_create函数将被stat_cache.c源文件内的stat_cache_get_entry函数(本章接下来会分析到)调用,创建的ETag保存在文件状态缓存器内。参数flags的取值如何,可根据函数的被调用情况向后回溯,首先是stat_cache.c源文件函数stat_cache_get_entry内的sce->etag,&(sce->st),con->etag_flags);调用,接着可以在configfile.c源文件内看到:

con->etag_flags=(con->conf.etag_use_mtime?ETAG_USE_MTIME:0)|
(con->conf.etag_use_inode?ETAG_USE_INODE:0)|
(con->conf.etag_use_size?ETAG_USE_SIZE:0);

而con->conf.etag_use_mtime、con->conf.etag_use_inode、con->conf.etag_use_size是用户配置信息,对应于用户在Lighttpd配置文件里对etag.use-mtime、etag.use-inode、etag.use-size三个配置项的值设置。因此参数flags的取值如何由用户决定,即用户可以自由指定ETag由Web资源文件的哪些信息生成,这里开发人员提供了三个文件信息(文件最后修改时间、文件结点号、以字节为单位的文件总大小)的组合选择,如果我们觉得还是无法满足特殊的需求则可自行修改该etag_create函数(体现了ETag提供更灵活验证模式的强大)。

清单6-2 函数etag_create

//etag.h
6.typedef enum{ETAG_USE_INODE=1,ETAG_USE_MTIME=2,ETAG_USE_SIZE=4}etag_flags_t;
7.
//etag.c
8.int etag_create(buffer*etag,struct stat*st,etag_flags_t flags){
9.if(0==flags)return 0;
10.
11.buffer_reset(etag);
12.
13.if(flags&ETAG_USE_INODE){
14.buffer_append_off_t(etag,st->st_ino);/*st_ino文件的i-node。*/
15.buffer_append_string_len(etag,CONST_STR_LEN("-"));
16.}
17.
18.if(flags&ETAG_USE_SIZE){
19.buffer_append_off_t(etag,st->st_size);/*st_size文件大小,以字节计算。*/
20.buffer_append_string_len(etag,CONST_STR_LEN("-"));
21.}
22.
23.if(flags&ETAG_USE_MTIME){
/*st_mtime文件最后一次被修改的时间,一般只有在用mknod、utime和write时才会改变。*/
24.buffer_append_long(etag,st->st_mtime);
25.}
26.
27.return 0;
28.}
29.

3.int etag_mutate(buffer*mut,buffer*etag)

     etag_mutate函数(如清单6-3所示)作用就是获取参数etag的hash值,并保存到参数mut中隐性传出。函数局部变量h的uint32_t类型定义出现在库文件stdint.h(/usr/include/stdint.h)中。

      #ifndef__uint32_t_defined

      typedef unsigned int uint32_t;

       接下来是一个hash计算方法<sup>39</sup>,将字符串中字符逐个异或(上一个结果左移5位异或上一个结果右移27位再异或下一个字符值得到下一个结果)得到hash值。在实际应用中,该hash方法(或类似hash计算)得到的hash值是很可靠的,即基本上不会出现两个不同字符串得到同一个hash值的情况。

      //etag.c
30.int etag_mutate(buffer*mut,buffer*etag){
31.size_t i;
32.uint32_t h;
33.
34.for(h=0,i=0;i<etag->used;++i)h=(h<<5)^(h>>27)^(etag->ptr[i]);
35.
36.buffer_reset(mut);
37.buffer_copy_string_len(mut,CONST_STR_LEN("\""));
38.buffer_append_long(mut,h);
39.buffer_append_string_len(mut,CONST_STR_LEN("\""));
40.
41.return 0;
42.}
43.

2.5 Lighttpd中Etag的使用

       Lighttpd中对ETag的使用主要是体现在对比客户端请求消息头域里传递的ETag值和请求文件当前计算出来的ETag值,如果两者相等(可能还接合Last-Modified头域)则直接返回304状态码以减少不必要的数据内容发送。下面,我们就来分析这段实现代码,如清单6-4所示。

         清单6-4 函数http_response_handle_cachable

 44.int http_response_handle_cachable(server*srv,connection*con,buffer*mtime){
45./*
46.*14.26 If-None-Match
47.*[...]
48.*If none of the entity tags match,then the server MAY perform the
49.*requested method as if the If-None-Match header field did not exist,
50.*but MUST also ignore any If-Modified-Since header field(s)in the
51.*request.That is,if no entity tags match,then the server MUST NOT
52.*return a 304(Not Modified)response.
53.*/
/*这里的英文注释来之RFC 2616第14.26节,其表明如果If-None-Match不匹配,那么服务器就不能返回状态码304响应,即使根据If-Modified-Since头域来看好像应该是要返回304,原因就在于前面曾提到的ETag能够比Last-Modified头域提供更多的验证信息,如果ETag不匹配就表示该请求文件已经被更新过,只不过Last-Modified头域仅根据时间来检测判断不出来而已,因此此时服务器不能返回状态码304响应。*/
54./*last-modified handling*/
/*con->request.http_if_none_match为字符指针(const char*)类型变量,初始值为NULL,如果某客户端请求包含有If-None-Match头域,则在解析客户端请求信息后,con->request.http_if_none_match指向If-None-Match头域的字符串域值。
此处判断为真则表示客户端请求包含有If-None-Match头域。*/
55.if(con->request.http_if_none_match){
/*ETag匹配?con->physical.etag存放当前服务器上该请求文件对应的ETag值,con->request.http_if_none_match存放客户端请求头域里的ETag值,此处进行匹配比较。*/
56.if(etag_is_equal(con->physical.etag,con->request.http_if_none_match)){
/*根据RFC 2616第14.26节,If-None-Match头域仅适合GET或HEAD请求方法,对于所有其他方法,服务器必须以412(Precondition Failed,先决条件失败)状态码响应。*/
57.if(con->request.http_method==HTTP_METHOD_GET||
58.con->request.http_method==HTTP_METHOD_HEAD){
59./*check if etag+last-modified*/
/*如果ETag+Last-Modified都存在,则需要两者都完全匹配时,服务器才能返回状态码304。*/
60.if(con->request.http_if_modified_since){
61.size_t used_len;
62.char*semicolon;
/*根据RFC 2616第2.1节,分号后是注释信息,需要截断。*/
63.if(NULL==(semicolon=
strchr(con->request.http_if_modified_since,';'))){
64.used_len=strlen(con->request.http_if_modified_since);
65.}else{
66.used_len=semicolon-con->request.http_if_modified_since;
67.}
68.if(0==strncmp(con->request.http_if_modified_since,mtime->ptr,
used_len)){
/*比较,Last-Modified也匹配,返回304状态码。*/
69.con->http_status=304;
70.return HANDLER_FINISHED;
71.}else{
/*sizeof运算符得到的字符串长度包含末尾的字符'\0',因此此处结果为30。*/
72.char buf[sizeof("Sat,23 Jul 2005 21:20:01 GMT")];
73.time_t t_header,t_file;
74.struct tm tm;
75./*check if we can safely copy the string*/
76.if(used_len>=sizeof(buf)){
/*请求头域包含的日期字符串长度太长。*/
77.log_error_write(srv,__FILE__,__LINE__,"ssdd",
"DEBUG:Last-Modified check failed as the received
timestamp was too long:",
con->request.http_if_modified_since,
used_len,sizeof(buf)-1);
78.con->http_status=412;
79.con->mode=DIRECT;
80.return HANDLER_FINISHED;
81.}
82.strncpy(buf,con->request.http_if_modified_since,used_len);
83.buf[used_len]='\0';
/*函数strptime()原型为char*strptime(const char*s,const char*format,struct tm*tm);,它完成的功能和前面介绍的strftime()函数相反,其将由s所指向的字符串格式(该格式由控制串format指定)时间转换并存储到由参数tm所指向的struct tm结构体格式时间。
此处转换请求头域内包含的字符串日期和时间。*/
84.if(NULL==strptime(buf,
"%a,%d%b%Y%H:%M:%S GMT",&tm)){
85.con->http_status=412;
86.con->mode=DIRECT;
87.return HANDLER_FINISHED;
88.}
/*利用函数mktime()返回从UTC时间1970年1月1日0时0分0秒到请求头域指定时间所经过的绝对时间秒数,这是个time_t类型(一般等同于long int类型),因此便于下面的时间进行前后比较。*/
89.t_header=mktime(&tm);
/*计算服务器上请求文件的修改最后修改时间。*/
90.strptime(mtime->ptr,
"%a,%d%b%Y%H:%M:%S GMT",&tm);
91.t_file=mktime(&tm);
/*比较,如果服务器上请求文件的最后修改时间戳比请求头域指定的时间戳新,此时不能返回304状态码,因此函数返回GOON退出。*/
92.if(t_file>t_header)return HANDLER_GO_ON;
/*比较,如果服务器上请求文件的最后修改时间戳旧于请求头域指定的时间戳,此时返回304状态码。*/
93.con->http_status=304;
94.return HANDLER_FINISHED;
95.}
96.}else{
/*只有ETag存在,则只要ETag匹配服务器就直接返回状态码304。*/
97.con->http_status=304;
98.return HANDLER_FINISHED;
99.}
100.}else{
/*根据RFC 2616第14.26节,If-None-Match头域仅适合GET或HEAD请求方法,对于所有其他方法,服务器必须以412(Precondition Failed,先决条件失败)状态码响应。*/
101.con->http_status=412;
102.con->mode=DIRECT;
103.return HANDLER_FINISHED;
104.}
105.}
/*只有Last-Modified存在的情况,和上面部分代码一致(除了处理错误细节方面),在此略过重复注释。*/
106.}else if(con->request.http_if_modified_since){
107.size_t used_len;
108.char*semicolon;
109.if(NULL==(semicolon=strchr(con->request.http_if_modified_since,';'))){
110.used_len=strlen(con->request.http_if_modified_since);
111.}else{
112.used_len=semicolon-con->request.http_if_modified_since;
113.}
114.if(0==strncmp(con->request.http_if_modified_since,mtime->ptr,used_len)){
115.con->http_status=304;
116.return HANDLER_FINISHED;
117.}else{
118.char buf[sizeof("Sat,23 Jul 2005 21:20:01 GMT")];
119.time_t t_header,t_file;
120.struct tm tm;
121./*convert to timestamp*/
122.if(used_len>=sizeof(buf))return HANDLER_GO_ON;
123.strncpy(buf,con->request.http_if_modified_since,used_len);
124.buf[used_len]='\0';
125.if(NULL==strptime(buf,"%a,%d%b%Y%H:%M:%S GMT",&tm)){
126./**
127.*parsing failed,let's get out of here
128.*/
129.log_error_write(srv,__FILE__,__LINE__,"ss",
130."strptime()failed on",buf);
131.return HANDLER_GO_ON;
132.}
133.t_header=mktime(&tm);
134.strptime(mtime->ptr,"%a,%d%b%Y%H:%M:%S GMT",&tm);
135.t_file=mktime(&tm);
136.if(t_file>t_header)return HANDLER_GO_ON;
137.con->http_status=304;
138.return HANDLER_FINISHED;
139.}
140.}
141.return HANDLER_GO_ON;
142.}

      函数http_response_handle_cachable的执行流程图如图6-2所示,该流程图把同时存在ETag+Last-Modified和只有Last-Modified存在的两种情况进行了重叠,因为它们的代码仅在处理错误发生时有所不同,差别较小,重叠在一起便于理解整个处理过程。

3 文件缓存器

3.1 缓存器设计思路

       

       文件状态缓存器用于缓存文件状态:将stat()调用得到的结果(以及附属结果比如由当前文件状态生成的ETag、获知的文件类型等)保存在Lighttpd自己构造的缓存器中,在下一次需要获得文件状态时就直接返回缓存的值,这样提高程序执行效率。当然如果外部文件在其状态被缓存后发生了变化(被修改甚至被删除),这种变化应该同步反应到缓存器中去,否则程序从缓存器中获得的值将是错误的。

         

Lighttpd实现源码通过FAM(File alteration monitor<sup>40</sup>)(或称为sgi_fam)的子系统来获取外部文件系统的变化。该子系统由Silicon Graphics开发,它可以替应用程序监视某一些(需要被监视的)文件,当这些(被监视的)文件发生变化(如被修改或被删除等)时通知(这种通知有两种方式,一种是FAM把被监控文件的变化反应在一个文件描述符内,应用程序通过select或epoll等函数调用来获知,另一种是应用程序直接通过FAM库提供的FAMPending函数调用来获知)应用程序,从而使得应用程序可以做出相应的处理(比如Lighttpd应用程序需要的更新文件状态缓存器)。很显然,这比传统的由应用程序自身每隔一段时间去查询各个被监视的文件是否发生变化要高效得多,而且同步更及时。
Lighttpd源码里关于缓存器的实现可以叙述如下:
1)对于某文件A(假设文件完整路径为"/web/111/a.htm"),Lighttpd应用程序会在首次获取它的文件状态相关信息时将其缓存到缓存器内(数据结构体stat_cache_entry),同时把文件A所在的目录("/web/111/")添加到FAM监控(数据结构体fam_dir_entry)内,并且此时在保存数据的两个数据结构体stat_cache_entry和fam_dir_entry内保存的版本号(stat_cache_entry.dir_version==fam_dir_entry.version)是一致的。

2)当某时刻文件A发生了变化,Lighttpd应用程序从FAM子系统那获得这个变化信息,于是改变文件A所在目录对应的版本号(fam_dir_entry.version)。
3)Lighttpd应用程序需要再次获得文件A的文件状态相关信息时会先查询缓存器,并且比较文件A和所在目录的版本号,如果一致则表示文件A在这段时间内未发生变化,于是直接从缓存器内获取文件状态相关信息。如果不一致则表示缓存器内的缓存信息不是最新的,需重新获取。
上面去掉了很多细节的描述,具体的情况下面的源码分析部分会详细说明。

3.2 缓存器结构定义

      与文件状态缓存器有关的数据结构定义在base.h头文件内,stat_cache.c也有部分定义,结构如清单6-5所示。

      清单6-5 文件状态缓存器数据结构定义

      //base.h
143.typedef struct{
144.buffer*name;/*保存文件名。*/
145.buffer*etag;/*保存文件对应的ETag,因为计算ETag也是需要时间的。*/
/*保存文件状态信息,程序通过调用lstat()函数或stat()函数来获得文件状态信息。*/
146.struct stat st;
/*time_t存的是1970年1月1日0时0分0秒算起至今的UTC时间所经过的秒数,time_t是long int类型。
#ifndef_TIME_T_DEFINED
typedef long time_t;
#define_TIME_T_DEFINED
#endif
*/
147.time_t stat_ts;
148.
149.#ifdef HAVE_LSTAT
150.char is_symlink;/*符号链接标记。*/
151.#endif
152.
153.#ifdef HAVE_FAM_H/*FAM相关字段。*/
154.int dir_version;/*文件目录版本号。*/
155.int dir_ndx;/*索引。*/
156.#endif
157.
158.buffer*content_type;/*文件类型。*/
159.}stat_cache_entry;
160.
161.typedef struct{
/*缓存文件状态信息的节点都插入到这个files伸展树内。*/
162.splay_tree*files;/*the nodes of the tree are stat_cache_entry's*/
163.buffer*dir_name;/*for building the dirname from the filename*/
164.#ifdef HAVE_FAM_H
/*记录被监控文件夹的节点都插入到这个dirs伸展树内。*/
165.splay_tree*dirs;/*the nodes of the tree are fam_dir_entry*/
166.FAMConnection*fam;
167.int fam_fcce_ndx;
168.#endif
169.buffer*hash_key;/*temp-store for the hash-key*/
170.}stat_cache;
171.
//stat_cache.c
172./*
173.*stat-cache
174.*we cache the stat()calls in our own storage
175.*the directories are cached in FAM
176.*if we get a change-event from FAM,we increment the version in the FAM->dir mapping
177.*if the stat()-cache is queried we check if the version id for the directory is the
178.*same and return immediatly.
179.*What we need:
180.*-for each stat-cache entry we need a fast indirect lookup on the directory name
181.*-for each FAMRequest we have to find the version in the directory cache(index as userdata)
182.*stat<<->directory<->FAMRequest
183.*if file is deleted,directory is dirty,file is rechecked...
184.*if directory is deleted,directory mapping is removed
185.**/
186.
187.#ifdef HAVE_FAM_H
188.typedef struct{
189.FAMRequest*req;
190.FAMConnection*fc;
191.buffer*name;
192.int version;/*文件目录版本号。*/
193.}fam_dir_entry;
194.#endif
195.
196./*the directory name is too long to always compare on it
197.*-we need a hash
198.*-the hash-key is used as sorting criteria for a tree
199.*-a splay-tree is used as we can use the caching effect of it
200.*/
201./*we want to cleanup the stat-cache every few seconds,let's say 10
202.*
203.*-remove entries which are outdated since 30s
204.*-remove entries which are fresh but havn't been used since 60s
205.*-if we don't have a stat-cache entry for a directory,release it from the monitor
206.*/
207.#ifdef DEBUG_STAT_CACHE
208.typedef struct{
209.int*ptr;
210.size_t used;
211.size_t size;
212.}fake_keys;
213.static fake_keys ctrl;
214.#endif

      这几个结构体的字段都不多,而且源代码里有Lighttpd开发者的详细英文注释(Lighttpd开发者关于缓存器的注解也列出来了,方便读者理解。),如果将它们放在函数代码里再看就更容易理解了。

3.3 缓存器实现

     前面讲了文件状态缓存器的数据结构设计,其相关功能设计在stat_cache.c源文件内得以实现,下面就来分析这个文件,将其具体的实现细节介绍给读者。

      

1.stat_cache*stat_cache_init(void)、void stat_cache_free(stat_cache*sc)
stat_cache结构体的初始化和释放操作函数,如清单6-6所示。
清单6-6 函数stat_cache_init、stat_cache_free

//stat_cache.c
215.stat_cache*stat_cache_init(void){
216.stat_cache*fc=NULL;
217.
218.fc=calloc(1,sizeof(*fc));
219.
220.fc->dir_name=buffer_init();
221.fc->hash_key=buffer_init();
222.#ifdef HAVE_FAM_H
223.fc->fam=calloc(1,sizeof(*fc->fam));
224.#endif
225.
226.#ifdef DEBUG_STAT_CACHE
227.ctrl.size=0;
228.#endif
229.
230.return fc;
231.}
232.
233.void stat_cache_free(stat_cache*sc){
234.while(sc->files){
235.int osize;
236.splay_tree*node=sc->files;
237.
238.osize=sc->files->size;
239.
240.stat_cache_entry_free(node->data);/*先释放数据data空间。*/
/*再释放根节点,并返回新根节点以便继续循环释放。*/
241.sc->files=splaytree_delete(sc->files,node->key);
242.
243.assert(osize-1==splaytree_size(sc->files));
244.}
245.
246.buffer_free(sc->dir_name);
247.buffer_free(sc->hash_key);
248.
249.#ifdef HAVE_FAM_H
250.while(sc->dirs){
251.int osize;
252.splay_tree*node=sc->dirs;
253.
254.osize=sc->dirs->size;
255.
256.fam_dir_entry_free(node->data);
257.sc->dirs=splaytree_delete(sc->dirs,node->key);
258.
259.if(osize==1){
260.assert(NULL==sc->dirs);
261.}else{
262.assert(osize==(sc->dirs->size+1));
263.}
264.}
265.if(sc->fam){
/*函数FAMClose原型:
int FAMClose(FAMConnection*fc);
该函数关闭到FAM的连接,执行成功返回0,失败返回-1。*/
266.
267.FAMClose(sc->fam);
268.free(sc->fam);
269.}
270.#endif
271.free(sc);
272.}

2.static stat_cache_entry*stat_cache_entry_init(void)、static void stat_cache_entry_free(void*data)
stat_cache_entry结构体的初始化和释放操作函数如清单6-7所示。

//stat_cache.c
273.static stat_cache_entry*stat_cache_entry_init(void){
274.stat_cache_entry*sce=NULL;
275.
276.sce=calloc(1,sizeof(*sce));
277.
278.sce->name=buffer_init();
279.sce->etag=buffer_init();
280.sce->content_type=buffer_init();
281.
282.return sce;
283.}
284.
285.static void stat_cache_entry_free(void*data){
286.stat_cache_entry*sce=data;
287.if(!sce)return;
288.
289.buffer_free(sce->etag);
290.buffer_free(sce->name);
291.buffer_free(sce->content_type);
292.
293.free(sce);
294.}
295.

3.static fam_dir_entry*fam_dir_entry_init(void)、static void fam_dir_entry_free(void*data)

fam_dir_entry结构体的初始化和释放操作函数如清单6-8所示。

//stat_cache.c
296.#ifdef HAVE_FAM_H
297.static fam_dir_entry*fam_dir_entry_init(void){
298.fam_dir_entry*fam_dir=NULL;
299.
300.fam_dir=calloc(1,sizeof(*fam_dir));
301.
302.fam_dir->name=buffer_init();
303.
304.return fam_dir;
305.}
306.
307.static void fam_dir_entry_free(void*data){
308.fam_dir_entry*fam_dir=data;
309.
310.if(!fam_dir)return;
/*函数FAMCancelMonitor原型:
int FAMCancelMonitor(FAMConnection*fc,FAMRequest*fr);
该函数取消对fr指定的文件或文件夹的监视,执行成功返回0,失败返回-1。*/
311.FAMCancelMonitor(fam_dir->fc,fam_dir->req);
312.
313.buffer_free(fam_dir->name);
314.free(fam_dir->req);
315.
316.free(fam_dir);
317.}
318.#endif
319.

4.static int stat_cache_attr_get(buffer*buf,char*name)

    该函数(如清单6-9所示)获取指定路径文件的文件类型。函数attr_get属于XFS<sup>41</sup>(The xattr extension allows for the manipulation of extended attributes on a filesystem),用于获取文件的扩展属性(关于这方面的更多知识,读者可以查阅注解里提供的网站)。

//stat_cache.c
320.#ifdef HAVE_XATTR
321.static int stat_cache_attr_get(buffer*buf,char*name){
322.int attrlen;
323.int ret;
324.
325.attrlen=1024;
326.buffer_prepare_copy(buf,attrlen);
327.attrlen--;
328.if(0==(ret=attr_get(name,"Content-Type",buf->ptr,&attrlen,0))){
329.buf->used=attrlen+1;
330.buf->ptr[attrlen]='\0';
331.}
332.return ret;
333.}
334.#endif
335.

5.static uint32_t hashme(buffer*str)

      该函数(如清单6-10所示)获取对应字符串(保存在结构体buffer内)的hash值。该函数采用的是非常流行的DJB<sup>42</sup>hash function,俗称"Times33"算法。Times33算法其实很简单,就是不断地乘33。这里是将hash左移5位再加上hash(即hash*32+hash,也就是乘33)。因为Time33在效率和随机性方面都俱佳,所以几乎所有流行的Hash Map都采用了它

 清单6-10 函数hashme

//stat_cache.c
336./*the famous DJB hash function for strings*/
337.static uint32_t hashme(buffer*str){
338.uint32_t hash=5381;
339.const char*s;
340.for(s=str->ptr;*s;s++){
341.hash=((hash<<5)+hash)+*s;
342.}
343.
344.hash&=~(1<<31);/*strip the highest bit*/
345.
346.return hash;
347.}
348.

6.handler_t stat_cache_handle_fdevent(void*_srv,void*_fce,int revent)、static int buffer_copy_dirname(buffer*dst,buffer*file)

前面说了,FAM可以把被监控文件的变化反映在一个文件描述符内使得应用程序通过select或epoll等函数调用来获知这种变化。Lighttpd里正是利用这种方法,将对外部文件变化事件的监控和客户端的访问请求事件的监控统一管理。函数stat_cache_handle_fdevent主要将在外部被监控文件发生变化时(还有Lighttpd断开和FAM的连接时)被调用,用来对外部文件变化事件做相应处理。
该函数最新出现在server.c源文件内,用于事件监控处理函数注册,源码如清单6-11所示。

清单6-11 事件监控处理函数注册

349.#ifdef HAVE_FAM_H
350./*setup FAM*/
351.if(srv->srvconf.stat_cache_engine==STAT_CACHE_ENGINE_FAM){
352.if(0!=FAMOpen2(srv->stat_cache->fam,"lighttpd")){
353.log_error_write(srv,__FILE__,__LINE__,"s",
354."could not open a fam connection,dieing.");
355.return-1;
356.}
357.#ifdef HAVE_FAMNOEXISTS
/*FAMNoExists函数是Gamin里新扩展的函数。Gamin是Gnome里的一个项目,实现的也是FAM同样的功能。Gamin项目页里提及说Gamin和FAM 2.6.8在二进制和源代码级兼容,只有一点点不同与一个有意义的扩展。此处FAMNoExists函数就是这个有意义的扩展,但是其是否被调用都不影响FAM主要功能。具体内容可以参阅注解提供的网站43。*/
358.FAMNoExists(srv->stat_cache->fam);
359.#endif
/*FAMCONNECTION_GETFD是一个宏,用于从FAMConnection结构体中获取FAM连接的文件描述符。*/
360.srv->stat_cache->fam_fcce_ndx=-1;
361.fdevent_register(srv->ev,FAMCONNECTION_GETFD(srv->stat_cache->fam),
stat_cache_handle_fdevent,NULL);
362.fdevent_event_add(srv->ev,&(srv->stat_cache->fam_fcce_ndx),
FAMCONNECTION_GETFD(srv->stat_cache->fam),FDEVENT_IN);
363.}
364.#endif

在FAM提供了两个函数(函数FAMOpen<sup>44</sup>和函数FAMOpen2)用于打开一个到FAM服务器的连接,这两个函数差别很小,仅函数FAMOpen2比FAMOpen多了第二个参数(保存应用程序名称),因此可以告诉FAM服务器更多的一点FAM客户端信息。函数打开连接成功则返回0,否则返回-1。函数原型如清单6-12所示,其中的FAMConnection参数将在FAMOpen(FAMOpen2)成功执行过程中被初始化,在之后的所有FAM函数调用过程中都将使用到这个被初始化的结构体变量。
清单6-12 函数FAMOpen、FAMOpen2原型

365.int FAMOpen(FAMConnection*fc);
366.int FAMOpen2(FAMConnection*fc,const char*appName);
367.

       接下来就是利用fdevent_register和fdevent_event_add将FAM文件描述符加入到多路I/O复用监控(监控FDEVENT_IN事件,这个具体过程后面章节详细讲解)里。如此之后,只要FAM监控的文件有变化发生导致FAM文件描述符有事件(FAM event)发生,应用程序通过Select或Epoll等获知发生事件,从而调用此处注册的stat_cache_handle_fdevent函数进行处理。

      回过来再看stat_cache_handle_fdevent函数。函数对FAM通知过来的事件做分别处理,如果是可读事件,表示外部文件有变化,于是更新缓存器内容,如果是挂断事件则注销事件、关闭连接、释放内存,最后返回HANDLER_GO_ON标志继续程序的执行。读者在第一次阅读理解这个函数有困难时,请先阅读后面即将讲到的另一个重要函数stat_cache_get_entry,如清单6-13所示。

      清单6-13 函数stat_cache_get_entry

   368.//stat_cache.c
369.#ifdef HAVE_FAM_H
/*handler_t是个枚举类型结构体,定义在settings.h头文件内,在后面的Lighttpd插件分析章节再详细讲解此数据结构。
typedef enum{HANDLER_UNSET,
HANDLER_GO_ON,
HANDLER_FINISHED,
HANDLER_COMEBACK,
HANDLER_WAIT_FOR_EVENT,
HANDLER_ERROR,
HANDLER_WAIT_FOR_FD
}handler_t;*/
370.handler_t stat_cache_handle_fdevent(void*_srv,void*_fce,int revent){
371.size_t i;
372.server*srv=_srv;
373.stat_cache*sc=srv->stat_cache;
374.size_t events;
375.
376.UNUSED(_fce);
377./**/
/*可读事件发生。*/
378.if((revent&FDEVENT_IN)&&
379.sc->fam){
/*获取可读事件数目。*/
380.events=FAMPending(sc->fam);
/*函数FAMPending原型:
int FAMPending(FAMConnection*fc);
该函数返回正整数表示有FAM event在队列中,返回0表示没有事件发生,-1表示发生错误。该函数调用后马上返回,即不阻塞等待事件发生。
函数FAMNextEvent原型:
int FAMNextEvent(FAMConnection*fc,FAMEvent*fe);
该函数执行成功返回0,返回-1表示发生错误。*/
/*对各个可读事件分别进行处理。*/
381.for(i=0;i<events;i++){
382.FAMEvent fe;
383.fam_dir_entry*fam_dir;
384.splay_tree*node;
385.int ndx,j;
/*FAMNextEvent函数将逐个把事件信息存储到结构体FAMEvent参数fe内隐性传出。typedef struct{
FAMConnection*fc;
FAMRequest fr;
char*hostname;
char filename[PATH_MAX];
void*userdata;
FAMCodes code;
}FAMEvent;
fc是通过函数FAMOpen或FAMOpen2初始化的。
fr是通过函数FAMMonitorFile()或FAMMonitorDirectory()调用初始化的(这两个函数后面会讲到)。
hostname现在已经不使用了,一般不要用它。
filename(发生改变的)被监控文件或文件夹的完整路径或是被监控目录下的文件名。
userdata可以指向任何数据,因此提供了从事件监控设置函数到事件发生处理函数之间数据传递方法。
code是枚举结构FAMCodes中的一个取值,表示当前发生的是哪个事件(比如,FAMChanged、FAMDeleted)、FAMCreated、FAMMoved等。
*/
386.FAMNextEvent(sc->fam,&fe);
387.
388./*handle event*/
389.
390.switch(fe.code){
391.case FAMChanged:
392.case FAMDeleted:
393.case FAMMoved:
394./*if the filename is a directory remove the entry*/
/*如果文件名所指的是目录(并且事件是FAMDeleted或FAMMoved)则移除相应节点。获得用户数据,该数据从函数stat_cache_get_entry内传递过来。*/
395.fam_dir=fe.userdata;
396.fam_dir->version++;/*更新版本号,表示有文件发生变化。*/
397.
398./*file/dir is still here*/
/*只是目录文件状态改变事件则跳出,不用进行接下来的节点删除操作。*/
399.if(fe.code==FAMChanged)break;
400.
401./*we have 2 versions,follow and no-follow-symlink*/
/*有两种情况(follow and no-follow-symlink),但是并不知道是哪种情况,因此对这种情况的删除操作都要尝试进行,结果最多就是某一次尝试失败(即找不到那个节点,if判断为假,表示不是这种情况)。*/
402.for(j=0;j<2;j++){
403.buffer_copy_string(sc->hash_key,fe.filename);
/*获取发生改变的文件名。*/
404.buffer_append_long(sc->hash_key,j);
405.
406.ndx=hashme(sc->hash_key);
/*如果不是目录移动、删除事件,则这里的伸展操作应该是找不到节点的。*/
407.sc->dirs=splaytree_splay(sc->dirs,ndx);
408.node=sc->dirs;
409.
410.if(node&&(node->key==ndx)){
411.int osize=splaytree_size(sc->dirs);
412.
413.fam_dir_entry_free(node->data);
414.sc->dirs=splaytree_delete(sc->dirs,ndx);
415.
416.assert(osize-1==splaytree_size(sc->dirs));
417.}
418.}
419.break;
420.default:
421.break;
422.}
423.}
424.}
425.
426.if(revent&FDEVENT_HUP){
427./*fam closed the connection*/
428.srv->stat_cache->fam_fcce_ndx=-1;
429.
430.fdevent_event_del(srv->ev,&(sc->fam_fcce_ndx),
FAMCONNECTION_GETFD(sc->fam));
431.fdevent_unregister(srv->ev,FAMCONNECTION_GETFD(sc->fam));
432.
433.FAMClose(sc->fam);
434.free(sc->fam);
435.
436.sc->fam=NULL;
437.}
438.
439.return HANDLER_GO_ON;
440.}
/*获取指定文件所在的父目录路径。*/
441.static int buffer_copy_dirname(buffer*dst,buffer*file){
442.size_t i;
443.
444.if(buffer_is_empty(file))return-1;
445.
446.for(i=file->used-1;i+1>0;i--){
447.if(file->ptr[i]=='/'){
448.buffer_copy_string_len(dst,file->ptr,i);
449.return 0;
450.}
451.}
452.
453.return-1;
454.}
455.#endif

7.static int stat_cache_lstat(server*srv,buffer*dname,struct stat*lst)

该函数(如清单6-14所示)检查dname是否为符号链接,是返回0,不是返回1,如果出错返回-1。另外,调用lstat函数获得的文件状态信息放入参数lst中隐性传出。

清单6-14 函数stat_cache_lstat

//stat_cache.c
456.#ifdef HAVE_LSTAT
457.static int stat_cache_lstat(server*srv,buffer*dname,struct stat*lst){
458.if(lstat(dname->ptr,lst)==0){
459.return S_ISLNK(lst->st_mode)?0:1;
460.}
461.else{
462.log_error_write(srv,__FILE__,__LINE__,"sbs",
"lstat failed for:",
dname,strerror(errno));
463.};
464.return-1;
465.}
466.#endif
467.

8.handler_t stat_cache_get_entry(server*srv,connection*con,buffer*name,stat_cache_entry**ret_sce)

      函数stat_cache_get_entry(如清单6-15所示)是缓存器实现的重点。函数首先声明了一些必要的临时变量并进行了初始化,接着将字符串形式(存储在buffer结构体内)的参数name(待查文件完整路径)通过Hash转换成整型索引值,并在当前工作进程的文件状态缓存器中查找(文件状态缓存器以伸展树结构存储,所以节点查找是通过对伸展树的伸展访问进行)。需要注意的是,在伸展树中查找到待查文件记录节点(以下简称为记录节点)仍不能保证该记录节点就是程序原本想要的那个,程序是通过name参数Hash转换得到整型索引值查找的。如果两个不同的name字符串得到同一个整型索引值会怎么样呢?(这种可能当然存在,就算Times33算法随机性再好也不能保证完全没有冲突的发生)。所以程序仍需要对查找到的记录节点内保存的name字符串和函数原本的参数name进行对比判断,只有它们两个也相等时才表示查找成功。查找成功意味着待查文件状态就缓存在文件状态缓存器中,如果该记录节点又是最新的(即记录节点时间戳和当前工作进程时间戳一致)则直接使用即可,无须再次调用stat()函数从而提高效率(这就是文件状态缓存器的关键作用),将结果存入指针参数ret_sce中,并返回执行成功的状态码HANDLER_GO_ON。

    上面提及的是一切都进行得非常顺利的情况,再来看看不顺利的情况下程序执行流程会怎样,继续往下看。程序接着判断待查文件是否记录有变化(通过比较记录节点与待查文件所在目录的监控节点各自的版本号,如果相等则表示待查文件没有变化),如果没有变化则表示就算记录节点不是最新的(即该节点时间戳和当前工作进程时间戳不一致),但是因为待查文件一直没有变化(其实是待查文件所在目录以及该目录下的所有文件都没有变化发生),所以仍然可以直接使用该记录节点内保存的信息而无须再次调用stat()函数。

      好了,如果程序仍然在往下执行就表示文件状态缓存未命中或者记录节点已失效,缓存失败,没有了现成的信息使用,程序就必须一步步自己组织信息并将其缓存到缓存器中以希望下次访问时可以直接使用。首先调用stat函数获取文件状态并判断是否有权限读取它(失败的因素有很多,不管怎样,只要失败马上返回)。创建一个新的记录节点(如果是缓存未命中则必须新建立记录节点并将其插入到文件状态缓存器伸展树中的正确位置;如果只是记录节点失效则可以继续使用,用于之后的信息更新),然后对节点进行正确赋值。

           对待查文件是否为符号链接的判断代码有点长,因为它是递推向上判断,先判断待查文件是否为符号链接,如果是则标记(sce->is_symlink=1;)并跳出,判断结束。否则判断待查文件父目录,如果父目录是符号链接则标记并跳出。否则再判断父目录的父目录,直到判断目录为符号链接或到根目录为止(作者假定了根目录“/”不能为符号链接)。    

        如果该文件是普通文件,则先根据用户配置信息来设置文件对应的文件类型。程序是通过比较文件名的最后几个字符和用户配置信息里的设置来判断的。以Lighttpd源文件下载包里提供的配置文件lighttpd.conf为例,可以在该配置文件里找到:

       #mimetype mapping
mimetype.assign=(
".pdf"=>"application/pdf",
#省略部分
".html"=>"text/html",
".htm"=>"text/html",
#省略部分
#default mime type
""=>"application/octet-stream",
)

    

       通过比较,程序认为文件名末尾几个字符为".pdf"的文件类型为"application/pdf"、字符为".html"的文件类型为"text/html"等。对于最后一个设置,如果循环进行到此处判断表名待查文件不属于以上任何一种类型,就都认为是"application/octet-stream"(此处的和空字符比较必为真,因为strncasecmp的第三个参数为0)。如果系统还支持文件扩展属性(Extended attributes)并且用户没有对mimetype.assign的配置,则还可利用库函数来获取文件类型。再接下来就是调用etag_create函数创建文件的ETag。

       

          函数最后的工作就是把这个待查文件添加到FAM监控队列(事实上是将待查文件所在的父目录添加到FAM监控队列中),以便待查文件将来发生变化时能由FAM通知给Lighttpd应用程序以更新缓存器内数据。首先判断待查文件所在目录是否已经在FAM中注册了,如果还没有则注册,于是接下来执行内存申请、初始化等常规操作,然后调用函数FAMMonitorDirectory()进行FAM目录监控注册。通过FAMMonitorDirectory()函数告诉FAM开始监控指定的目录(待查文件的父目录),这种监控不仅仅是对于指定目录自身的变化,也包括该指定目录内文件(如果有)的变化(值得注意的是,如果该指定目录内包含有子目录则只监控该子目录的变化,对于子目录内文件(如果有)的变化不再予以监控)。FAM监控注册好之后再完成一些数据结构的组织工作(插入、或更新相应的内容,其中对待查文件记录节点的版本更新很重要,即将其设置为和所在目录的版本号一致,即sce->dir_version=fam_dir->version;,这样在下次调用本函数时,文件节点和所在目录的版本号对比判断才有效果),该函数就算完成任务了,而后隐性传出保存有最新待查文件状态信息的节点并返回执行成功状态码,函数完成。

清单6-15 函数stat_cache_get_entry

//stat_cache.c
468./***
469.*returns:
470.*-HANDLER_FINISHED on cache-miss(don't forget to reopen the file)
471.*-HANDLER_ERROR on stat()failed->see errno for problem
472.*/
473.
474.handler_t stat_cache_get_entry(server*srv,connection*con,buffer*name,
stat_cache_entry**ret_sce){
475.#ifdef HAVE_FAM_H
476.fam_dir_entry*fam_dir=NULL;
477.int dir_ndx=-1;
478.splay_tree*dir_node=NULL;
479.#endif
480.stat_cache_entry*sce=NULL;
481.stat_cache*sc;
482.struct stat st;
483.size_t k;
484.int fd;
485.struct stat lst;
486.#ifdef DEBUG_STAT_CACHE
487.size_t i;
488.#endif
489.
490.int file_ndx;
491.splay_tree*file_node=NULL;
492.
493.*ret_sce=NULL;
494.
495./*
496.*check if the directory for this file has changed
497.*/
498.
499.sc=srv->stat_cache;/*文件状态信息缓存器。*/
500.
501.buffer_copy_string_buffer(sc->hash_key,name);/*文件路径临时存储。*/
/*follow_symlink取值0或1。*/
502.buffer_append_long(sc->hash_key,con->conf.follow_symlink);
503.
504.file_ndx=hashme(sc->hash_key);/*根据文件路径字符串计算对应的Hash值。*/
/*在缓存器中的文件状态信息存储伸展树中查找记录节点。*/
505.sc->files=splaytree_splay(sc->files,file_ndx);
506.
507.#ifdef DEBUG_STAT_CACHE
508.for(i=0;i<ctrl.used;i++){
509.if(ctrl.ptr[i]==file_ndx)break;
510.}
511.#endif
/*已经存在旧记录节点,即待查文件的状态信息之前已经被保存到缓存器中。*/
/*伸展树中的节点查找会把节点伸展到根节点处。*/
512.if(sc->files&&(sc->files->key==file_ndx)){
513.#ifdef DEBUG_STAT_CACHE
514./*it was in the cache*/
515.assert(i<ctrl.used);
516.#endif
517.
518./*we have seen this file already and
519.*don't stat()it again in the same second*/
520.
521.file_node=sc->files;/*找到的记录节点。*/
522.
523.sce=file_node->data;/*该记录节点保存的文件状态等相关信息。*/
524.
525./*check if the name is the same,we might have a collision*/
/*对不同的key(这儿也就是name),DBJ散列出同一个值,即是发生了冲突。因此,这里还需要进行路径(name)的对比判断。*/
526.if(buffer_is_equal(name,sce->name)){/*简单引擎。*/
527.if(srv->srvconf.stat_cache_engine==STAT_CACHE_ENGINE_SIMPLE){
528.if(sce->stat_ts==srv->cur_ts){
/*记录节点保存的信息是最新的。*/
529.*ret_sce=sce;/*隐性传出。*/
530.return HANDLER_GO_ON;/*返回执行成功的代号。*/
531.}
532.}
533.}else{
534./*oops,a collision,
535.*
536.*file_node is used by the FAM check below to see if we know this file
537.*and if we can save a stat().
538.*
539.*BUT,the sce is not reset here as the entry into the cache is ok,we
540.*it is just not pointing to our requested file.
541.*
542.**/
/*发生了冲突,因此必须把file_node置为NULL,表示未找到。另外,这颗伸展树中的节点不用删除,因为其是正确的,只是不是目前这个待查文件对应的节点。*/
543.file_node=NULL;
544.}
545.}else{
546.#ifdef DEBUG_STAT_CACHE
547.if(i!=ctrl.used){
548.fprintf(stderr,"%s.%d:%08x was already inserted but not found in cache,
%s\n",__FILE__,__LINE__,file_ndx,name->ptr);
549.}
550.assert(I==ctrl.used);
551.#endif
552.}
553.
554.#ifdef HAVE_FAM_H
555./*dir-check*/
556.if(srv->srvconf.stat_cache_engine==STAT_CACHE_ENGINE_FAM){
/*找到待查文件所在的父目录。*/
557.if(0!=buffer_copy_dirname(sc->dir_name,name)){
558.log_error_write(srv,__FILE__,__LINE__,"sb",
559."no'/'found in filename:",name);
560.return HANDLER_ERROR;
561.}
/*计算父目录的Hash值。*/
562.buffer_copy_string_buffer(sc->hash_key,sc->dir_name);
563.buffer_append_long(sc->hash_key,con->conf.follow_symlink);
564.
565.dir_ndx=hashme(sc->hash_key);
/*根据Hash值在缓存器中的监控目录存储伸展树中查找对应节点。*/
566.sc->dirs=splaytree_splay(sc->dirs,dir_ndx);
567.
568.if(sc->dirs&&(sc->dirs->key==dir_ndx)){/*找到对应节点。*/
569.dir_node=sc->dirs;
570.}
/*待查文件记录节点和监控父目录对应节点都找到了。*/
571.if(dir_node&&file_node){
572./*we found a file*/
573.
574.sce=file_node->data;/*各自的存储信息。*/
575.fam_dir=dir_node->data;
576.
577.if(fam_dir->version==sce->dir_version){/*比较版本号。*/
578./*the stat()-cache entry is still ok*/
579.
580.*ret_sce=sce;/*好,缓存的文件状态信息是可以用的。*/
581.return HANDLER_GO_ON;
582.}
583.}
584.}
585.#endif
586.
587./*
588.**lol*
589.*-open()+fstat()on a named-pipe results in a(intended)hang.
590.*-stat()if regular file+open()to see if we can read from it is better
591.*
592.**/
593.if(-1==stat(name->ptr,&st)){
594.return HANDLER_ERROR;
595.}
596.
597.
598.if(S_ISREG(st.st_mode)){
/*如果是普通文件,则试着打开它,如果出错则返回错误。*/
599./*try to open the file to check if we can read it*/
600.if(-1==(fd=open(name->ptr,O_RDONLY))){
601.return HANDLER_ERROR;
602.}
603.close(fd);
604.}
605.
606.if(NULL==sce){/*这里需要新建一个文件状态信息记录节点。*/
607.int osize=0;
608.
609.if(sc->files){
610.osize=sc->files->size;
611.}
612.
613.sce=stat_cache_entry_init();/*初始化。*/
614.buffer_copy_string_buffer(sce->name,name);
615.
616.sc->files=splaytree_insert(sc->files,file_ndx,sce);
/*插入新记录到伸展树。*/
617.#ifdef DEBUG_STAT_CACHE
618.if(ctrl.size==0){
619.ctrl.size=16;
620.ctrl.used=0;
621.ctrl.ptr=malloc(ctrl.size*sizeof(*ctrl.ptr));
622.}else if(ctrl.size==ctrl.used){
623.ctrl.size+=16;
624.ctrl.ptr=realloc(ctrl.ptr,ctrl.size*sizeof(*ctrl.ptr));
625.}
626.
627.ctrl.ptr[ctrl.used++]=file_ndx;
628.
629.assert(sc->files);
630.assert(sc->files->data==sce);
631.assert(osize+1==splaytree_size(sc->files));
632.#endif
633.}
634.
635.sce->st=st;
636.sce->stat_ts=srv->cur_ts;
637.
638./*catch the obvious symlinks
639.*
640.*this is not a secure check as we still have a race-condition between
641.*the stat()and the open.We can only solve this by
642.*1.open()the file
643.*2.fstat()the fd
644.*
645.*and keeping the file open for the rest of the time.But this can
646.*only be done at network level.
647.*
648.*per default it is not a symlink
649.**/
650.#ifdef HAVE_LSTAT
651.sce->is_symlink=0;
652.
653./*we want to only check for symlinks if we should block symlinks.
654.*/
655.if(!con->conf.follow_symlink){
656.if(stat_cache_lstat(srv,name,&lst)==0){/*找到符号链接文件。*/
657.#ifdef DEBUG_STAT_CACHE
658.log_error_write(srv,__FILE__,__LINE__,"sb",
659."found symlink",name);
660.#endif
661.sce->is_symlink=1;
662.}
663.
664./*
665.*we assume"/"can not be symlink,so
666.*skip the symlink stuff if our path is/
667.**/
668.else if((name->used>2)){/*假定根文件不能符号链接。*/
669.buffer*dname;
670.char*s_cur;
671.
672.dname=buffer_init();
673.buffer_copy_string_buffer(dname,name);
674.
/*strrchr函数为查找某字符在另一个字符串中最后出现的位置。*/
675.while((s_cur=strrchr(dname->ptr,'/'))){
676.*s_cur='\0';/*更新。*/
677.dname->used=s_cur-dname->ptr+1;
678.if(dname->ptr==s_cur){/*到达根目录了。*/
679.#ifdef DEBUG_STAT_CACHE
680.log_error_write(srv,__FILE__,__LINE__,"s","reached/");
681.#endif
682.break;
683.}
684.#ifdef DEBUG_STAT_CACHE
685.log_error_write(srv,__FILE__,__LINE__,"sbs",
686."checking if",dname,"is a symlink");
687.#endif
/*如果该目录为符号链接,则设置标志并跳出,否则继续父目录判断(即开始下个循环)。*/
688.if(stat_cache_lstat(srv,dname,&lst)==0){
689.sce->is_symlink=1;
690.#ifdef DEBUG_STAT_CACHE
691.log_error_write(srv,__FILE__,__LINE__,"sb",
692."found symlink",dname);
693.#endif
694.break;
695.};
696.};
697.buffer_free(dname);
698.};
699.};
700.#endif
701.
702.if(S_ISREG(st.st_mode)){/*普通文件。*/
703./*determine mimetype*//*开始检测文件类型(mimetype)。*/
704.buffer_reset(sce->content_type);
/*根据用户的配置,根据逐个对比来判定文件类型。*/
705.for(k=0;k<con->conf.mimetypes->used;k++){
706.data_string*ds=(data_string*)con->conf.mimetypes->data[k];
707.buffer*type=ds->key;
708.
709.if(type->used==0)continue;
710.
711./*check if the right side is the same*/
712.if(type->used>name->used)continue;
/*根据配置找出文件类型,通过比较文件的后面的类似于扩展名的字符串。这里的比较是不区分大小写的。*/
713.if(0==strncasecmp(name->ptr+name->used-type->used,
type->ptr,type->used-1)){
714.buffer_copy_string_buffer(sce->content_type,ds->value);
715.break;
716.}
717.}
718.etag_create(sce->etag,&(sce->st),con->etag_flags);/*创建ETAG。*/
719.#ifdef HAVE_XATTR
/*如果用户配置使用XATTR,并且之前逐个对比判定文件类型失败。*/
720.if(con->conf.use_xattr&&buffer_is_empty(sce->content_type)){
721.stat_cache_attr_get(sce->content_type,name->ptr);
722.}
723.#endif
724.}else if(S_ISDIR(st.st_mode)){
725.etag_create(sce->etag,&(sce->st),con->etag_flags);
726.}
727.
728.#ifdef HAVE_FAM_H
729.if(sc->fam&&
730.(srv->srvconf.stat_cache_engine==STAT_CACHE_ENGINE_FAM)){
731./*is this directory already registered?*/
/*父目录已经被监控(即对应fam_dir_entry节点已经在dirs伸展树中),所以前面代码是应该找到该节点的(即dir_node不为NULL)。*/
732.if(!dir_node){/*未被监控,因此要加入到FAM监控里。*/
733.fam_dir=fam_dir_entry_init();
734.fam_dir->fc=sc->fam;
735.
736.buffer_copy_string_buffer(fam_dir->name,sc->dir_name);
737.
738.fam_dir->version=1;/*初始版本号为1。*/
739.
740.fam_dir->req=calloc(1,sizeof(FAMRequest));
/*将目录添加到FAM监控里。
该函数原型如下:
int FAMMonitorDirectory(FAMConnection*fc,
char*filename,
FAMRequest*fr,
void*userData);
FAMMonitorDirectory注册的目录监控不仅仅是对于指定目录自身的变化,也包括该指定目录内文件(如果有)的变化,但是如果该指定目录内包含有子目录则只监控该子目录本身的变化,对于子目录内文件(如果有)的变化不再予以监控。
另一个添加监控的函数是FAMMonitorFile,该函数注册某文件的监控。原型如下:
int FAMMonitorFile(FAMConnection*fc,
char*filename,
FAMRequest*fr,
void*userData);
这两个函数的返回值都为0如果执行成功,否则返回-1表示失败。
两个函数的参数一致,其中第一个参数是经过函数FAMOpen或FAMOpen2初始化过的FAMConnection结构体。第二个参数是要被监控的文件夹或文件的完整路径(注意:不可以使用相对路径)。第三个参数是一个FAMRequest结构体变量,它将在这两个函数执行监控注册的执行过程中被初始化。之后,可以利用这个结构体变量调用函数FAMSuspendMonitor()、FAMResumeMonitor()或者FAMCancelMonitor()来完成对这个监控的挂起、重启或取消。第四个参数是一个可以指向任何用户数据的指针变量,当有事件发生时,该用户数据会通过FAMEvent结构体(事件对应的该结构体值可以通过函数FAMNextEvent()获取)传递到事件处理函数里,这里是指向目录节点数据,该结构里保存有目录的路径、版本号等,在事件处理函数stat_cache_handle_fdevent里可以获取数据进行所需操作。
int FAMSuspendMonitor(FAMConnection*fc,FAMRequest*fr);
int FAMResumeMonitor(FAMConnection*fc,FAMRequest*fr);
int FAMCancelMonitor(FAMConnection*fc,FAMRequest*fr);
关于FAM这些函数的更多信息请读者查阅相关资料,这里的讲解对于读者理解Lighttpd源码已经足够了。*/
741.if(0!=FAMMonitorDirectory(sc->fam,fam_dir->name->ptr,
742.fam_dir->req,fam_dir)){
743.
744.log_error_write(srv,__FILE__,__LINE__,"sbsbs",
745."monitoring dir failed:",
746.fam_dir->name,
747."file:",name,
748.FamErrlist[FAMErrno]);
749.
750.fam_dir_entry_free(fam_dir);
751.}else{/*监控添加成功,把目录节点添加到dirs伸展树中。*/
752.int osize=0;
753.
754.if(sc->dirs){
755.osize=sc->dirs->size;
756.}
757.
758.sc->dirs=splaytree_insert(sc->dirs,dir_ndx,fam_dir);
759.assert(sc->dirs);
760.assert(sc->dirs->data==fam_dir);
761.assert(osize==(sc->dirs->size-1));
762.}
763.}else{
764.fam_dir=dir_node->data;/*直接获取目录节点。*/
765.}
766.
767./*bind the fam_fc to the stat()cache entry*/
768.
769.if(fam_dir){/*更新文件信息节点的版本号,和目录节点的版本号一致。*/
770.sce->dir_version=fam_dir->version;
771.sce->dir_ndx=dir_ndx;
772.}
773.}
774.#endif
775.
776.*ret_sce=sce;
777.
778.return HANDLER_GO_ON;
779.}
780.

9.static int stat_cache_tag_old_entries(server*srv,splay_tree*t,int*keys,size_t*ndx)、int stat_cache_trigger_cleanup(server*srv)

    这两个函数(如清单6-16所示)联合起来共同完成删除距上次被访问时间超过2秒的文件状态信息存储节点。

    清单6-16 函数stat_cache_tag_old_entries、stat_cache_trigger_cleanup

    //stat_cache.c
781./**
782.*remove stat()from cache which havn't been stat()ed for
783.*more than 10 seconds
784.*
785.*
786.*walk though the stat-cache,collect the ids which are too old
787.*and remove them in a second loop
788.*/
789.
790.static int stat_cache_tag_old_entries(server*srv,splay_tree*t,int
*keys,size_t*ndx){
791.stat_cache_entry*sce;
792.
793.if(!t)return 0;
794.
/*遍历:左->右->中,即后序遍历。*/
795.stat_cache_tag_old_entries(srv,t->left,keys,ndx);
796.stat_cache_tag_old_entries(srv,t->right,keys,ndx);
797.
798.sce=t->data;
/*当节点缓存时间超过2秒就记录它以便删除,这和前面的英文注释不太一致(remove stat()from cache which havn't been stat()ed for more than 10 seconds,即英文注释是10秒),不过对于代码理解并无大碍,超时秒数大小只是性能调节。*/
799.if(srv->cur_ts-sce->stat_ts>2){
800.keys[(*ndx)++]=t->key;/*将待删除节点的索引记录下来。*/
801.}
802.
803.return 0;
804.}
805.
806.int stat_cache_trigger_cleanup(server*srv){
807.stat_cache*sc;
808.size_t max_ndx=0,i;
809.int*keys;
810.
811.sc=srv->stat_cache;
812.
813.if(!sc->files)return 0;
814.
815.keys=calloc(1,sizeof(size_t)*sc->files->size);
/*获取待删除节点。*/
816.stat_cache_tag_old_entries(srv,sc->files,keys,&max_ndx);
/*进行清除操作。*/
817.for(i=0;i<max_ndx;i++){
818.int ndx=keys[i];
819.splay_tree*node;
/*获得该节点,查找过程中会进行伸展。*/
820.sc->files=splaytree_splay(sc->files,ndx);
/*的确是需要删除的节点。*/
821.node=sc->files;
822.
823.if(node&&(node->key==ndx)){
824.#ifdef DEBUG_STAT_CACHE
825.size_t j;
826.int osize=splaytree_size(sc->files);
827.stat_cache_entry*sce=node->data;
828.#endif
829.stat_cache_entry_free(node->data);/*进行删除。*/
830.sc->files=splaytree_delete(sc->files,ndx);
831.
832.#ifdef DEBUG_STAT_CACHE
833.for(j=0;j<ctrl.used;j++){
834.if(ctrl.ptr[j]==ndx){
835.ctrl.ptr[j]=ctrl.ptr[--ctrl.used];
836.break;
837.}
838.}
839.
840.assert(osize-1==splaytree_size(sc->files));
841.#endif
842.}
843.}
844.
845.free(keys);
846.
847.return 0;
848.}

      通过以上的分析不难发现,程序没有记录具体哪个文件发生了变化,而仅仅是通过更新(增一)监控目录对应节点的版本号来做标记表示该目录或该目录下有文件发生了变化。因此,对于前后两次访问监控目录下的某一文件(如文件A)的这段时间内,就算文件A没有发生变化,但是如果监控目录下的另一文件(如文件B)发生了变化而使得监控目录对应节点的版本号增大导致文件A对应节点的版本号和目录对应节点的版本号不一致从而程序需要重新对文件A调用stat函数,在我们看来对文件A调用stat函数是不必要的,因为文件A没有发生任何变化。当然,作者没有记录具体哪个文件发生了变化有其他的考虑,比如程序结构的设计,参数的传递等。

4 本章总结

      本章主要对Lighttpd里的文件状态信息缓存器做了较为详细的介绍。缓存器的设计实现着重依赖于另外一个开源子系统—File Alteration Monitor,如果不了解FAM,理解这部分代码的确会有不少困难,因此本章对代码里出现的FAM函数也做了比较详细的注解,读者如果仍不清楚则可以参考页面注解里提供的Web站点。

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

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

猜你喜欢

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