存档

文章标签 ‘Apache’

Apache MPM Prefork设计方法浅析

五月 16th, {2011 2 条评论 9,576 人阅读过  

最近几天翻阅了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还有平稳启动机制,关于平衡启动的代码我暂时略过了,没有细读,以后有时间再回过头来仔细研究。

分类: C/C++ 标签:

Apache基础数据结构(tables)代码浅析

四月 29th, {2011 1 条评论 7,255 人阅读过  

提到Apache社区脑子里立马会呈现出一系列的Java项目集合,尽管@gnawux师兄教导我不要纠结于语言,但对于Java的抵制还是很难一下子就消失的,所幸Apache社区最重量级的项目Apache开源HTTP服务器httpd的源代码是完全使用C语言开发的,尽管近年来涌现出种种轻量级高性能Web服务器,Apache仍以它的功能广泛和真正的高性能而处于无可取代的位置。

Apache也经常受人诟病,矛头直指它的低效,我没有太多这方面的经验,因此没有过多的发言权,看到一些大牛们对这个问题的评论是觉得Apache低效是因为对Apache缺少了解。我个人也觉得这样一款多年风行Web服务器领域的软件肯定有它存在的理由,我们认为它不好可能是我们对它的了解不够。

在网上看到过关于Apache源码是否值得阅读的讨论,很多人认为Apache源码组织结构不好,不太适合阅读,更多人推荐nginx等源码,其实我觉得像Apache这种项目正是”重剑无锋,大巧不工“,Linux源码也有人评价说组织的不好,可读它的代码也仍然非常有意义。我个人认为阅读Apache源码一方面能加深对Apache的了解,在使用的时候真正发挥出它的高性能,另一方面也能借鉴它作为大型服务器软件设计的方法。当然,如果要编写扩展模块来实现一些附加功能的话那阅读源码甚至是必须的工作。

以上都是我个人的理解,最近简单地翻阅了部分代码,长时间没有更新博客,这里就写一些Apache里面最基本的tables数据结构(和其它模块关联较小,相对比较独立,当然我也觉得读任何源码都应该先搞清楚它最基础的数据结构)。

C语言里面没有类似于STL的vector这样的数据结构,动态数组肯定是需要自己来实现的。看一下apache里面关于动态数组结构的定义:

struct apr_array_header_t {
    /** The pool the array is allocated out of */
    apr_pool_t *pool;
    /** The amount of memory allocated for each element of the array */
    int elt_size;
    /** The number of active elements in the array */
    int nelts;
    /** The number of elements allocated in the array */
    int nalloc;
    /** The elements in the array */
    char *elts;
};

pool是内存池,apache有自己的内存管理机制,它在内存池中分配内存,而不是直接使用malloc分配,内存池由apache统一进行管理,这是后话,现在只需要知道在apache中如果要申请一块内存空间就必须有一个对应的内存池。

elt_size是数组中每个元素的大小。

nelts是数组中活跃元素的个数。何为活跃元素?相对于nalloc来看,nalloc是数组中分配空间的元素个数,它在数组创建的时候会先分配nalloc个元素的空间,而这些内存空间仅仅分配而未进行初始化,也就是nelts为0,当往数组中插入元素时,nelts会递增,nelts也就是数组中实际存在的元素个数。当nelts个数大于nalloc时,则表明数组中已分配的内存空间不够,则需要再从内存池中申请额外的空间来扩充数组的容量。

elts便指向元素列表的起始地址。

创建一个数组:

APR_DECLARE(apr_array_header_t *) apr_array_make(apr_pool_t *p,
						int nelts, int elt_size)
{
    apr_array_header_t *res;
 
    res = (apr_array_header_t *) apr_palloc(p, sizeof(apr_array_header_t));
    make_array_core(res, p, nelts, elt_size, 1);
    return res;
}
 
static void make_array_core(apr_array_header_t *res, apr_pool_t *p,
			    int nelts, int elt_size, int clear)
{
    /*
     * Assure sanity if someone asks for
     * array of zero elts.
     */
    if (nelts < 1) {
        nelts = 1;
    }
 
    if (clear) {
        res->elts = apr_pcalloc(p, nelts * elt_size);
    }
    else {
        res->elts = apr_palloc(p, nelts * elt_size);
    }
 
    res->pool = p;
    res->elt_size = elt_size;
    res->nelts = 0;		/* No active elements yet... */
    res->nalloc = nelts;	/* ...but this many allocated */
}

上面的两个函数功能都很清晰,没有什么需要解释的,只是分配内存用的是apr_palloc()和apr_pcalloc()两个函数从内存池中分配内存,这两个函数的区别是后者在分配了内存之后会先清零,这也就是那个clear参数的作用了。

往数组中压入元素:

APR_DECLARE(void *) apr_array_push(apr_array_header_t *arr)
{
    if (arr->nelts == arr->nalloc) {
        int new_size = (arr->nalloc <= 0) ? 1 : arr->nalloc * 2;
        char *new_data;
 
        new_data = apr_palloc(arr->pool, arr->elt_size * new_size);
 
        memcpy(new_data, arr->elts, arr->nalloc * arr->elt_size);
        memset(new_data + arr->nalloc * arr->elt_size, 0,
               arr->elt_size * (new_size - arr->nalloc));
        arr->elts = new_data;
        arr->nalloc = new_size;
    }
 
    ++arr->nelts;
    return arr->elts + (arr->elt_size * (arr->nelts - 1));
}

上面提到如果数组中活跃元素已经占满了所有的已分配的内存空间之后,便需要向内存池申请额外的内存才能便新元素得已插入,apache的做法也很简单,申请双陪的内存,将旧内存中的内容copy到新内存中的前半部分,后半部分清零,或不做处理(apr_array_push_noclear()函数)。然后就是增加活跃元素数,返回最新元素的内存地址。

还有一系列关于数据操作的函数,apr_array_pop()从数组中弹出元素,apr_array_cat()连接两个数组,apr_array_copy()拷贝一个数组,原理都很简单,不再一一赘述,详情可见srclib/arp/tables/apr_tables.c

接下来看apache里面另一个很重要的数据结构,一个类似于数据字典的table,但它允许有多个相同健的元素存在。

struct apr_table_t {
    /* This has to be first to promote backwards compatibility with
     * older modules which cast a apr_table_t * to an apr_array_header_t *...
     * they should use the apr_table_elts() function for most of the
     * cases they do this for.
     */
    /** The underlying array for the table */
    apr_array_header_t a;
    /** Who created the array. */
    void *creator;
    /* An index to speed up table lookups.  The way this works is:
     *   - Hash the key into the index:
     *     - index_first[TABLE_HASH(key)] is the offset within
     *       the table of the first entry with that key
     *     - index_last[TABLE_HASH(key)] is the offset within
     *       the table of the last entry with that key
     *   - If (and only if) there is no entry in the table whose
     *     key hashes to index element i, then the i'th bit
     *     of index_initialized will be zero.  (Check this before
     *     trying to use index_first[i] or index_last[i]!)
     */
    apr_uint32_t index_initialized;
    int index_first[TABLE_HASH_SIZE];
    int index_last[TABLE_HASH_SIZE];
};

数据表中的元素是存放在变量名为a的数组中的。creator是该数据表的创建者,通常是个字符串。最后的三个字段是比较有学问的字段。TABLE_HASH_SIZE宏的大小定义为32,index_initialized变量也是一个32位的常数,该变量用于对每个key的hash值进行一个映射,key的hash算法很简单:

#define TABLE_HASH(key)  (TABLE_INDEX_MASK & *(unsigned char *)(key))

相当于把key的前8个字节转换成整形再与32求余,最后结果是一个小于32的整数。于是,当表中存在某个key时,假设key对应的hash值i,则index_initialized的第i位是被置位的,这样可以快速地检测某个key是否在表中存在。

至于index_first和index_last这两上数组中存储的是某个hash值所对应的元素在a数组中的索引范围(肯定是需要一个范围的,32个元素的hash表非常容易出现碰撞)。如果key对应的hash值为4,而index_first[4]=22,index_last[4]=26,则要查找的元素在a数组中第22个元素到第26个元素之间。至于index_last和index_first最面的值是怎么确定的接下来讨论。

需要提一下的是a数组中每一个元素都是一个apr_entry_t结构:

struct apr_table_entry_t {
    /** The key for the current table entry */
    char *key;          /* maybe NULL in future;
                         * check when iterating thru table_elts
                         */
    /** The value for the current table entry */
    char *val;
 
    /** A checksum for the key, for use by the apr_table internals */
    apr_uint32_t key_checksum;
};

我们看下是如何获取某个健值的:

APR_DECLARE(const char *) apr_table_get(const apr_table_t *t, const char *key)
{
    apr_table_entry_t *next_elt;
    apr_table_entry_t *end_elt;
    apr_uint32_t checksum;
    int hash;
 
    if (key == NULL) {
	return NULL;
    }
 
    hash = TABLE_HASH(key);
    if (!TABLE_INDEX_IS_INITIALIZED(t, hash)) {
        return NULL;
    }
    COMPUTE_KEY_CHECKSUM(key, checksum);
    next_elt = ((apr_table_entry_t *) t->a.elts) + t->index_first[hash];;
    end_elt = ((apr_table_entry_t *) t->a.elts) + t->index_last[hash];
 
    for (; next_elt <= end_elt; next_elt++) {
	if ((checksum == next_elt->key_checksum) &&
            !strcasecmp(next_elt->key, key)) {
	    return next_elt->val;
	}
    }
 
    return NULL;
}

程序遍历index_first[hash]和index_last[hash]区间内的链表,通过比对健值来获取要寻找的值。

接下来看设置某个健值的函数,稍微复杂一点:

APR_DECLARE(void) apr_table_set(apr_table_t *t, const char *key,
                                const char *val)
{
    apr_table_entry_t *next_elt;
    apr_table_entry_t *end_elt;
    apr_table_entry_t *table_end;
    apr_uint32_t checksum;
    int hash;
 
    COMPUTE_KEY_CHECKSUM(key, checksum);
    hash = TABLE_HASH(key);
    if (!TABLE_INDEX_IS_INITIALIZED(t, hash)) {
        t->index_first[hash] = t->a.nelts;
        TABLE_SET_INDEX_INITIALIZED(t, hash);
        goto add_new_elt;
    }
    next_elt = ((apr_table_entry_t *) t->a.elts) + t->index_first[hash];;
    end_elt = ((apr_table_entry_t *) t->a.elts) + t->index_last[hash];
    table_end =((apr_table_entry_t *) t->a.elts) + t->a.nelts;
 
    for (; next_elt <= end_elt; next_elt++) {
	if ((checksum == next_elt->key_checksum) &&
            !strcasecmp(next_elt->key, key)) {
 
            /* Found an existing entry with the same key, so overwrite it */
 
            int must_reindex = 0;
            apr_table_entry_t *dst_elt = NULL;
 
            next_elt->val = apr_pstrdup(t->a.pool, val);
 
            /* Remove any other instances of this key */
            for (next_elt++; next_elt <= end_elt; next_elt++) {
                if ((checksum == next_elt->key_checksum) &&
                    !strcasecmp(next_elt->key, key)) {
                    t->a.nelts--;
                    if (!dst_elt) {
                        dst_elt = next_elt;
                    }
                }
                else if (dst_elt) {
                    *dst_elt++ = *next_elt;
                    must_reindex = 1;
                }
            }
 
            /* If we've removed anything, shift over the remainder
             * of the table (note that the previous loop didn't
             * run to the end of the table, just to the last match
             * for the index)
             */
            if (dst_elt) {
                for (; next_elt < table_end; next_elt++) {
                    *dst_elt++ = *next_elt;
                }
                must_reindex = 1;
            }
            if (must_reindex) {
                table_reindex(t);
            }
            return;
        }
    }
 
add_new_elt:
    t->index_last[hash] = t->a.nelts;
    next_elt = (apr_table_entry_t *) table_push(t);
    next_elt->key = apr_pstrdup(t->a.pool, key);
    next_elt->val = apr_pstrdup(t->a.pool, val);
    next_elt->key_checksum = checksum;
}

TABLE_INDEX_IS_INITIALIZED(t,i)这个宏是检测index_initialized这个变量中的第i位是否被置位。如果没有被置位,则表示该key对应的节点已经在a数组中存在。如果存在则index_first[hash]和index_last[hash]都是存在的,为什么呢?先假设key对应的hash位未被置位,则新插入该key时,我们会把这个key对应的apr_entry_t结构放到a数组的最后,它的索引值便是a.nelts,这时候我们也把index_first[hash]和index_last[hash]的值都设置为a.nelts。当插入另一个key时,它的hash值可能与上一个key出现碰撞,此时该hash值对应的index_first[hash]的索引值不变,将index_last[hash]设置为新索引值,这样这两个key所对应的apr_entry_t结构便都存在于a[index_first[hash]]和a[index_last[a]]之间,我们只需要遍历找到该结构并将其val属性设置为我们要设置的值即可。同时由于该表允许多个相同健的存在,我们在设置健值之后也需要从表中移除其它的具有相同健的对象。与table有关的函数也有很多,大原理都比较简单,也不再赘述了。

本文纯属个人见解,如有谬误烦请指出。

分类: C/C++ 标签: