参考资料学习APR库

APR分析-整体篇

    一、何为APR?

         Apache Server经过这么多年的发展后,将一些通用的运行时接口封装起来供给大家,这就是Apache Portable Run-time libraries,APR。

    二、APR的目录组织

          1)所有的头文件都放在$(APR)/include目录中;

          2)所有功能接口的实现都放在各自的独立目录下,如threadproc、mmap等;

          3)此外就是相关平台构建工具文件如Makefile.in等。曾经看过ACE的代码,ACE的所有源文件(.cpp)都放在一个目录下,显得很混乱。

           4)进入各功能接口子目录,以threadproc为例,在其下面的子目录有5个,分别是beos、netware、os2、unix和win32.

    三、APR构建

           如果想要使用APR,需要先在特定平台上构建它,这里不考虑多个平台的特性,仅针对Unix平台进行分析。

           1) apr.h、apr.h.in、apr.h.hw和apr.h.hnw的关系

               在$(APR)/include目录下,由于APR考虑移植性等原因,最基本的apr.h文件是在构建时自动生成的,其中apr.h.in类似一模板作为apr.h生成程序的输入源。其中apr.h.hw和apr.h.hnw分别是Windows和NetWare的特定版本。

           2) 编译时注意事项

                在Unix上编译时,注意$(APR)/build下*.sh文件的访问权限,应该先chmod以下,否则Make的时候会提示ERROR。

    四、应用APR

            我们首先make install一下,比如我们在Makefile中指定prefix=$(APR)/dist,则make install后,在$(APR)/dist下会发现4个子目录,分别为bin、lib、include和build,其中我们感兴趣的只有include和lib。

             下面是一个APR app的例子project。

             该工程的目录组织如下:

$(apr_path)
 - dist
    - lib
    - include
 - examples
    - apr_app
      - Make.properties
      - Makefile
      - apr_app.c

              我们的Make.properties文件内容如下:

#
# The APR app demo
#
CC             = gcc -Wall
BASEDIR         =$(HOME)/apr-1.1.1/examples/apr_app
APRDIR          =$(HOME)/apr-1.1.1
APRVER          = 1
APRINCL         =$(APRDIR)/dist/include/apr-$(APRVER)
APRLIB          =$(APRDIR)/dist/lib
DEFS           = -D_REENTRANT -D_POSIX_PTHREAD_SEMANTICS -D_DEBUG_
LIBS           = -L$(APRLIB) -lapr-$(APRVER) /
                 -lpthread -lxnet -lposix4 -ldl -lkstat -lnsl -lkvm -lz -lelf -lm -lsocket -ladm

INCL           = -I$(APRINCL)
CFLAGS          =$(DEFS) $(INCL)

Makefile文件内容如下:
include Make.properties
TARGET  = apr_app
OBJS    = apr_app.o
all: $(TARGET)
$(TARGET): $(OBJS)
        $(CC) ${CFLAGS} -o $@$(OBJS) ${LIBS}
clean:
        rm -f core $(TARGET)$(OBJS)

     而apr_app.c文件采用的是$(par_path)/test目录下的proc_child.c文件。

五、GO ON

    5.1 APR分析-设计篇

        5.1.1 类型

             1) APR提供的节本自定义数据类型包括:

typedef unsigned char apr_byte_t;
typedef short apr_int16_t;
typedef unsigned short apr_uint16_t;
typedef int apr_int32_t;
typedef unsigned int apr_uint32_t;
typedef long long apr_uint32_t;
typedef long long apr_int64_t;
typedef unsigned long long apr_uint64_t;

             这些都是在apr.h中定义的。

             2)在APR的设计文档中,它称"dso、mmap、process、thread"等为"base types"。

             3) 另外的一个特点就是大多APR类型中都包含一个apr_pool_t类型的字段,你最好在该类型中假如一个apr_pool_t类型的字段,否则所有操作该类型的APR函数都需要一个apr_pool_t类型的参数。

       5.1.2 函数

            1) APR的固定个数参数公共函数的声明形式APR_DECLARE(rettype) apr_func(args);而非固定个数参数的公共函数的声明形式为APR_DECLARE_NONSTD(rettype) apr_func(args,...);".在Unix上的apr.h中有着两个宏的定义:

#define APR_DECLARE(type) type
#define APR_DECLARE_NONSTD(type)  type

                  在apr.h文件中解释了这么做就是为了在不同平台上编译时使用“the most appropriate calling convention”,这里的"calling convention"是一术语,叫"调用约定".常见的调用约定有:stdcall、cdecl、fastcall、thiscall和naked call,其中cdecl调用约定又称为C调用约定,是C语言缺省的调用约定。

             2)如果你想新增APR函数,APR建议你最好按照如下做:

                 a)输出参数为第一个参数:

                  b)如果某个函数需要内部分配内存,则将一个apr_pool_t 参数放在最后。

          5.1.3 错误处理

                APR作为一通用的库接口集合详细的说明了使用APR时如何进行错误处理。

               1) 错误处理的第一步就是"错误码和状态码分类"。APR的函数大部分都返回apr_status_t类型的错误码,这是一个int型,在apr_errno.h中定义,和它在一起定义的还有apr所用的所有错误码和状态码。

                2) 如何定义错误捕捉策略?

                   由于APR是可移植的,这样就可能遇到这样一个问题:不同平台错误码的不一致。如何处理呢?APR给我们提供了2中策略:

                     a) 跨多平台返回相同的错误码

                     b)返回平台相关错误码,如果需要将它转换为通用错误码

                         程序的执行录像往往要根据函数返回错误码来定,这么做的缺点就是把这些工作推给了程序员。执行流程如下:

                         make syscall that fails

                                   return error code

                       ----------------------------------------------------------------

                                          convert to common error code (using ap_canonical_error)

                                          decide execution based on common error code

                    ///

                    注1 调用约定

                           我们知道函数调用是通过栈操作来完成的,在栈操作过程中需要函数的调用者和被调用者在下面的两个问题上作出协调,达成协议:

                           a) 当参数个数多余一个时,按照什么顺序把参数压入堆栈

                           b) 函数调用后,由谁来把堆栈回复原来状态

                           在像C/C++这样的中、高级语言中,使用"调用约定"来说明这两个问题。

                   ///

   5.2 APR分析-进程篇

         Apache Server的进程调度一直为人所称道,Apache 2.0推出的APR对进程进行了封装,特别是Apache 2.0的MPM(Multiple Precess Management)框架就是以APR封装的进程为基础的。

             APR进程封装源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的proc.c文件内容,其相应头文件为$(APR_HOME)/include/apr_thread_proc.h。

         一、APR进程概述

              APR进程封装采用了传统的fork-exec配合方式(spawn),即父进程在fork出子进程后继续执行其自己的代码,而子进程调用exec函数加载新的程序影响到其地址空间,执行新的程序。我们先来看看使用APR创建一个新的进程的流程,然后再根据流程做细节分析:

apr_proc_t newproc;
apr_pool_t *p;
apr_status_t rv;
const char *args[2];
apr_procattr_t *attr;
/* 初始化APR内部使用的内存*/
rv = apr_pool_initialize();
HANDLE_RTVAL(apr_pool_initialize, rv);
rv = apr_pool_create(&p, NULL);
HANDLE_RTVAL(apr_pool_create, rv);
/*创建并初始化新进程的属性*/
rv = apr_procattr_create(&attr, p);
HANDLE_RTVAL(apr_procattr_create, rv);
rv = apr_procattr_io_set(attr, APR_FULL_BLOCK, APR_FULL_BLOCK, APR_NO_PIPE); /*可选*/
HANDLE_RTVAL(apr_procattr_io_set, rv);
rv = apr_procattr_dir_set(attr, "startup_path");/*可选*/
HANDLE_RTVAL(apr_procattr_dir_set, rv);
rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM);/*可选*/
HANDLE_RTVAL(apr_procattr_cmdtype_set, rv);
... .../*其他设置进程属性的函数*/
/*创建新进程*/
args[0] = "proc_child";
args[1] = NULL;
rv = apr_proc_create(&newproc,"your_progname", args, NULL, attr, p);
HANDLE_RTVAL(apr_proc_create, rv);
/*等待子进程结束*/
rv = apr_proc_wait(&newproc, NULL, NULL, APR_WAIT);
HANDLE_RTVAL(apr_proc_wait, rv);

           二、APR procattr创建

                 在我们平时的Unix进程相关编程时,我们大致会接触两类进程操作函数:进程创建函数(如fork和exec等)和进程属性操作函数(getpid、chdir等),APR将进程的相关属性信息封装到apr_procattr_t结构体中,我们来看看这个重要的结构体定义:(这里只列出Unix下可用的属性)

/* in $(APR_HOME)/include/arch/unix/apr_arch_threadproc.h */
struct apr_procattr_t {
  /* PART 1 */
  apr_pool_t *pool;
  /* PART 2 */
  apr_file_t *parent_in;
  apr_file_t *child_in;
  apr_file_t *parent_out;
  apr_file_t *child_out;
  apr_file_t *parent_err;
  apr_file_t *child_err;
  /* PART 3 */
  char *currdir;
  apr_int32_t cmdtype;
  apr_int32_t detached;
  /* PART 4 */
  struct rlimit *limit_cpu;
  struct rlimit *limit_mem;
  struct rlimit *limit_nproc;
  struct rlimit *limit_nofile;
  /* PART 5 */
  apr_child_errfn_t *errfn;
  apr_int32_t errchk;
  /* PART 6 */
  apr_uid_t uid;
  apr_gid_t gid;
};

       我这里讲apr_procattr_t 包含的字段大致分为6部分,下面逐一说明:

        [PART 1]

        在上一篇关于APR的blog中说过,大部分的APR类型中都会有一个apr_pool_t 类型字段,用于APR内部的内存管理,此结构也无例外。该字段用来标识procattr在哪个pool中分配的内存。

        [PART 2]

        进程不是孤立存在的,进程也是由父有子的。父子进程间通过传统的匿名pipe进行通信。在apr_procattr_io_set(attr,APR_FULL_BLOCK,APR_FULL_BLOCK,APR_FULL_BLOCK)调用后,我们可以用下面的图来表示这些字段的状态:

parent_in----------------------------------
       ------------------------
       filedes[0]  "in_pipe"   filedes[1]
       ------------------------
chiild_in -----
parent_out---
       ------------------------
       filedes[0]    "out_pipe"   filedes[1]
       ------------------------
child_out  ----------------------
parent_err ---
       ------------------------
       filedes[0]   "err_pipe"   filedes[1]
       ------------------------
child_err ---------------------------

        还有一点指的注意的是apr_procattr_io_set调用apr_file_pipe_create创建pipe的时候,为相应的in/out字段注册了cleanup函数apr_unix_file_cleanup, apr_unix_file_cleanup在相应的in/out字段的pool销毁时被调用,在后面的apr_proc_create时还会涉及到这块。

        [PART 3]

        进程的一些常规属性。

        currdir标识新进程启动时的工作路径(执行路径),默认时为何父进程相同;

        cmdtype标识新的子进程将执行什么类型的命令;共5种类型,默认为APR_PROGRAM

        detached标识新进程是否为分离后台进程,默认为前台进程。

        [PART 4]

        这4个字段标识平台对进程资源的限制,一般我们接触不到。struct rlimit的定义在/usr/include/sys/resource.h中。

        [PART 5] 

        errfn为一函数指针,原型为typedef void (apr_child_errfn_t)(apr_pool_t *proc, apr_status_t err, const char *description);这个函数指针如果被赋值,那么当子进程遇到错误退出前将调用该函数。

            errchk一个标志值,用于告知apr_proc_create是否对子进程属性进行检查,如检查curdir的access属性等。

        [PART 6]

            用户ID和组ID,用于检索允许该用户所使用的权限。

        三、APR proc创建

            APR proc的描述结构为apr_proc_t:

typedef struct apr_proc_t {
  /** The process ID */
  pid_t pid;
  /** Parent's side of pipe to child's stdin */
  apr_file_t *in;
  /** Parent's side of pipe to child's stdout */
  apr_file_t *out;
  /** Parent's side of pipe to child's stderr*/
  apr_file_t *err;
} apr_proc_t;

            创建一个新的进程的接口为apr_proc_create,其参数也都很简单。前面说过apr_proc_create先fork出一个子进程,众所周知fork后子进程是父进程的复制品,然后子进程再通过exec函数加载新的程序映像,并开始执行新的程序。这里分析一下apr_proc_create的执行流程,其伪码如下:

apr_proc_create {
  if (attr->errchk)
     对attr做有效性检查,让错误尽量发生在parentprocess中,而不是留给child process;   ---(1)
  fork子进程;
  { /*在子进程中*/
    清理一些不必要的从父进程继承下来的描述符等,为exec提供一个"干净的"的环境 ---(2)
    关闭attr->parent_int、parent_out和parent_err,并分别重定向attr->child_in、child_out和child_err为STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO; ---(3)
    判断attr->cmdtype,选择执行exec函数; --(4)
  }
  /* 在父进程中*/
  关闭attr->child_in、child_out和child_err;
}

       下面针对上述伪码进行具体分析:

        1)有效性检查

            attr->errchk属性可以通过apr_procattr_error_check_set函数在apr_proc_create之前设置。一旦设置,apr_proc_create就会在fork子进程前对procattr的有效性进行检查,比如attr->curdir的访问属性(利用access检查)、progname文件的访问权限检查等。这些的目的就是一个"让错误发生在fork前,不要等到在子进程中出错"。

        2) 清理"不必要的"继承物

                   由于子进程复制了父进程的地址空间,随之而来的还包含一些"不必要"的"垃圾"。为了给exec提供一个"干净的"环境,在exec之前首先要做一下必要的清理,APR使用apr_pool_cleanup_for_exec来完成这项任务。apr_pool_cleanup_for_exec做了哪些工作呢?apr_pool_cleanup_for_exec通过pool内部的global_pool搜索其子节点,并逐一递归cleanup,这里的cleanup并不释放任何内存,也不flushI/O Buffer,仅是调用节点注册的相关cleanup函数,这里我们可以回顾一下apr_procattr_io_set调用,在创建相关pipe时就为相应的in/out/err描述符注册了cleanup函数。同样就是因为这点,子进程再调用apr_pool_cleanup_for_exec之前,首先要kill掉(这里理解就是去掉相关文件描述符上的cleanup注册函数)这些注册函数。防止相关pipe的描述符被意外关闭。

        3) 建立起与父进程"对话通道"

           父进程在创建procattr时就建立了若干个pipe,fork后子进程继承了这些。为了关掉一些不必要的描述符和更好的和父进程通讯,子进程作了一些重定向的工作,这里用图来表示重定向前后的差别:(图中显示的是子进程关闭parent_in/out/err三个描述符后的文件描述符表)

            重定向前:

               子进程文件描述符

                   ----------------------------|

                   [0] STDIN_FILENO|

                   -----------------------------|

                   [1] STDOUT_FILENO

                   -----------------------------|

                   [2] STDERR_FILENO

                   ------------------------------|

                   [3] child_in.fd |----> in_pipe的filedes[0]

                   -------------------------|

                   [4] child_out.fd|--->out_pipe的filedes[1]

                   -------------------------|

                   [5] child_err.fd|--->err_pipe的filedes[1]

                   ----------------------|

                   重定向后:

                   -----------|

                   [0] child_in.fd |--->in_pipe的filedes[0]

                   -----------|

                   [1]child_out.fd |--->out_pipe的filedes[1]

                   -----------|

                   [2]child_err.fd |---->err_pipe的filedes[1]

                    -----------|

                   为了能更好的体现出"对话通道"的概念,这里再画出父进程再关闭attr->child_in、child_out和child_err后的文件描述表:

                     父进程文件描述表

                     -----------------|

                     [0] STDIN_FILENO |

                     -----------------|

                     [1] STDOUT_FILENO |

                     ------------------|

                     [2] STDERR_FILENO |

                      ------------------|

                      [3]parent_in.fd | ---->in_pipe的filedes[1]

                      -------------------|

                      [4] parent_out.fd |---->out_pipe的filedes[0]

                      -------------------|

                      [5] parent_err.fd |---->err_pipe的filedes[0]

                      -------------------|

        4) 启动新的程序

            根据APR proc的设计,子进程在被fork出来后,将根据procattr的cmdtype等属性信息决定调用哪种exec函数。当子进程调用一种exec函数时,子进程将完全由新程序代换,而新程序则从其main函数开始执行(与fork不同,fork返回后子进程从fork点开始往下执行)。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

        四、总结

           xx_in/xx_out都是相对于child process来说的,xx_in表示通过该描述符child process从in_pipe读出parent process写入in_pipe的数据;xx_out表示通过该描述符child process将数据写入out_pipe供parent process使用;xx_err则是child process将错误信息写入err_pipe供parent process使用。

           fork后子进程和父进程的同和异

              同:

               --父进程已经打开的文件描述符;

               --实际用户ID、实际组ID、有效用户ID、有效组ID;

               --添加组ID;

               --进程组ID;

               --对话期ID;

               --控制终端;

               --设置用户ID标志和设置组ID标志;

               --当前工作目录;

               --根目录;

               --文件方式创建屏蔽字;

               --信号屏蔽和排列;

               --对任一打开文件描述符的在执行时关闭标志;

               --环境;

                --连接的共享存储段;

                --资源限制.

               异:

                 --fork的返回值;

                 --进程ID;

                  --不同的父进程ID;

                 --子进程的tms_utime,tms_stime,tms_cutime以及tme_ustime设置为0;

                 --父进程设置的锁,子进程不继承;

                  --子进程的未决告警被清除;

                   --子进程的未决信号集设置为空集

  5.3 APR分析-内存篇

        内存管理一直是让C程序员头痛的问题,作为一个通用接口集,APR当然也提供其自己的内存管理接口--APR Pool。APR Pool作为整个APR的一个基础功能接口,直接影响着APR的设计风格。

        APR Pool源代码的位置在$(APR_HOME)/memory目录下,本篇blog着重分析unix子目录下的apr_pools.c文件内容,其相应头文件为$(APR_HOME)/include/apr_pools.h;在apr_pools.c中还实现了负责APR内部内存分配的APRallocator的相关操作接口(APR allocator相关头文件为$(APR_HOME)/include/apr_allocator.h)。

        一、APR Pool概述

             我们平时常用的内存管理方式都是基于"request-style"的,即分配所请求大小的内存,使用之,销毁之。而APR Pool的设计初衷是为Complex Application提供良好的内存管理接口,其使用方式与"request-style"有所不同。而$(APR_HOME)/docs/pool-design.htm文档中,设计者道出了"使用好"APR Pool的几个Rules,同时也从侧面反映出APRPool的设计。

             1.任何Object都不应该有自己的Pool,它应在其构造函数的调用者的Pool中分配。因为一般调用者知道该Object的声明周期,并通过Pool管理之。也就说Object无须自己调用"Close" or "Free",这些操作在Object所在Pool被摧毁时被隐式调用的。

              2.函数无须为了他们的行为而去Create/Destroy Pool,它们应该使用它们调用者传给它们的Pool。

              3.为了防止内存无限制的增长,APR Pool建议当遇到unbounded iteration时使用sub_pool,标准格式如下:

subpool=apr_pool_create(pool,NULL);
for(i=0;i<n;++i) {
  apr_pool_clear(subpool);
  ... ...
  do_operation(..., subpool);
}
apr_pool_destroy(subpool);

           二、深入APR Pool

               1.分析apr_pool_initialize

                  任何使用APR的应用程序一般都会调用apr_app_initalize来初始化APR的内部使用的数据结构,查看一下app_app_initialize的代码,你会发现apr_pool_initialize在被apr_app_initialize调用的apr_initialize中被调用,该函数用来初始化使用Pool所需的内部结构(用户无须直接调用apr_pool_initialize,在apr_app_initialize时它被自动调用,而apr_app_initailize又是APR  program调用的第一个function,其在apr_general.h中声明,在misc/unix/start.c中实现)。

                 apr_pool_initialize {

                      如果(!apr_pools_initialized) {

                             创建global_allocator;   ------(1)

                       }


                        创建global_pool; ------(2)

                        给global_pool起名为"apr_global_pool";

                  }

                      (1) Pool和Allocator

                           每个Pool都有一个allocator相伴,这个allocator可能是Pool自己的,也可能是其ParentPool的。allocator的结构如下:

/* in apr_pools.c*/
struct apr_allocator_t {
  apr_uint32_t max_index;
  apr_uint32_t max_free_index;
  apr_uint32_t current_free_index;
  ... ...
  apr_pool_t *owner;
  apr_memnode_t *free[MAX_INDEX];
};
         在(1)调用后,global_allocator的所有xx_index字段都为0,owner-->NULL,free指针数组中的指针也都-->NULL。这里的index是大小的级别,这里最大级别为20(即MAX_INDEX=20),free指针数组中free[0]所指的node大小为MIN_ALLOC大小,即8192,即2的13次幂。按此类推free[19]所指的node大小应为2的32次幂,即4G byte。allocator_alloc中是通过index=(size >> BOUNDARY_INDEX) - 1来得到这一index的。allocator维护了一个index不同的memnode池,每一index级别上又有一个

memnode list,以后用户调用apr_palloc分配size大小内存时,allocator_alloc函数就会在free memnode池中选和要寻找的size的index级别相同的memnode,而不是重新malloc一个size大小的memnode。另外要说明一点的是APR Pool中所有ADT中的xx_index字段都是大小级别的概念。

           (2) 创建global_pool

              在APR Pool初始化的时候,唯一创建一个Pool --global_pool。apr_pool_t的非Debug版本如下:

/* in apr_pools.c */
struct apr_pool_t {
  apr_pool_t    *parent;
  apr_pool_t    *child;
  apr_pool_t    *sibling;
  apr_pool_t    **ref;
  cleanup_t     *cleanups;
  cleanup_t     *free_cleanups;
  apr_allocator_t *allocator;
  struct process_chain *subprocesses;
  apr_abortfunc_t  abort_fn;
  apr_hash_t *user_data;
  const char *tag;
  apr_memnode_t  *active;
  apr_memnode_t  *self;  /*The nodecontaining the pool itself */
  char *self_first_avail;
  ... ...
}

              而apr_memnode_t的结构如下:

/* in apr_allocator.h */
struct apr_memnode_t {
  apr_memnode_t *next;   /*next memnode */
  apr_memnode_t **ref;   /*reference to self*/
  apr_uint32_t index;   /*size*/
  apr_uint32_t free_index; /*how much free*/
  char *first_avail;   /*pointer to first free memory*/
  char *endp;  /*pointer to end of free memory */
};

              apr_pool_create_ex首先通过allocator寻找合适的node用于创建Pool,但由于global_allocator尚未分配过任何node,所以global_allocator创建一个新的node,该node大小为MIN_ALLOC(即8192),该node的当前状态如下:

node-->|----------|0
       |          |
       |          |
       |----------|APR_MEMNODE_T_SIZE <----node->first_avail
       |          |
       |          |
       |          |
       |----------size(一般为8192)<---node->endp

             其他属性值如下:

             node->next = NULL;

             node->index = (APR_UINT32_TRUNC_CAST)index; /*这里为1*/

             创建完node后,我们将在该node上的avail space划分出我们的global_pool来。划分后状态如下(pool与node关系):

node-->|----------|0 <---pool->self=pool_active
       |           |
       |           |
       |-------------|APR_MEMNODE_T_SIZE <------global_pool
       |           |
       |           |
       |--------------|APR_MEMNODE_T_SIZE+SIZEOF_POOL_T <------node->first_avail=pool->self_first_avail
       |            |
       |            |
       |------------size(一般为8192) <------------node->endp

             pool其他一些属性值(pool与pool之间关系)如下:

pool->allocator=global_allocator;
pool->child = NULL;
pool->sibling = NULL;
pool->ref = NULL;

                 2.APR Sub_Pool创建(pool与pool之间关系)

                  上面我们已经初始化了global_pool,但是global_pool是不能直接拿来就用的,我们需要创建其sub_pool,也就是用户自己的pool。一般创建user的sub_pool我们都使用apr_pool_create宏,它只需要2个参数,并默认sub_pool继承parent_pool的allocator和abort_fn。在apr_pool_create内部调用的还是apr_pool_create_ex函数。我们来看一下创建sub_pool后pool之间的关系:

                  例:

                   static apr_pool_t *sub_pool = NULL;

                   apr_pool_create(&sub_pool, NULL);

                   这里sub_pool的创建过程与global_pool相似,也是先创建其承载体node,然后设置相关属性,使其成为global_pool的child_pool。创建完后global_pool和该sub_pool的关系如下图:

global_pool <------/  ----->sub_pool
----------         //         --------
sibling --->NULL   /--------parent
----------         /          --------
child--------------/        sibling--->NULL
--------                    ---------
                            child--->NULL
                             ---------

                 APR Pool是按照二叉树结构组织的,并采用"child-sibling"的链式存储方式,global_pool作为整个树的Root Node。

                 3.从pool中分配内存

                    上面我们已经拥有了一个sub_pool,我们现在就可以从sub_pool中分配内存了。APR提供了函数apr_palloc来做这件事情。

                      例如:apr_alloc(sub_pool,wanted_mem_size);

                       apr_palloc在真正分配内存前会把wanted_mem_size做一下处理。它使用APR_ALIGN_DEFAULT宏处理wanted_mem_size得到一个圆整到8的new_size,然后再在pool中分配new_size大小的内存,也就是说pool中存在的用户内存块的大小都是8的倍数。举个例子,如果wanted_mem_size=30,apr_alloc实际会在pool中划分出32个字节的空间。

                              apr_palloc的工作流程简单描述如下:

                             a) 如果在pool->active node的avail space足够满足要申请的内存大小size时,则直接返回active->first_avail,并调整active->first_avail= active->first_avail+size;

                                     b)如果a)不满足,则查看active->next这个node满足与否;如果满足则将返回所要内存,并将该node设为active node,将以前的active node放在新active node的next位置上;

                                      c)如果b)也不满足,则新创建一个memnode,这个node可能为新创建的,也可能是从allocator的free memnode池中取出的,取决于当时整个Pool的状态。

                                  从上面我们也可以看出node分为2类,一种是作为pool的承载体,但pool结构的空间不足以完全占满一个node,所以也可以用来分配用户内存;另一种就是完全用于分配用户内存的了。每个pool有一个node list,当然这个list中包括它自己所在的node了。

                  4.apr_pool_clear和apr_pool_destroy

                    创建和分配结束后,我们需要clear或者destroy掉Pool。

                    clear和destroy的区别在于clear并不真正free内存,只是清理便于以后alloc时重用,而destroy则是真正的free掉内存了。

  5.4 APR分析-信号篇

         信号是Unix的重要系统机制。

         一、信号介绍

               1.Signal的引入用来进行User Mode进程间的交互,系统内核也可以利用它通知User Mode进程发生了哪些系统事件。从最开始引入到现在,信号只是做了很小的一些改动(不可靠信号模型到可靠信号模型).

                 2.信号服务于两个目的:

                   1) 通知某进程某特定事件发生了;

                   2) 强制其通知进程执行相应的信号处理程序。

          二、基础概念

                  1.信号的一个特性就是可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行态,则该信号被内核Save起来,知道该进程恢复执行才传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消它才被传递给进程。

                   2.系统内核严格区分信号传送的两个阶段:

                     1)Signal Generation: 系统内核更新目标进程描述结构来表示一个信号已经发送出去。

                     2)Signal Delivery:内核强制目标进程对信号做出反应,或执行相关信号处理函数,或改变进程执行状态。

                     信号的诞生和传输我们可以这样理解:把信号作为"消费品",其Generation状态就是"消费品诞生",其Delivery状态就是理解为"被消费了"。这样势必存在这样的一个情况:"消费品诞生了,但是还没有被消费掉",在信号模型中,这样的状态被称为"pending"(悬而未决)。

                      任何时候一个进程只能有一个这样的某类型的pending信号,同一进程的其他同类型的pending信号将不排队,将被简单的discard(丢弃)掉。

                   3.如何消费一个signal

                      1) 忽略该信号;

                      2)响应该信号,执行一特定的信号处理函数;

                      3)响应该信号,执行系统默认的处理函数。包括:Terminate、Dump、Ignore、Stop、Continue等。

                      这里有特殊:SIGKILL和SIGSTOP两个信号不能忽略、不能捕捉、不能阻塞,而只是执行系统默认处理函数。

           三、APR Signal封装

                 APR Signal源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的signals.c文件内容,其相应头文件为$(APR_HOME)/include/apr_signal.h。

                   1.apr_signal函数

                       早期版本处理方式:进程每次处理信号后,随机将信号的处理动作重置为默认值。

                       后期版本处理方式:进程每次处理信号后,信号的处理动作不被重置为默认值。

                       我们举例测试一下:分别在Solaris9、Cygwin和RedHat Linux 9上。

                       例子:

                       eg 1:

void siguser1_handler(int sig);
int main(void) {
  if (signal(SIGUSR1, siguser1_handler) == SIG_ERR) {
    perror("siguser1_handler error");
    exit(1);
  }
  while(1) {
    pause();
  }
}
void siguser1_handler(int sig) {
  printf("in siguser1_handler,%d/n", sig);
}
input:
 kill -USR1 9122
 kill -USR1 9122
output(Solaris 9):
 in siguser1_handler, 16
 用户信号1(程序终止)
output:(Cygwin and RH9):
 in siguser1_handler, 30
 in siguser1_handler, 30
 ...
 ..
 

                          eg.1结果表示在Solaris 9上,信号的处理仍然按照早期版本的方式,而Cygwin和RH9则都按照后期版本的方式。

                             那么有什么替代signal函数的办法么?在最新的X/Open和UNIXspecifications中都推荐使用一个新的信号接口sigaction,该接口采用后期版本的信号处理方式。在《Unix高级环境编程》中就有使用sigaction实现signal的方法,而APR恰恰也是使用了该方法实现了apr_signal。其代码如下:

APR_DECLARE(apr_sigfunc_t *) apr_signal(int signo, apr_sigfunc_t *func)
{
  struct sigaction act, oact;
  act.sa_handler = func;
  sigemptyset(&act.sa_mask);   ----(1)
  act.sa_flags=0;
#ifdef SA_INTERRUPT    /* SunOS */
  act.sa_flags |= SA_INTERRUPT;
#endif
  ... ...
  if (sigaction(signo, &act, &oact) <0) 
    return SIG_ERR;
  return oact.sa_handler;
}

                        (1)这里有一个Signal Set(信号集)的概念,通过相关函数操作信号集以改变内核传递信号给进程时的行为。Unix用sigset_t结构来表示信号集。信号集总是和sigprocmask或sigaction一起使用。

                     2、apr_signal_block和apr_signal_unblock

                          这两个函数分别负责阻塞和取消阻塞内核传递某信号给目标进程。其主要利用的就是sigprocmask函数来实现的。每个进程都有其对应的信号屏蔽字,它让目标进程能够通知内核"哪些传给我的信号该阻塞,哪些畅通无阻"。

                           这里想举例说明的是:如果多次调用SET_BLOCK的sigprocmask设置屏蔽字,结果是什么呢?

                         eg.3

int main(void)
{
  sigset_t newmask, oldmask, pendmask;
  /* 设置进程信号屏蔽字,阻塞SIGQUIT */
  sigemptyset(&newmask);
  sigaddset(&newmask, SIGQUIT);
  if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
    perror("SIG_BLOCK error");
  }
  printf("1st towait 30 seconds/n");
  sleep(30);
  /*第一次查看当前的处于pend状态的信号*/
  if(sigpending(&pendmask) <0) {
    perror("sigpending error");
  }
  if (sigismember(&pendmask, SIGQUIT)) {
    printf("SIGQUIT pending /n");
  } else {
    printf("SIGQUIT unpending/n");
  }
  if (sigismember(&pendmask, SIGUSR1)) {
    printf("SIGUSR1 pending/n");
  } else {
    printf("SIGUSR1 unpending/n");
  }
  /*重新设置屏蔽字,阻塞SIGUSR1*/
  sigemptyset(&newmask);
  sigaddset(&newmask, SIGUSR1);
  if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
    perror("SIG_BLOCK error");
  }
  printf("2nd to wait 30 seconds/n");
  sleep(30);
  /*再次查看当前的处于pend状态的信号*/
  if (sigpending(&pendmask) < 0) {
    perror("sigpending error");
  }
  if (sigismember(&pendmask, SIGQUIT)) {
    printf("SIGQUIT pending/n");
  } else {
    printf("SIGQUIT unpending /n");
  }
  if (sigismember(&pendmask, SIGUSR1)) {
    printf("SIGUSR1 pending/n");
  } else {
    printf("SIGUSR1 unpending/n");
  }
  exit(0);
}

//output
1st to wait 30 seconds
^/
SIGQUIT pending
SIGUSR1 unpending
2nd to wait 30 seconds --这之后发送kill -USR128821
SIGQUIT pending
SIGUSR1 pending

                       第一次输出SIGUSR1 unpending是因为并未发送USR1信号,所以自然为unpending状态;我想说的是第二次重新sigprocmask时我们仅加入了SIGUSR1,并未显示假如SIGQUIT,之后查看pending信号中SIGQUIT仍然为pending状态,这说明两次SET_BLOCK的sigprocmask调用是"或"的关系,第二次SET_BLOCK的sigprocmask调用不会将第一次SET_BLOCK的sigprocmask调用设置的阻塞信号变成非阻塞的。

  5.5 APR分析-文件IO篇

         文件I/O在Unix下占据着非常重要的地位。APR就是本着这个思想对Unix文件I/O进行了再一次的抽象封装,以提供更为强大和友善的文件I/O接口。

          APR File I/O源代码的位置在$(APR_HOME)/file_io目录下,本篇blog着重分析unix子目录下的相关.c文件内容,其相应头文件为$(APR_HOME)/include/apr_file_io.h和apr_file_info.h.

           一、APR File I/O介绍

              APR用了"不小的篇幅"来"描述"文件I/O,在$(APR_HOME)/file_io/unix目录下,你会看到多个.c文件,每个.c都是一类文件I/O操作,比如:

                open.c --封装了 文件的打开、关闭、改名和删除等操作;

                readwrite.c -- 顾名思义,它里面包含了文件的读写操作;

                pipe.c -- 包含了pipe相关操作。

           二、基本APR I/O

               APR定义了apr_file_t类型来表示广义的文件。先来看一下这个核心数据结构的"模样":

/* in apr_arch_file_io.h */
struct apr_file_t {
  apr_pool_t *pool;
  int filedes;
  char *fname;
  apr_int32_t flags;
  int eof_hit;
  int is_pipe;
  apr_interval_time_t timeout;
  int buffered;
  enum {BLK_UNKNOWN, BLK_OFF, BLK_ON } blocking;
  int ungetchar; /* Last charprovided by an unget op.(-1=no char)*/
#ifndef WAITIO_USES_POLL
  /* if there is a timeout set, then this pollsetis used */
  apr_pollset_t *pollset;
#endif
  /* Stuff for buffered mode */
  char *buffer;
  int bufpos; /*Read/Write position in buffer */
  unsigned long dataRead; /* a mount of valid data read into buffer */
  int direction; /*buffer being used for 0 = read, 1 = write */
  unsigned long filePtr; /*position in file of handle */
#if APR_HAS_THREADS
  struct apr_thread_mutex_t *thlock;
#endif
};

            1.apr_file_open

               ANSI C标准库和Unix系统库函数都提供对"打开文件"这个操作语义的支持。他们提供的接口很相似,参数一般都为"文件名+打开标志位+权限标志位",apr_file_open也不能忽略习惯的巨大力量,也提供了类似的接口如下:

                APR_DECLARE(apr_status_t) apr_file_open(apr_file_t **new,

                                                                                   const char *fname, 

                                                                                   apr_int32_t flag,

                                                                                   apr_fileperms_t perm,

                                                                                   apr_pool_t *pool);

                 每个封装都有自定义的一些标志宏,这里也不例外,flag和perm参数都需要用户传入APR自定义的一些宏组合,这里介绍apr_file_open操作:

apr_file_open
{
  "打开标志位"转换;  ----(1)
  "权限标志位"转换;  ----(2)
  调用Unix原生API打开文件;
  设置apr_file_t变量相关属性值;  ----(3)
}

                  (1)由于上面说了,APR定义了自己的"文件打开标志位",所以在apr_file_open的开始需要将这些专有的"文件打开标志位"转换为Unix平台通用的"文件打开标志位";

                   (2)同(1)理,专有的"权限标志位"需要转换为Unix平台通用的"权限标志位";

                  (3) APR file I/O封装支持非阻塞I/O带超时等待以及缓冲I/O,默认情况下为阻塞的,是否缓冲可通过"文件打开标志位"设置。一旦设置为缓冲I/O,则apr_file_open会在pool中开辟大小为APR_FILE_BUFSIZE(4096)的缓冲区供使用。

              2.apr_file_read/apr_file_write

                 该两个接口的看点是其缓冲区管理(前提:在apr_file_open该文件时指定了是Buffer I/O及非阻塞I/O 带超时等待)。还有一点就是通过这两个借口的实现我们可以了解到上面提到的apr_file_t 中某些"晦涩"字段的真正含义。

                  (1) 带缓冲I/O

                    这里的缓冲是APR自己管理的,带缓冲的好处很简单,即减少直接操作文件的次数,提高I/O性能。要知道无论lseek还是read/write都是很耗时的,尽可能的减少直接I/O操作次数,会带来性能上明显改善。

                   读写切换:如果先读后写,则每次写的时候都要重新定位文件指针到上次读的结尾处;如果先写后读,则每次读前都要flush缓冲区。

                   (2) 非阻塞I/O带超时等待

                     这里分析下面一段apr_file_read的代码:

do {
  rv = read(thefile->filedes, buf, *nbytes);
}while(rv == -1 && errno == EINTR); ---(a)
#ifdef USE_WAIT_FOR_IO
  if (rv == -1 &&
    (errno == EAGAIN || errno == EWOULDBLOCK) &&
    thefile->timeout != 0) {
    apr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 1); ----(b)
    if (arv != APR_SUCCESS) {
      *nbytes = bytes_read;
      return arv;
    }
    else {
      do {
        rv = rad(thefile->filedes, buf, *nbytes);
      }while (rv == -1 && errno == EINTR);
    }
  }
