标签 Apache 下的文章

APR源代码分析-进程同步篇

最新的统计数据显示Apache服务器在全世界仍然占据着Web服务器龙头老大的位置,而且市场占有率遥遥领先,所以学习Apache相关知识是完全正确的方向,这里我们继续分析APR进程同步相关内容。

进程同步的源代码的位置在$(APR_HOME)/locks目录下,本篇blog着重分析unix子目录下的proc_mutex.c、global_mutex文件内容,其相应头文件为$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同进程之间的同步以及多进程多线程中的同步问题。

APR提供三种同步措施,分别为:
apr_thread_mutex_t – 支持单个进程内的多线程同步;
apr_proc_mutex_t – 支持多个进程间的同步;
apr_global_mutex_t  – 支持不同进程内的不同线程间同步。
在本篇中着重分析apr_proc_mutex_t。

1、同步机制
APR提供多种进程同步的机制供选择使用。在apr_proc_mutex.h中列举了究竟有哪些同步机制:
typedef enum {
    APR_LOCK_FCNTL,         /* 记录上锁 */
    APR_LOCK_FLOCK,         /* 文件上锁 */
    APR_LOCK_SYSVSEM,       /* 系统V信号量 */
    APR_LOCK_PROC_PTHREAD,  /* 利用pthread线程锁特性 */
    APR_LOCK_POSIXSEM,      /* POSIX信号量 */
    APR_LOCK_DEFAULT        /* 默认进程间锁 */
} apr_lockmech_e;

这几种锁机制,随便拿出哪一种都很复杂。APR的代码注释中强调了一点就是“只有APR_LOCK_DEFAULT”是可移植的。这样一来用户若要使用APR进程同步机制接口,就必须显式指定一种同步机制。

2、实现点滴
APR提供每种同步机制的实现,每种机制体现为一组函数接口,这些接口被封装在一个结构体类型中:

/* in apr_arch_proc_mutex.h */
struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);
    const char *name;
};

之后在apr_proc_mutex_t类型中,apr_proc_mutex_unix_lock_methods_t的出现也就在情理之中了:)
/* in apr_arch_proc_mutex.h */
struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth;
    const apr_proc_mutex_unix_lock_methods_t *inter_meth;
    int curr_locked;
    char *fname;
    … …
#if APR_HAS_PROC_PTHREAD_SERIALIZE
    pthread_mutex_t *pthread_interproc;
#endif
};

这样APR提供的用户接口其实就是对mech各个“成员函数”功能的“薄封装”,而真正干活的其实是apr_proc_mutex_t中的meth字段的“成员函数”,它们的工作包括mutex的创建、获取(加锁)和清除(解锁)等。以“获取锁”为例APR的实现如下:
APR_DECLARE(apr_status_t) apr_proc_mutex_lock(apr_proc_mutex_t *mutex)
{
    return mutex->meth->acquire(mutex);
}

3、同步机制
按照枚举类型apr_lockmech_e的声明,我们知道APR为我们提供了5种同步机制,下面分别简单说说:
(1) 记录锁
记录锁是一种建议性锁,它不能防止一个进程写已由另一个进程上了读锁的文件,它主要利用fcntl系统调用来完成锁功能的,记得在以前的一篇关于APR 文件I/O的Blog中谈过记录锁,这里不再详细叙述了。

(2) 文件锁
文件锁是记录锁的一个特例,其功能由函数接口flock支持。值得说明的是它仅仅提供“写入锁”(独占锁),而不提供“读入锁”(共享锁)。

(3) System V信号量
System V信号量是一种内核维护的信号量,所以我们只需调用semget获取一个System V信号量的描述符即可。值得注意的是与POSIX的单个“计数信号量”不同的是System V信号量是一个“计数信号量集”。所以我们在注意的是在初始化时设定好信号量集的属性以及在调用semop时正确选择信号量集中的信号量。在APR的System V信号量集中只是申请了一个信号量。

(4) 利用线程互斥锁机制
APR使用pthread提供的互斥锁机制。原本pthread互斥锁是用来互斥一个进程内的各个线程的,但APR在共享内存中创建了pthread_mutex_t,这样使得不同进程的主线程实现互斥,从而达到进程间互斥的目的。截取部分代码如下:
new_mutex->pthread_interproc = (pthread_mutex_t *)mmap(
                                       (caddr_t) 0,
                                       sizeof(pthread_mutex_t),
                                       PROT_READ | PROT_WRITE, MAP_SHARED,
                                       fd, 0);

(5) POSIX信号量
APR使用了POSIX有名信号量机制,从下面的代码中我们可以看出这一点:
/* in proc_mutex.c */
apr_snprintf(semname, sizeof(semname), "/ApR.%lxZ%lx", sec, usec); /* APR自定义了一种POSIX信号量命名规则,在源代码中有说明 */
psem = sem_open(semname, O_CREAT, 0644, 1);

4、如何使用
我们知道父进程的锁其子进程并不继承。APR进程同步机制的一个典型使用方法就是:“Create the mutex in the Parent, Attach to it in the Child”。APR提供接口apr_proc_mutex_child_init在子进程中re-open the mutex。

5、小结
APR提供多种锁机制,所以使用的时候要根据具体应用情况细心选择。

APR源代码分析-环篇

APR中少见对数据结构的封装,好像唯一例外的就是其对循环链表,即环(RING)的封装。

在大学的时候学的不是计算机专业,但大三的时候我所学的专业曾开过一门好像叫“计算机软件开发基础”的课,使用的是清华的一本教材,课程的内容包括数据结构。说实话听过几节课,那个老师讲的还不错,只是由于课程目标所限,没讲那么深罢了。当然我接触数据结构要早于这门课的开课时间。早在大一下学期就开始到计算机专业旁听“数据结构”,再说一次实话,虽号称名校名专业,但是那个老师的讲课水平却不敢恭维。

言归正传! 简单说说环(RING):环是一个首尾相连的双向链表,也就是我们所说的循环链表。对应清华的那本经典的《数据结构》一书中线性表一章的内容,按照书中分类其属于线性表中的链式存储的一种。环是很常见也很实用的数据结构,相信在这个世界上环的实现不止成千上万,但是APR RING(按照APR RING源代码中的注释所说,APR RING的实现源自4.4BSD)却是其中较独特的一个,其最大的特点是其所有对RING的操作都由一组宏(大约30个左右)来实现。在这里不能逐个分析,仅说说一些让人印象深刻的方面吧。

1、如何使用APR RING?
我们先来点感性认识! 下面是一个典型的使用APR RING的样例:
假设环节点的结构如下:
struct  elem_t {    /* APR RING链接的元素类型定义 */
    APR_RING_ENTRY(elem_t)  link; /* 链接域 */
    int                                     foo; /* 数据域 */
};

APR_RING_HEAD(elem_head_t, elem_t);

