博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

apache scoreboard worker_record

Posted on 2013-09-18 15:54  bw_0927  阅读(979)  评论(0编辑  收藏  举报

http://basiccoder.com/apache-mpm-prefork-design-intro.html

http://blog.csdn.net/tingya/article/details/867371

 

最近几天翻阅了apache的MPM(Multi-Processing Module)机制相关的代码,虽然还有很多细节没有搞明白,但对apache的服务器模型有了一个大体的概念,对于不同的操作系统,apache提供了不同的默认MPM模型,下表是不同操作系统默认的MPM模型:

BeOS beos
Netware mpm_netware
OS/2 mpmt_os2
Unix prefork
Windows mpm_winnt

Unix平台则对应着prefork模型,prefork从名字上看意思是预先生成子进程,所以这种模型大致上是怎么工作的我们心里差不多有些认识了,prefork是一种很重要的服务器程序设计模型,对应的还有prethread,prefork一般应用在Unix平台上,因为在服务器启动时需要预告fork出一些空闲的子进程,由它们共同监听客户端的请求,这样来实现快速高并发的特性,这种机制之所以不适合Windows等平台,是因为在Windows等平台上进程的代价太高。

apache的进程管理中有一个叫做scoreboard(记分牌)的概念,主进程在进入MPM循环以前会先在进程池中创建一个scoreboard对象,该对象定义如下:

typedef struct {
    global_score *global;
    process_score *parent;
    worker_score **servers;
    lb_score     *balancers;
} scoreboard;

global_score保存主进程的状态,process_score则是一个数组插槽,每个插槽保存一个子进程的状态,worker_score则是一个二维数组插槽,用来保存每个子进程创建的线程状态,根据这个结构主进程可以对子进程以及相关的线程进行管理,apache按照最大化的原则来分配内存,比如会按配置中允许最多的进程数目来为parent分配内存空间。

MPM初始化还有一个很重要的方面是创建一个进程锁,fork()出来的子进程与父进程并不共享内存空间,多进程之于多线程的优势在于多进程可以省去多线程进行线程同步的开销,而这里创建的进程锁,主要作用是为了给accept()加锁,为了避免thundering herd问题。apache实现了五种类型的进程锁,使用flock()或fcntl()实现的文件锁,Posix信号量或System V信号量,以及使用pthread线程库实现的互斥锁。我理解的是文件锁的效率会低于其它类型的锁,因为文件锁要涉及到文件系统的IO操作。我只阅读了跟pthread相关的代码,和一般的多进程程序实现方式一致,因为子进程与父进程以及子进程之间不共享内存空间的,所以不可能像多线程程序一样将互斥锁定义为全局变量 ,因此使用共享内存机制,将互斥锁变量存放到共享内存里面,并设置共享属性。然后便可以使用该互斥锁对子进程中的accept()过程进行加锁。

初始化最后需要开始创建预定个数的子进程,调用startup_children()函数创建指定个数的子进程,该函数会检查scoreboard的空闲插槽,在空闲插槽上调用make_child()函数来在该插槽位置处创建一个子进程,该函数设置scoreboard中进程的状态,并fork()一个子进程,将子进程的pid写入到scoreboard对应的插槽处,子进程创建之后设置SIGHUP和SIGTERM信号,这两个信号对应的回调函数均为clean_child_exit()函数,该函数销毁内存池然后退出子进程。

make_child()函数执行成功后进行child_main()函数,即子进程的主循环。该函数看起来比较复杂,其实做的事情也很简单,首先是创建相关的内存池,对进程锁进行初始化(对于pthread进程锁对应是一个空函数,即无需进行初始化),将socket描述符加入到pollset中,这里的pollset也是apache抽象出来的概念,它的实现可以是kqueue/port/epoll/poll/select,具体采用哪种方式也是配置可选的。这里是我不太明白的地方,经常看到评论说nginx效率高于apache,当问起nginx效率高于apache的主要原因时,得到的答案很多都是nginx采用kqueue和epoll实现了高并发,其实感觉这个理由并不充分,我们可以看到apache同样也实现了kqueue和epoll的多路复用,如果这因为这个的话那apache没有理由会比nginx效率低多少的,另外也看到有说apache的进程管理机制占用内存过高,而且时常需要进行进程切换从而占用了CPU时间,这个说法可以接受,现在非常想去读下nginx的源码,想看看它到底是采用了什么样的机制带来了它如此之多的好评,下一步就可始阅读下nginx的源码,对比着apache,探索一些高并发服务器设计的最优方法。

子进程进入主循环之后会调用accept()方法,这个方法是需要进行加锁的,之后创建一个新的连接对象,并调用HOOK函数对连接进行处理,HOOK机制是apache模块化很重要的一种机制,在主程序中调用HOOK函数,具体的实现由具体的模块来定义。

父进程在创建完子进程之后也进行主循环,监控活动子进程的数目,并通过一定的调度使用子进程数目维护一个平衡,父进程使用waitpid()函数来检测子进程的退出情况,如果有进程退出,则创建一个新的进程来替代已结束的进程从而维持总数的一个平衡。

当然apache还有平稳启动机制,关于平衡启动的代码我暂时略过了,没有细读,以后有时间再回过头来仔细研究。

===========================

6.1 多进程并发处理概述

6.1.1 概述

第五章中我们讨论Apache主程序的时候,当主程序调用了函数ap_mpm_run之后,整个主程序就算结束了。那么函数进入到ap_mpm_run之后它干什么去了呢?

如果让我们来写服务器程序的话,按照正常的思路,通常主程序在进行了必要的准备工作后会调用诸如fork之类的函数产生一个新的进程或者线程,然后由子进程进行并发处理。事实上,Apache尽管是一个先进的服务器,但是它也不能脱离窠臼。

主进程一旦调用ap_mpm_run之后,它就进入多进程并发处理状态。为了并发处理客户端请求,Apache或者产生多个进程,或者产生多个线程,或者产生多个进程,每个进程又产生一定数目的线程等等。

Apache HTTP服务器从一开始就被设计为一个强大、灵活的能够在多种平台上及不同的环境下工作的服务器。不同的平台和不同的环境经常产生不同的需求,或是会为了达到同样的最佳效果而采用不同的方法。当然,Apache中提供了多种多进程并发模型,比如PreforkWindow NTEventperchild等等。并且为了方便移植和替换,Apache将这些多进程并发处理模型设计成模块。Apache凭借它的模块设计很好的适应了大量不同的环境。这一设计使得网站管理员能够在编译时和运行时凭借载入不同的模块来决定服务器的不同附加功能。

Apache 2.0 将这种模块式设计延伸到web服务器的基础功能上。这个发布版本带有多道处理模块的选择以处理网络端口绑定、接受请求并指派子进程来处理这些请求。

将模块设计延伸到这一层面主要有以下两大好处:

Apache可以更简洁、更有效地支持各种操作系统。尤其是在mpm_winnt使用本地网络特性以代替Apache 1.3中使用的POSIX层后, Windows版本的Apache现在有了更好的性能。这个优势借助特定的MPM同样延伸到其他各种操作系统。

服务器可以为某些特定的站点进行自定义。比如,需要更好缩扩性的站点可以选择象worker这样线程化的MPM,而需要更好的稳定性和兼容性以适应一些旧的软件的站点可以用prefork。此外,象用不同的用户号(perchild)伺服不同的站点这样的特性也能提供了。

从用户层面来讲,MPMs更像其他Apache模块。而主要的不同在于:不论何时,有且仅有一个MPM必须被载入到服务器中。现有的MPM列表可以在这里找到模块索引

下表列出了不同操作系统下默认的MPMs。如果你在编译时没有进行选择,这将是默认选择的MPM

BeOS

beos

Netware

mpm_netware

OS/2

mpmt_os2

Unix

prefork

Windows

mpm_winnt

在详细深入的描述各个MPM之前,我们有必要了解一下MPM中所使用到的公共数据结构,主要包括两种:记分板和父子进程的通信管道。记分板类似于共享内存,主要用于父子进程之间进行数据交换,类似于白板。任何一方都可以将对方需要的信息写入到记分板上,同时任何一方也可以到记分板上获取需要的数据。

6.2 MPM公共数据结构

6.2.1记分板

6.2.1.1记分板概述

Apache的MPM中通常总是包含一个主服务进程以及若干个子进程,因此不可避免的存在主进程和子进程通信的问题。Apache中采用了两种主要的通信方法:记分板和管道。

记分板就是一块共享内存块,同时可以被父进程和子进程访问,通过共享实现了父子之间的通信。尽管如此,但是记分板则更主要用于父进程对子进程进行控制。在Apache中主进程的一个重要的职责就是控制空闲子进程的数目:如果空闲子进程过多,则父进程将终止一些子进程;如果空闲子进程太少,则父进程将创建一些新的空闲子进程以备使用。因此,父进程必须随时能够知道子进程的数目以便进行调整。子进程把自己的状态信息忙碌或者空闲写入到记分板中,这样通过读取记分板,父进程就可以知道子进程的数目了。

记分板的数据结构可以描述如下:

typedef struct {

global_score *global;

process_score *parent;

worker_score **servers;

} scoreboard;

该结构定义在scoreboard.h中,由该数据结构可见,Apache中的记分板可以记录三种类型的信息:全局信息、进程间共享信息以及线程间共享信息。

global_score是记分板中描述全局信息的结构,通常这些信息是针对整个Apache服务器的,而不是针对某个进程或者某个线程的,该结构定义如下:

typedef struct {

int server_limit;

int thread_limit;

ap_scoreboard_e sb_type;

ap_generation_t running_generation;

apr_time_t restart_time;

} global_score;

从该结构中我们可以看出,全局的共享信息包括下面的几个内容:server_limit描述系统中所存在的服务进程的极限值,thread_limit则是描述的线程的极限值。ap_scoreboard是枚举类型,只有两个值SB_NOT_SHARED和SB_SHARED,分别表示该记分板是否进程间共享还是不共享。

ap_generation_t的定义实际上是整数值:typedef int ap_generation_t。该值主要用于平稳启动(graceful restart)Apache中允许在不终止Apache的情况下对Apache进行重新启动,这种启动称之为平稳启动。平稳启动的时候,主服务进程将退出,同时创建新的子进程。此时这些子进程由父进程创建,它们形成一个继承称此上的家族概念,只要是主进程产生的所有子进程都属于这个主进程家族,因此我们称它们称之为为新的代(generation),在本书中我们统一用家族这个术语进行描述。只有子进程与父进程具有亲缘关系,它们才是一个家族。每一个进程在执行完任务之后都会检查它与当前的主进程是否属于同一个家族。如果属于,则继续等待处理下一个任务;否则其将退出。由于Apache在进行平稳启动的时候对于那些尚未结束的进程并不强行将其终止,而是让其继续执行,但是主进程必须退出重新启动。因此当新的主进程启动之后,这些残余的子进程显然已经跟它不是同一个家族,它们属于上一辈的。因此他们在执行完任务之后立即退出。

某个主进程产生后它就产生一个唯一的家族号,用running_generation进行记录。该值永远不会重复。主进程的所有子进程将继承该家族号。running_generation是识别其家族的唯一标记。如果子进程的running_generation与父进程相同,则说明本家族的进程尚存在;反之,如果不相同,则它们执行完后必须结束。这正应了一句古语:”覆巢之下无完卵”或者为”树倒猢狲散”阿。

restart_time则记录了主服务器重新启动的时间。

进程间通信则可以使用process_score进行,其定义如下:

typedef struct process_score process_score;

struct process_score{

pid_t pid;

ap_generation_t generation; /* generation of this child */

ap_scoreboard_e sb_type;

int quiescing;

};

通常情况下,父进程往该数据结构中写入数据,而子进程则从其中读取数据。其中pid是主进程的进程号;generation则是当前主进程以及其产生的所有子进程的家族号。sb_type的含义与global_score中的sb_type含义相同。

Quiescing

与process_score用于主进程和子进程通信不同,worker_score则用于记录线程的运行信息,其定义如下:

typedef struct worker_score worker_score;

struct worker_score {

/*第一部分*/

int thread_num;

#if APR_HAS_THREADS

apr_os_thread_t tid;

#endif

unsigned char status;

/*第二部分*/

unsigned long access_count;

apr_off_t bytes_served;

unsigned long my_access_count;

apr_off_t my_bytes_served;

apr_off_t conn_bytes;

unsigned short conn_count;

/*第三部分*/

apr_time_t start_time;

apr_time_t stop_time;

#ifdef HAVE_TIMES

struct tms times;

#endif

apr_time_t last_used;

/*第四部分*/

char client[32]; /* Keep 'em small... */

char request[64]; /* We just want an idea... */

char vhost[32]; /* What virtual host is being accessed? */

};

整个worker_score结构可以被分成四部分理解:

第一部分,主要描述线程的状态和识别信息

thread_numApache识别该线程的唯一识别号,tid则是该线程的线程号。两者是不同的概念:后者是由操作系统或者线程库分配,应用程序无法参与,而前者是Apache设定,跟操作系统无关,具体的含义也只有Apache本身理解。不过thread_num通常遵循下面的设定原则:

thread_num = 线程所在的进程的索引 * 每个进程允许产生的线程极限 + 线程在进程内的索引

status则是当前线程的状态,它的状态种类与进程的状态种类相同,用SERVER_XXX常量进行识别。

第二部分,主要描述线程的状态和识别信息

第三部分,主要描述线程相关的时间信息

start_timestop_time分别是记录线程的启动和停止时间。last_used则用于记录线程最后一次使用的时间。

第四部分,主要描述线程的状态和识别信息

该部分主要描述当前线程处理的请求连接上的相关信息。client是请求客户端的主机名称或者是IP地址。request则是客户端发送的请求行信息,比如”GET /server-status?refresh=100 HTTP/1.1”,而vhost则是当前请求所请求的虚拟主机名称,比如”www.myserver.com”

worker_score结构中可以看出,该结构中的记录的大部分信息并不是线程间通信而需要的。那么这些信息到底做什么用的呢?为什么worker_score结构中需要记录这些信息呢?为此我们必须了解Apache中的一个特殊的功能模块mod_status。尽管这个模块要到第三卷才能详细介绍,但是我们还是提前描述。

为了时刻了解Apache的运行状态,一种方法就是直接在服务器上检测,另一种方法就是远程监控,通过http://www.xxxx.com/server-status URI在浏览器中显示服务器的信息:

 

每一个进程或者线程都将自身信息写入到记分板上,这样,mod_status通过读取记分板,就可以知道各个线程的运行状态信息。当然,如果需要显示的信息越多,记分板上需要保存的信息也就越多,worker_score结构也就需要扩展。

除此之外,Apache中还定义了几个与记分板相关的全局变量,它们是记分板的核心变量:

(1)AP_DECLARE_DATA extern scoreboard *ap_scoreboard_image;

Apache使用使用该变量记录全局记分板,任何进程或者线程都可以通过ap_scoreboard_image直接访问记分板。

(2)AP_DECLARE_DATA extern const char *ap_scoreboard_fname;

该全局变量描述了记分板的名称。

(3)AP_DECLARE_DATA extern int ap_extended_status;

该全局变量描述了当前记分板的状态,

(4)AP_DECLARE_DATA extern ap_generation_t volatile ap_my_generation;

该全局变量描述了当前Apache中的主进程的家族号,任何时候,主进程只要退出进行重新启动,ap_my_generation都会跟着发生变化。该变量与其余的变量相比特殊的地方在于它被声明为volatile类型。

(5)static apr_size_t scoreboard_size;

该变量记录整个记分板所占用的内存的大小。

在了解了记分板的数据结构之后,我们有必要了解一下记分板的内存组织结构,它的内存布局可以用下图进行描述:

 

6.1.1.2记分板处理函数

从前一节的图片中我们看一看出,每个记分板都包括多个插槽,每一个插槽分别用于记录一个进程的相关信息,不过这种记录的进程信息相对非常的简单,仅仅包括进程的当前状态以及进程号。从记分板的角度而言,每一个进程可以处于12中不同的状态:

#define SERVER_DEAD 0 /* 当前的进程执行完毕*/

#define SERVER_STARTING 1 /* 进程刚开始执行 */

#define SERVER_READY 2 /* 进程已经准备就绪,正在等待客户端连接 */

#define SERVER_BUSY_READ 3 /* 进程正在读取客户端的请求*/

#define SERVER_BUSY_WRITE 4 /* 进程正在处理客户端的请求*/

#define SERVER_BUSY_KEEPALIVE 5 /* 进程在同一个活动连接上正在等待更多的请求*/

#define SERVER_BUSY_LOG 6 /* 进程正在进行日志操作*/

#define SERVER_BUSY_DNS 7 /* 进程正在查找主机名称 */

#define SERVER_CLOSING 8 /* 进程正在关闭连接 */

#define SERVER_GRACEFUL 9 /* 进程正在平稳的完成请求 */

#define SERVER_IDLE_KILL 10 /* 进程正在清除空闲子进程. */

#define SERVER_NUM_STATUS 11 /* 进程的多个状态都被设置 */

进程总是从状态SERVER_STARTING开始,最后在SERVER_DEAD状态结束。当一个进程的状态处于SERVER_DEAD的时候,意味着记分板中的该插槽可以被重新利用。

 

6.1.1.2.1创建记分板

记分板的所有的操作都是从创建开始的,通常只有在刚启动Apache或者平稳启动之后才需要创建。Apache中通过ap_create_scoreboard函数实现记分板的创建,该函数在scoreboard.c中实现,函数原型如下:

int ap_create_scoreboard(apr_pool_t *p, ap_scoreboard_e sb_type);

参数p指定创建记分板中所需要的内存来自的内存池,而sb_type则是创建的记分板的类型,或者为SB_SHARED,或者为SB_NOT_SHARED。前者允许记分板在不同的进程之间共享,而后者则不允许。

int running_gen = 0;

int i;

if (ap_scoreboard_image) {

running_gen = ap_scoreboard_image->global->running_generation;

ap_scoreboard_image->global->restart_time = apr_time_now();

memset(ap_scoreboard_image->parent, 0, sizeof(process_score) * server_limit);

for (i = 0; i < server_limit; i++) {

memset(ap_scoreboard_image->servers[i], 0, sizeof(worker_score) * thread_limit);

}

if (lb_limit) {

memset(ap_scoreboard_image->balancers, 0, sizeof(lb_score) * lb_limit);

}

return OK;

}

公告板的创建与几个系统值密切相关的,比如server_limit和thread_limit。server_limit描述了允许同时存在的进程的最大极限,包括父进程和子进程,每一个进程通常都是用上面的process_score数据结构进行描述;而thread_limit则是每一个子进程又允许生成的子线程的数目,这些子线程用worker_score进行描述。因此创建记分板的一个重要的步骤就是分配足够的插槽。由于Apache需要记录每一个进程以及进程中的每一个线程的运行信息,因此,创建记分板之前必须能够分配足够多的空间以容纳process_score和worker_score结构。

正如前面描述,系统中允许存在server_limit个进程,它们中的每一个都必须在记分板中拥有一个插槽,因此我们至少必须分配sizeof(process_score)*server_limit大小的内存空间,同时使用ap_scoreboard_image->parent指向该空间。

同时对于server_limit个进程中的每一个进程,他们可能产生的线程数为thread_limit,这些线程也必须在记分板中拥有相应的插槽,为此共分配server_limit*sizeof(worker_score)*thread_limit的内存大小。

Apache按照最大化的原则进行分配,一旦分配完毕肯定能够保证需要。不过这样的话可能存在很多的空闲插槽。因为即使只有一个进程和一个线程存在,Apache也是会分配所有的内存的。

现在回到上面的代码中。如果是平稳启动,那么在创建新的记分板之前系统中应该已经存在一个旧的记分板(ap_scoreboard_image不为NULL)。在这种情况下,Apache首先得到当前记分板的家族号,同时重新设置启动时间。另外一个重要的任务就是清理初始化记分板上的数据,将其全部清零,彻底扫荡前一个家族的所有信息,并将其返回出去供使用。

ap_calc_scoreboard_size();

如果创建的时候发现公告板不存在,那么这意味着这是Apache启动以来的第一次记分板创建。因此创建之前必须计算记分板分配的空间大小。创建记分板的内存大小由函数ap_calc_scoreboard_size()函数完成:

AP_DECLARE(int) ap_calc_scoreboard_size(void)

{

ap_mpm_query(AP_MPMQ_HARD_LIMIT_THREADS, &thread_limit);

ap_mpm_query(AP_MPMQ_HARD_LIMIT_DAEMONS, &server_limit);

 

if (!proxy_lb_workers)

proxy_lb_workers = APR_RETRIEVE_OPTIONAL_FN(ap_proxy_lb_workers);

if (proxy_lb_workers)

lb_limit = proxy_lb_workers();

else

lb_limit = 0;

 

scoreboard_size = sizeof(global_score); u

scoreboard_size += sizeof(process_score) * server_limit; v

scoreboard_size += sizeof(worker_score) * server_limit * thread_limit; w

 

if (lb_limit)

scoreboard_size += sizeof(lb_score) * lb_limit;x

 

return scoreboard_size;

}

如前所述,记分板内存大小由thread_limit和server_limit决定,这两个值可以通过ap_mpm_query进行查询键值AP_MPMQ_HARD_LIMIT_DAEMONS和AP_MPMQ_HARD_LIMIT_THREADS获取。一旦确定这两个核心值,那么我们就可以计算记分板的分配空间,它包括四个部分:

(1)、global_score的大小,因此在整个系统中global_score只有一个,因此它所占的大小为sizeof(global_score),如u说示。

(2)、由于每一个进程都必须在记分板中拥有一个插槽来记录其相关信息,因此server_limit个进程所占的插槽的大小为server_limit*sizeof(process_score),如v所示。

(3)、对于每一个进程而言,其允许产生的线程的数目为thread_limit个,因此server_limit个进程允许产生的总线程数目为thread_limit个,这些线程也必须在记分板中拥有各自的信息插槽它们所占的内存为server_limit*thread_limit*sizeof(worker_score),如w所示。

(4)、,如x所示。

很容易看出,Apache必须为记分板分配的空间大小为sizeof(global_score) + server_limit*sizeof(process_score) + server_limit*thread_limit*sizeof(worker_thread)+ sizeof(lb_score) * lb_limit。计算后的内存大小保存在scoreboard_size全局变量中。

#if APR_HAS_SHARED_MEMORY

if (sb_type == SB_SHARED) {

void *sb_shared;

rv = open_scoreboard(p);

if (rv || !(sb_shared = apr_shm_baseaddr_get(ap_scoreboard_shm))) {

return HTTP_INTERNAL_SERVER_ERROR;

}

memset(sb_shared, 0, scoreboard_size);

ap_init_scoreboard(sb_shared);

}

else

#endif

{

/* A simple malloc will suffice */

void *sb_mem = calloc(1, scoreboard_size);

if (sb_mem == NULL) {

ap_log_error(APLOG_MARK, APLOG_CRIT, 0, NULL,

"(%d)%s: cannot allocate scoreboard",

errno, strerror(errno));

return HTTP_INTERNAL_SERVER_ERROR;

}

ap_init_scoreboard(sb_mem);

}

正常情况下,记分板应该作为共享内存存在从而被访问,但是并不是所有的操作系统都支持共享内存的操作。因此不同的操作系统,可能会采取不同的措施。

对于那些不支持共享内存的操作系统,Apache只是简单的调用calloc函数分配scoreboard_size大小的内存块,同时调用ap_init_scoreboard对其进行初始化而已;如果操作系统支持共享内存,那么Apache将采用IPC技术创建一块共享内存,同时使用apr_shm_baseaddr_get得到该共享内存的首地址,并对其进行初始化。

ap_scoreboard_image->global->sb_type = sb_type;

ap_scoreboard_image->global->running_generation = running_gen;

ap_scoreboard_image->global->restart_time = apr_time_now();

 

apr_pool_cleanup_register(p, NULL, ap_cleanup_scoreboard, apr_pool_cleanup_null);

在整个记分板创建完毕之后,对global中的全局属性进行设定,同时在内存池销毁链表中注册清除函数。当内存池被销毁的时候,其将调用ap_cleanup_scoreboard对记分板进行清除。

现在我们再回头看看共享内存的初始化函数ap_init_scoreboard(),该函数主要用于对给定的共享内存块进行初始化,只有了解ap_init_scoreboard函数的初始化细节我们才能够明白记分板的内存布局状况。

在初始化的过程中一直存在两个内存块:一个是记分板本身的的内存块,即scoreboard数据结构内存块;一个是分配的共享内存块,其中保存实际的进程以及线程信息。初始化的任务实际上就是将记分板结构中的各个指针指向共享内存块中的相应的位置。

void ap_init_scoreboard(void *shared_score)

{

char *more_storage;

int i;

 

ap_calc_scoreboard_size();

ap_scoreboard_image = calloc(1, sizeof(scoreboard) + server_limit * sizeof(worker_score *) +

server_limit * lb_limit * sizeof(lb_score *));

在初始化之前,分配记分板所用内容。从上面的代码中,我们看到,分配的内存除了sizeof(scoreboard)大小是我们意料之内的,剩余的两部分server_limit*sizeof(worker_score)和server_limit*lb_limit*sizeof(lb_score*)则有点出乎意料之外。这两部分内存的用处我们稍后介绍。

more_storage = shared_score;

ap_scoreboard_image->global = (global_score *)more_storage;

more_storage += sizeof(global_score);

ap_scoreboard_image->parent = (process_score *)more_storage;

more_storage += sizeof(process_score) * server_limit;

 

ap_scoreboard_image->servers =

(worker_score **)((char*)ap_scoreboard_image + sizeof(scoreboard));

for (i = 0; i < server_limit; i++) {

ap_scoreboard_image->servers[i] = (worker_score *)more_storage;

more_storage += thread_limit * sizeof(worker_score);

}

 

if (lb_limit) {

ap_scoreboard_image->balancers = (lb_score *)more_storage;

more_storage += lb_limit * sizeof(lb_score);

}

对于传入的共享内存按照下面的顺序进行布局初始化:首先保存全局共享信息global_score,占用内存sizeof(global_score);然后保存server_limit个进程的信息,占用内存为server_limit *sizeof(process_score);第三部分的内存保存所有进程中产生的线程的信息,占用内存为server_limit*thread_limit*sizeof(worker_score);最后一部分的内存用于保存lb_limit个lb_score结构,四部分合计正好是分配的内存大小。

上述的四部分分别通过ap_scoreboard_image中相关的成员指针指向:global指针指向第一部分,parent指向第二部分,balancers指向第四部分。稍微复杂的这是的三部分线程信息的指定。共享内存中从more_storage指针往后sizeof(process_score)*server_limit的内存区域用于保存进程信息,因此将这块区域赋值给parent指针。

global_score和process_score它们在本质上都可以用一维线性结构进行保存,而worker_score则更类似于二维结构,一方便它要记录本身的信息,另一方面还必须知道它是哪一个进程产生的线程,因此它们的保存方法不太一样。在ap_scoreboard_image分配的内存中我们可以看到除了正常的sizeof(scoreboard)大小之外,还包括了server_limit * sizeof(worker_score *)大小的内存区域。该数组中的每一个元素都是一个指向worker_score结构的指针,指向thread_limit个元素的worker_score类型的数组。因此servers[i]对应的数组记录的则是进程i创建的所thread_limit个线程的信息。按照这种规律,第i个进程内的第j个线程可以用server[i][j]进行描述。这个表达式在后面我们会多次使用到。

经过分配,整个记分板的内存布局可以用下图描述。

 

 

4.1 记分板内存分配图

ap_assert(more_storage == (char*)shared_score + scoreboard_size);

ap_scoreboard_image->global->server_limit = server_limit;

ap_scoreboard_image->global->thread_limit = thread_limit;

ap_scoreboard_image->global->lb_limit = lb_limit;

在所有的处理结束后判断more_storage指针时候指向了需要初始化空间的末尾,即判断more_storage是否与(char*)shared_score + scoreboard_size相等,如果不相等,表明可能出错;

至此,一个完整的记分板已经创建完毕。

6.1.1.2.2记分板插槽管理

对于记分板而言,其最频繁使用的一个功能就是在记分板中查找指定进程信息,函数find_child_by_pid(apr_proc_t *pid)用以完成该功能。

AP_DECLARE(int) find_child_by_pid(apr_proc_t *pid)

{

int i;

int max_daemons_limit;

ap_mpm_query(AP_MPMQ_MAX_DAEMONS, &max_daemons_limit);

for (i = 0; i < max_daemons_limit; ++i) {

if (ap_scoreboard_image->parent[i].pid == pid->pid) {

return i;

}

}

return -1;

}

函数只需要一个参数,就是进程的描述数据结构。函数所做的事情无非就是对记分板中的parent数组逐一比较,判断其进程id是否是指定的id,如果是则表明找到,并返回其索引值;否则意味着没有找到该进程信息。

记分板中的每一个插槽都记录了进程的相关信息,其中一个重要的属性就是当前进程的状态。当进程的状态发生变化的时候,记分板中的状态字段也应该随之变化。ap_update_child_status_from_indexes函数用以更新记分板中指定进程的状态。

AP_DECLARE(int) ap_update_child_status_from_indexes(int child_num,

int thread_num,

int status,

request_rec *r)

函数需要四个参数,child_num是需要更新状态的进程的索引;thread_num则是线程在该进程的线程组中的索引;status是设置的新的状态值;r则是与当前工作线程关联的请求结构。

ws = &ap_scoreboard_image->servers[child_num][thread_num];

old_status = ws->status;

ws->status = status;

 

ps = &ap_scoreboard_image->parent[child_num];

更新之前首先的任务就是获取索引为child_num的进程以及该进程内索引为thread_num的线程的描述数据结构。根据记分板的内存布局很容易理解上面的语句。不过对于线程,在对其进行更改之前必须保存其以前的状态。

if (status == SERVER_READY

&& old_status == SERVER_STARTING) {

ws->thread_num = child_num * thread_limit + thread_num;

ps->generation = ap_my_generation;

}

Apache中每个进程都会用一个唯一的整数进行标识。与此类似,每一个线程也会用一个唯一的整数进行标识。线程的识别号取决于它所在的进程索引以及它在进程内的索引:

线程号 = 线程所在的进程的索引 * 每个进程允许产生的线程极限 + 线程在进程内的索引

线程号实际上就是该线程描述结构在整个线性描述数组中的索引,同时线程的家族号就是父进程的家族号。不过并不是每一个线程都会有一个线程号。只有处于就绪状态或者正在工作的线程才会安排到对应的线程号。如果一个进程刚刚创建尚未准备好处理客户端请求,那么它暂时还不会分配线程号。

if (ap_extended_status) {

ws->last_used = apr_time_now();

if (status == SERVER_READY || status == SERVER_DEAD) {

if (status == SERVER_DEAD) {

ws->my_access_count = 0L;

ws->my_bytes_served = 0L;

}

ws->conn_count = 0;

ws->conn_bytes = 0;

}

if (r) {

conn_rec *c = r->connection;

apr_cpystrn(ws->client, ap_get_remote_host(c, r->per_dir_config,

REMOTE_NOLOOKUP, NULL), sizeof(ws->client));

if (r->the_request == NULL) {

apr_cpystrn(ws->request, "NULL", sizeof(ws->request));

} else if (r->parsed_uri.password == NULL) {

apr_cpystrn(ws->request, r->the_request, sizeof(ws->request));

} else {

apr_cpystrn(ws->request, apr_pstrcat(r->pool, r->method, " ",

apr_uri_unparse(r->pool, &r->parsed_uri,

APR_URI_UNP_OMITPASSWORD),

r->assbackwards ? NULL : " ", r->protocol, NULL),

sizeof(ws->request));

}

apr_cpystrn(ws->vhost, r->server->server_hostname,

sizeof(ws->vhost));

}

}

在前面部分,我们讨论worker_score结构的时候讨论了mod_status模块,它允许详细的显示进程和线程的状态信息。不过这也是有条件的。通常情况下很容易理解线程信息显示的越详细肯定会影响服务器的效率。因此通常情况下,监控信息显示的都不是很详细。除非你手工设置。Apache中提供了一个指令ExtendedStatus来控制是否需要在记分板中记录每个线程的详细信息。该指定反映到程序中则是通过全局变量ap_extended_status来控制。ap_extended_stauts1的话则意味着必须将线程的详细信息写入到记分板中。

上面的代码正是在记分板中记录线程的详细信息,包括请求客户端的IP地址,请求行以及请求虚拟主机名称。

6.1.1.2.3记分板内存释放

当记分板不再使用的时候,记分板占用的内存必须被使用。记分板的释放通常只在Apache完全重新启动的时候才会进行。对于平稳启动,记分板不会被释放,只是完成重新初始化。

记分板通过函数ap_cleanup_scoreboard()完成内存释放。

apr_status_t ap_cleanup_scoreboard(void *d)

{

if (ap_scoreboard_image == NULL) {

return APR_SUCCESS;

}

if (ap_scoreboard_image->global->sb_type == SB_SHARED) {

ap_cleanup_shared_mem(NULL);

}

else {

free(ap_scoreboard_image->global);

free(ap_scoreboard_image);

ap_scoreboard_image = NULL;

}

return APR_SUCCESS;

}

如果记分板没有被共享,那么对它的释放就非常简单,直接调用free即可,如前所述,记分板中的两个内存块分别用ap_scoreboard_imageap_scoreboard_image->global分别标识。

如果记分板被用于进程间共享,则还必须调用共享内存的相关删除函数。