#endif

                    (a) 第一个do-while块:之所以使用do-while块是为了当read操作被信号中断后重启read操作;

                 (b) 一旦文件描述符设为非阻塞,(a)则瞬间返回,一旦(a)并未读出数据,则rv = -1并且errno被设置为errno = EAGAIN,这时开始带超时的等待该文件描述符I/O就绪。这里的apr_wait_for_io_or_timeout使用了I/O的多路 复用技术Poll,在后面的APR分析中会详细理解之。apr_file_t中的timeout字段就是用来做超时等待的。

             3.apr_file_close

                该接口主要完成的工作为刷新缓冲区、关闭文件描述符、删除文件(如果设置了APR_DELONCLOSE标志位)和清理Pool中内存的工作。

5.6 APR分析-高级IO篇

     一、记录锁或(区域锁)

            我见过的对记录锁讲解最详细的书就是《Unix高级环境编程》,特别是关于进程、文件描述符和记录锁三者之间的关系的讲解更是让人受益匪浅。

            关于记录锁的自动继承和释放有三条规则:

            (1) 锁与进程、文件两方面有关。这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些所都是该进程设置的)。

             (2) 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁来说,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。

             (3) 在执行exec后,新程序可以继承原执行程序的锁。

                  APR记录锁源码位置在$(APR_HOME)/file_io/unix目录下flock.c,头文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock仅提供对整个文件的枷锁和解锁,而并不支持对文件中任意范围数据的加锁与解锁。至于该锁是建议锁(advisory lock)还是强制锁(mandatory lock),需要看具体的平台实现了。两个函数均利用fcntl实现记录锁功能。代码中有一处值得借鉴:

while((rc = fcntl(thefile->filedes, fc, &l)) < 0 && errno == EINTR)
  continue;

                       这么做的原因就是考虑到fcntl的调用可能被某信号中断,一旦中断我们要去重启fcntl函数。

         二、I/O多路复用

             在经典的《Unix网络编程第1卷》 Chapter 6中作者详细介绍了五种I/O模型,分别为:

             - blocking I/O

             - nonblocking I/O

             - I/O multiplexing (select and poll)

             - signal driven I/O(SIGIO)

             - asynchronous I/O (the POSIX aio_functions)

             这里所说的I/O多路复用就是第三种模型,它既解决了Blocking I/O数据处理不及时,又解决了Non-Blocking I/O采用轮询的CPU浪费问题,同时它与异步I/O不同的是它得到了各大平台的广泛支持。

              APR I/O多路复用源码主要在$(APR_HOME)/poll/unix目录下的poll.c和select.c中,头文件为apr_poll.h。APR提供统一的apr_poll接口,但是apr_pollset_t结构定义和apr_poll的实现则根据宏POLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定义与否而不同。

               在poll的实现下,apr_pollset_t的定义如下:

/* in poll.c */
struct apr_pollset_t {
  apr_pool_t *pool;
  apr_uint32_t nelts;
  apr_uint32_t nalloc;
  struct pollfd *pollset;
  apr_pollfd_t *query_set;
  apr_pollfd_t *result_set;
};

                统一的apr_pollfd_t定义如下:

/* in apr_poll.h */
struct apr_pollfd_t {
  apr_pool_t *p; /*associated pool*/
  apr_datatype_e desc_type; /*descriptor type*/
  apr_int16_treqevents; /*requested events*/
  apr_int16_trtnevents; /*returned events*/
  apr_descriptordesc; /* @see apr_descriptor*/
  void *client_data; /*allowsapp to associate context */
};

  5.7 APR分析-共享内存篇

             共享内存是一种重要的IPC方式。在项目中多次用到共享内存,只是用而并未深入研究。

         APR共享内存封装的源代码的位置在$(APR_HOME)shmem目录下,本篇blog着重分析unix子目录下的shm.c文件内容,其相应头文件为$(APR_HOME)/include/apr_shm.h.

         一、共享内存简单小结

            共享内存时最快的IPC方式,因为一旦这样的共享内存段映射到各个进程的地址空间,这些进程间通过共享内存的数据传递就不需要内核的帮忙了。Stevens的解释是"各进程不是通过执行任何进入内核的系统调用来传递数据,显然内核的责任仅仅是建立各进程地址空间与共享内存的映射,当然像处理页面故障这一类的底层活还是要做的"。相比之下,管道和消息队里交换数据时都需要内核来中转数据,速度就相对较慢。

         二、APR共享内存封装

              APR提供多种创建共享内存的方式,其中最主要的就是apr_shm_create接口,其伪码如下:      

apr_shm_create 
{
  if (要创建匿名shm) {
#if APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON
#if APR_USE_SHMEM_MMAP_ZERO
  xxxx  ---------(1) 
#elif APR_USE_SHMEM_MMAP_ANON
  xxxx   ----------(2)
#endif
#endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */
#if APr_USE_SHMEM_SHMGET_ANON
  xxxx  ----------(3)
#endif
} else { /* 创建有名shm */
#if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM
#if APR_USE_SHMEM_MMAP_TMP
 xxxx  --------(4)
#endif
#if APR_USE_SHMEM_MMAP_SHM
  xxxx   ----------(5)
#endif
#endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */
#if APR_USE_SHMEM_SHMGET
  xxxx     -----------(6)
#endif
  }
}

             其中不同版本Unix创建匿名shmem的做法如下:

             (1) SVR4通过映射"/dev/zero"设备文件来获得匿名共享内存,其代码一般为:

fd = open("/dev/zero", ..);
ptr = mmap(..., MAP_SHARED, fd, ...);

             (2) 4.4 BSD提供更加简单的方式来支持匿名共享内存(注意标志参数MAP_XX)

ptr = mmap(..., MAP_SHARED | MAP_ANON, -1, ...);

             (3) System V匿名共享内存区的做法如下:

shmid = shmget(IPC_PRIVATE, ...);
ptr = shmat(shmid, ...);

                匿名共享内存一般都用于有亲缘关系的进程间的数据通讯。由父进程创建共享内存,子进程自动继承下来。由于是匿名,没有亲缘关系的进程是不能动态链接到该共享内存区的。

              不同版本Unix创建有名shmem的做法如下:

              (4) 由于是有名的shmem,所以与匿名不同的地方在于用filename替代"/dev/zero"做映射。

fd = open(filename, ...);
apr_file_trunc(...);
ptr = mmap(..., MAP_SHARED, fd, ...);

              (5) Posix共享内存的做法

fd = shm_open(filename, ...);
apr_file_trunc(...);
ptr = mmap(..., MAP_SHARED, fd, ...);

                  值得注意的一点就是通过shm_open映射的共享内存可以供无亲缘关系的进程共享。apr_file_trunc用于重新设定共享内存对象长度。

               (6) System V有名共享内存区的做法如下:

shmkey = ftok(filename, 1);
shmid = shmget(shmkey, ...); //相当于open orshm_open
ptr = shmat(shmid, ...); //相当于mmap

               有名共享内存一般都与一个文件相关,该文件映射到共享内存段,而不同的进程(包括无亲缘关系的进程)则都映射到该文件以达到目的。在APR中通过apr_shm_attach可以动态将调用进程连接到已存在的共享内存上,前提是你必须知道该共享内存区的标识,在APr中一律用filename做标识。

        三、总结

            内核架起了多个进程间共享数据的纽带--共享内存。通过上面的叙述你会发现共享内存的创建其实并不困难,真正困难的是共享内存的管理,在正规的软件公司像内存/共享内存管理这样的重要底层功能都是封装成库形式的。

        四、参考资料

            SIGSEGV和SIGBUS

            涉及共享内存的管理就不能不提到访问共享内存对象。谈到访问共享内存对象就要留神"SIGSEGV和SIGBUS"这两个信号。

              系统分配内存页来承载内存映射区,由于内存页大小是固定的,所以存在多余的页空间空闲,比如待映射文件大小为5000 bytes,内存映射区大小也为5000bytes。而一个内存页大小4096,系统势必要分配两页来承载,这时空闲的有效空间为从5000-8191,如果进程访问这段地址空间也不会发生错误。但是要超出8191,就会收到SIGSEGV信号,导致程序停止。关于SIGBUS信号的来历,这里也举例说明:若待映射文件大小为5000 bytes,我们在mmap时指定内存映射区size = 15000 > 5000,这时内核真正的共享区承载体大小只有8192(能包容映射文件大小即可),此时在[0, 8191]内访问均没问题,但在[8192,14999]之间会得到SIGBUS信号;超出15000访问时会触发SIGSEGV信号。

  5.8 APR分析-环篇

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

          简单说说环(RING):环是一个首尾相连的双线链表,也就是我们所说的循环链表。

          1.如何使用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->next = 链表的第一个节点;

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

                但是查看APR RING的源码你会发现sentinel节点只是个虚拟存在的节点,这个虚拟节点既有数据域(虚拟出来的,不能引用)又有链接域,好似与普通节点并无差别。

                 再看看下面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_EMPTRY宏就是这么做的:

#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不足之处

                    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))

  5.9 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_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;

         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信号量是一个"计数信号量集"。   

           (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 in the Child"。APR提供接口apr_proc_mutex_child_init在子进程中reopen themutex。

  5.10 APR分析-线程篇

          并行一直是程序设计领域的难点,而线程是并行的一种重要的手段,而且现成的一些特性也能在进程并行时发挥很好的作用(“在线程同步篇”中详细阐述)。

        APR线程的源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的thread.c文件内容,其相应头文件为$(APR_HOME)/include/apr_threadproc.h。

          一、线程基础

              (1) 在传统观点中,进程是由存储于用户虚拟内存中的代码、数据和栈,以及由内核维护的"进程上下文"组成的,其中"进程上下文"又可以看成"程序上下文"和"内核上下文"组成,可参见下图所示:

进程--
  |- 进程上下文
    |-程序上下文
       |-数据寄存器
       |-条件码
       |-栈指针
       |-程序计数器
    |- 内核上下文
       |- 进程ID
       |- VM结构
       |- Open files
       |- 已设置的信号处理函数
       |- brk pointer
  |- 代码、数据和栈(在虚存中)
    |- 栈区<--SP
    |- 共享库区
    |- 运行时堆区 <--brk
    |- 可读/写数据区
    |- 只读代码/数据区  <--PC

               (2) 另一种观点中,进程是由线程、代码和数据以及内核上下文组成的。下图更能直观的展示出两种观点的异同:

进程--+
  |- 线程
    |- 栈区 <--SP
    |- 线程上下文
    |- 线程ID
    |- 数据寄存器
    |- 条件码
    |- 栈指针
    |- 程序计数器
  |- 内核上下文
    |- 进程ID
    |- VM结构
     |- Open files
    |-已设置的信号处理函数
     |- brk pointer
  |- 代码、数据(在虚存中)
     |- 共享库区
     |- 运行时堆区  <-- brk
     |- 可读/写数据区
     |- 只读代码/数据区 <--PC

               对比两种观点我们可以得出以下几点结论:

             (a) 从观点(2)可以看出进程内的多个线程共享进程的内核上下文和代码、数据(当然不包括栈区);

             (b) 线程上下文比进程上下文小,且切换代价小;

             (c) 线程不像进程那样有着"父-子"体系,同一个进程内的线程都是"对等的",主线程与其他线程不同之处就在于其是进程创建的第一个线程。

         二、APR线程管理接口

             如今应用最广泛的线程包就是PosixThread了。APR对线程的封装也是基于Posix thread的。

             APR线程管理接口针对apr_thread_t 这个基本的数据结构进行操作,apr_thread_t的定义很简单:

/* apr_arch_threadproc.h */
struct apr_thread_t {
  apr_pool_t *pool;
  pthread_t *td;
  void *data;
  apr_thread_start_t func;
  apr_status_t exitval;
};

             这个结构中包含了线程ID、线程函数以及该函数的参数数据。不过APR的线程函数定义与Pthread的有不同,“Pthread线程函数”是这样的:

          typedef void *(start_routine)(void *);

           而"APR线程函数"如下:

          typedef void *(APR_THREAD_FUNC *apr_thread_start_t)(apr_thread_t *, void *);

         1.apr_thread_create

            apr_thread_create 内部定义了一个dummy_worker的"Pthread线程函数",并将apr_thread_t结构作为参数传入,然后在dummy_worker中启动"APR的线程函数"。在该函数的参数列表中有一项类型为apr_threadattr_t:

struct apr_threadattr_t {
  apr_pool_t *pool;
  pthread_attr_t attr;
};

          2.apr_thread_exit

              进程退出我们可以直接调用exit函数,而线程退出也有几种方式:

             (1) 隐式退出 - 可以理解为线程main routine代码结束返回;

             (2) 显式退出 - 调用线程包提供的显示退出接口,在apr中就是apr_thread_exit;

             (3) 另类显式退出 - 调用exit函数,不仅自己退出,其所在线程也跟着退出了;

             (4)被"黑"退出 - 被别的"对等"线程调用pthread_cancel而被迫退出。

             apr_thread_exit属于种类(2),该种类退出应该算是线程的优雅退出了。apr_thread_exit做了3个工作,分别为设置线程返回值、释放pool中资源和调用pthread_exit退出。

            3.apr_thread_join和apr_thread_detach

              进程有waitpid,线程有join。线程在调用apr_thread_exit后,只是其执行停止了,其占有的"资源"并不一定释放,这里的"资源"我想就是"另种观点"中的"线程上下文",线程有两种方式来释放该"资源",这主要由现成的"可分离"属性决定的。如果线程是"可分离的",当线程退出后会自动释放其"资源",如果线程为"非可分离的",则必须由"对等线程"调用join接口来释放其资源。apr_thread_detach用来将其调用线程转化为"可分离"线程,而apr_thread_join用来等待某个线程结束并释放其资源。

  5.11 APR分析-网络IO篇

         APR网络I/O的源代码的位置在$(APR_HOME)/network_io目录下,本篇blog着重分析unix子目录下的各.c文件内容,其相应头文件为$(APR_HOME)/include/apr_network_io.h。

       一、IP地址 -- 主机通信

           我们熟知的并且每天工作于其上的因特网是一个世界范围的主机的集合,这个主机结合被映射为一个32位(目前)或者64位(将来)IP地址;而IP地址又被映射为一组因特网域名;一个网络中的主机上的进程能通过一个连接(connection)和任何其他网络中的主机上的进程通信。

          1.IP地址存储

             在如今的IPv4协议中我们一般使用一个unsigned int来存储IP地址,在UNIX平台下,使用如下结构来存储一个IP地址的值:

/* Internet address structure */
struct in_addr {
  unsigned int s_addr; /* network byte order(big-endian) */
};

             这里值得一提的是APR关于IP地址存储的做法,看如下代码:

#if (!APR_HAVE_IN_ADDR) 
/*We need to make sure we always have an in_addr type, so APR will just define it ourselves, if the platform doesn't provide it.
*/
struct in_addr {
  apr_uint32_t  s_addr;
};
#endif 

              APR保证了其所在平台上in_addr的存在。在in_addr中,s_addr是以网络字节序存储的。如果你的IP地址不符合条件,可通过调用一些辅助接口来做转换,这些接口包括:

htonl: host to network long;
htons: host to network short;
ntohl: network to host long;
ntohs: network to host short.

              2.IP地址表示

               我们平时看到的IP地址都是类似"xxx.xxx.xxx.xxx"这样的点分十进制的。上面说过IP地址使用的是一个unsigned int整形数来表示。这样就存在着一个IP地址表示和IP地址存储之间的一个转换过程。APR提供这一转换支持,我们用一个例子来说明:

#include <apr.h>
#include <apr_general.h>
#include "apr_network_io.h"
#include "apr_arch_networkio.h"
int main(int argc, const char *const *argv, const char *const *env) {
  apr_app_initialize(&argc, &argv, &env);
  char presentation[100];
  int networkfmt;
  memset(presentation, 0, sizeof(presentation));
  apr_inet_pton(AF_INET, "255.255.255.255", &networkfmt);
  printf("0x%x/n", networkfmt);
  apr_inet_ntop(AF_INET, &networkfmt, presentation, sizeof(presentation));
  printf("presentation is %s/n", presentation);
  apr_terminate();
  return 0;
}

               APR提供apr_inet_pton将我们熟悉的点分十进制形式转换成一个整形数存储的IP地址;而apr_inet_ntop则将一个存整形数存储的IP地址转换为我们可读的点分十进制形式。这两个接口的功能类似于系统调用inet_pton和inet_ntop,至于使用哪个就看你的喜好了。

       二、SOCKET --进程通信

           1.SOCKET描述符

              获取SOCKET描述符:

int socket(int domain, int type, int protocol);

              从Unix程序的角度来看,SOCKET就是一个有相应描述符的打开的文件。在APR中我们可以通过调用apr_socket_create来创建一个APR自定义的SOCKET对象,该SOCKET结构如下:

/* apr_arch_networkio.h*/
struct apr_socket_t {
  apr_pool_t *cntxt;
  int socketdes;
  int type;
  int protocol;
  apr_sockaddr_t *local_addr;
  apr_sockaddr_t *remote_addr;
  apr_interval_time_t timeout;
#ifdef HAVE_POLL
  int connected;
#endif
  int local_port_unknown;
  int local_interface_unknown;
  int remote_addr_unknown;
  apr_int32_t options;
  apr_int32_t inherit;
  sock_userdata_t *userdata;
#ifndef WAITIO_USES_POLL
  /* if there is a timeout set, then this pollset is used */
  apr_pollset_t *pollset;
#endif
};

                       该结构中的socketdes字段其实是真实存储由socket函数返回的SOCKET描述符的,其他字段都是为APR自己所使用的,这些字段在Bind、Connect等过程中使用。我们如果不显示将SOCKET描述符绑定到某SOCKET地址上,系统内核就会自动为该SOCKET描述符分配一个SOCKET地址。

            2.SOCKET属性

                还是与文件对比,在文件系统调用中有一个fcntl接口可以从来获取或设置已分配的文件描述符的属性,如是否Block、是否Buffer等。SOCKET也提供类似的接口调用setsockopt和getsockopt。在APR中等价于该功能的接口时apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的参数属性:

#define APR_SO_LINGER  1 /*Linger*/
#define APR_SO_KEEPALIVE 2 /*Keepalive*/
#define APR_SO_DEBUG    4 /*Debug*/
#define APR_SO_NONBLOCK 8 /*Non-blocking IO*/
#define APR_SO_REUSEADDR 16 /*Reuse addresses*/
#define APR_SO_SNDBUF 64  /*Send buffer*/
#define APR_SO_DISCONNECTED 256 /*Disconnected*/
......

           另外从上面这些属性值(都是2的n次方)可以看出SOCKET也是使用一个属性控制字段中的"位"来控制SOCKET属性的。

           再有APR提供一个宏apr_is_option_set来判断一个SOCKET是否拥有某个属性。

            3.Connect、Bind、Listen、Accept-- 建立连接

              (1) apr_socket_connect

               客户端连接服务器端的唯一调用就是connect,connect试图建立一个客户端进程与服务器端进程的连接。apr_socket_connect的参数分别为客户端已经打开的一个SOCKET以及指定的服务器端的SOCKET地址(IP ADDR:PORT)。apr_socket_connect内部实现的流程大致如下:

apr_socket_connect {
  do {
    rc = connect(sock->socketdes,
                 (const struct sockaddr *)&sa->sa.sin,
                 sa->salen);
   } while (rc == -1 && errno == EINTR);  ---(a)
   if ((rc == -1) && (errno == EINPROGRESS || errno == EALREADY)
          && (sock->timeout > 0)) {
       rc = apr_wait_for_io_or_timeout(NULL, sock, 0); ----(b)
       if (rc != APR_SUCCESS) {
         return rc;
       }
       if (rc == -1 && errno != EISCONN) {
         return errno;  ----(c)
       }
       初始化sock->remote_addr;
     ...  ...
}

                  对上述代码进行若干说明:

              (a) 执行系统调用connect连接服务器端,注意这里做了防止信号中断的处理。

              (b) 如果系统操作正在进行中,调用apr_wait_for_io_or_timeout进行超时等待;

              (c) 错误返回,前提errno不是表示已连接上。

              一旦apr_socket_connect成功返回,我们就已经成功建立一个SOCKET对,即一个连接。

         (2) apr_socket_bind

              Bind、Listen和Accept这三个过程是服务器端用于接收"连接"的必经之路。其中Bind就是告诉操作系统内核显示地位该SOCKET描述符分配一个SOCKET地址,这个SOCKET地址就不能被其他SOCKET描述符占用了。

         (3)apr_socket_listen

             SOCKET描述符在初始分配时都处于"主动连接"状态,Listen过程将该SOCKET描述符从"主动连接"转换为"被动状态",并告诉内核接受该SOCKET描述符的连接请求。apr_socket_listen的背后直接就是listen接口调用。

          (4) apr_socket_accept

              Accept过程在"被动状态"SOCKET描述符上接受一个客户端的连接,这时系统内核会自动分配一个新的SOCKET描述符,内核为该描述符自动分配一个SOCKET地址,来代表这条连接的服务器端。注意在SOCKET编程接口中除了socket函数能分配新的SOCKET描述符之外,accept也是另外的一个也是唯一的一个能分配新的SOCKET描述符的系统调用了。apr_socket_accept首先在pool中分配一个新的apr_socket_t结构变量,然后调用accept,并设置新变量的各个字段。

            4.Send/Recv --数据传输

              网络通信最重要的还是数据传输,在SOCKET编程接口中最常见的两个接口就是recv和send。在APR中分别有apr_socket_recv和apr_socket_send与前面二者对应。下面逐一分析。

               (1) apr_socket_recv

                 首先来看看apr_socket_recv的实现过程:

                 

apr_socket_recv {
                      if (上次调用apr_socket_recv没有读完所要求的的字节数) {   ----------(a)
                         设置sock->options;
                         goto do_select;
                      }
                      do {
                           rv = read(sock->socketdes, buf, (*len)); ------(b)
                      } while (rv == -1 && errno == EINTR);
                      if ((rv == -1) && (errno == EAGAIN || errno == EWOULDBLOCK) && (sock->timeout > 0)) {
do_select:
         arv = apr_wait_for_io_or_timeout(NULL, sock, 1);
         if (arv != APR_SUCCESS) {
             *len = 0;
             return arv;
         }else {
              do {
                 rv = rad(sock-》socketdes,buf, (*len));
              } while(rv == -1 && errno == EINTR);
           }
      } ---------(c)
     设置(*len)和sock->options; -----(d)
     ... ...
}

                       针对上面代码进行说明:

                       (a) 一次apr_socket_recv调用完全有可能没有读完所要求的字节数,这里做个判断以决定是否继续读完剩下的数据;

                        (b) 调用read读取SOCKET缓冲区数据,注意这里做了防止信号中断的处理。

                        (c) 如果SOCKET操作正忙,我们调用apr_wait_for_io_or_timeout等待,直到SOCKET可用。

                        (d) 将(*len)设置为实际从SOCKET Buffer中读取的字节数,并根据这一实际数据与要求数据作比较来设置sock->options.

              (2) apr_socket_send

                    apr_socket_send负责发送数据到SOCKET Buffer,其实现的方式与apr_socket_recv大同小异。

                 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值