int main() {
    struct elem_head_t  head;
    struct elem_t       *el;

    APR_RING_INIT(&head, elem_t, link);

    /* 使用其他操作宏插入、删除等操作,例如 */
    el = malloc(sizeof(elem_t);
    el->foo = 20051103;
    APR_RING_ELEM_INIT(el, link);
    APR_RING_INSERT_TAIL(&h, el, elem_t, link);
}

2、APR RING的难点–“哨兵”
环是通过头节点来管理的,头节点是这样一种节点,其next指针指向RING的第一个节点,其prev指针指向RING的最后一个节点,即尾节点。但是通过察看源码发现APR RING通过APR_RING_HEAD宏定义的头节点形式如下:
#define APR_RING_HEAD(head, elem)     \
    struct head {       \
             struct elem *next;      \
             struct elem *prev;      \
    }
如果按照上面的例子进行宏展开,其形式如下:
struct elem_head_t {
     struct elem_t *next;
     struct elem_t *prev;
};

而一个普通的元素elem_t展开形式如下:
struct elem_t {
     struct {       \
        struct elem_t *next;     \
        struct elem_t *prev;     \
     } link;

     int foo;
};
通过对比可以看得出头节点仅仅相当于一个elem_t的link域。这样做的话必然带来对普通节点和头节点在处理上的不一致,为了避免这种情况的发生,APR RING引入了“哨兵(sentinel)”节点的概念。我们先看看哨兵节点在整个链表中的位置。

sentinel->next = 链表的第一个节点;
sentinel->prev = 链表的最后一个节点;

但是察看APR RING的源码你会发现sentinel节点只是个虚拟存在的节点,这个虚拟节点既有数据域(虚拟出来的,不能引用)又有链接域,好似与普通节点并无差别。在APR RING的源文件中使用了下面这幅图来说明sentinel的位置,同时也指出了sentinel和head的关系 — head即为sentinel虚拟节点的link域。

 普通节点
+->+——-+<–
   |struct |
   |elem   |
   +——-+
   |prev   |
   |   next|
   +——-+
   | etc.  |
   .       .
   .       .

sentinel节点
+->+——–+<–
   |sentinel|
   |elem    |
   +——–+
   |ring    |
   |   head |
   +——–+

再看看下面APR_RING_INIT的源代码:
#define APR_RING_INIT(hp, elem, link) do {    \
            APR_RING_FIRST((hp)) = APR_RING_SENTINEL((hp), elem, link); \
           APR_RING_LAST((hp))  = APR_RING_SENTINEL((hp), elem, link); \
    } while (0)
你会发现:初始化RING实际上是将head的next和prev指针都指向了sentinel虚拟节点了。从sentinel的角度来说相当于其自己的link域的next和prev都指向了自己。所以判断APR RING是否为空只需要判断RING的首个节点是否为sentinel虚拟节点即可。APR_RING_EMPTY宏就是这么做的:
#define APR_RING_EMPTY(hp, elem, link)     \
    (APR_RING_FIRST((hp)) == APR_RING_SENTINEL((hp), elem, link))

那么如何计算sentinel虚拟节点的地址呢?
我们这样思考:从普通节点说起,如果我们知道一个普通节点的首地址(elem_addr),那么我们计算其link域的地址(link_addr)的公式就应该为link_addr = elem_addr + offsetof(elem_t, link);前面我们一直在说sentinel虚拟节点看起来和普通节点没什么区别,所以它仍然符合该计算公式。前面我们又说过head_addr是sentinel节点的link域,这样的话我们将head_addr输入到公式中得到head_addr = sentinel_addr + offsetof(elem_t, link),做一下变换即可得到sentinel_addr = head_addr – offsetof(elem_t, link)。看看APR RING源代码就是这样实现的:
#define APR_RING_SENTINEL(hp, elem, link)    \
    (struct elem *)((char *)(hp) – APR_OFFSETOF(struct elem, link))

至此APR RING使用一个虚拟sentinel节点分隔RING的首尾节点,已达到对节点操作一致的目的。

3、使用时注意事项
这里在使用APR RING时有几点限制:
a) 在定义RING的元素结构时,需要把APR_RING_ENTRY放在结构的第一个字段的位置。
b) 链接一种类型的元素就要使用APR_RING_HEAD宏定义该种类型RING的头节点类型。学过C++或者了解泛型的人可能都会体味到这里的设计有那么一点范型的味道。比如:
模板:APR_RING_HEAD(T_HEAD, T) —- 链接—-> T类型元素
实例化:APR_RING_HEAD(elem_head_t, elem_t) — 链接—->elem_t类型元素
 
4、APR RING不足之处
1) 缺少遍历接口
浏览APR RING源码后发现缺少一个遍历宏接口,这里提供一种正向遍历实现:

#define APR_RING_TRAVERSE(ep, hp, elem, link)    \
            for ((ep)  = APR_RING_FIRST((hp));     \
            (ep) != APR_RING_SENTINEL((hp), elem, link);   \
           (ep)  = APR_RING_NEXT((ep), link))
大家还可以模仿写出反向遍历的接口APR_RING_REVERSE_TRAVERSE。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats