LibApr 阅读笔记

基础环境搭建.. 2016/02/23

apr

APR 是什么

   apr 运行时库目标是: 提供一个可移植的运行时支持库,是Apache HTTP服务器的支持库,提供了一组映射到下层操作系统的API。
   可以屏蔽平台相关性,使程序员可以叫方便的写出在不同平台上移植的程序。	  

功能特性

1. 内存管理和内存池功能
2. 原子操作(Linearizability)
3. 动态库处理
4. 文件I/O
5. 命令参数解析
6. 锁机制(Locking)
7. 散列表和数组
8. Mmap(mmap)功能
9. 网络套接字和协议
10.  线程,进程和互斥锁功能
11.  共享内存功能
12. 时间子程序
13.  用户和组ID服务

应用背景

 由于公司的设备品种繁多,staragent 作为运维工具的基础设施,广泛部署在公司的各个服务器上,考虑代码的维护性,和可移植性,我们考虑采用 apr 运行库来实现staragent的客户端的核心功能。其中主要涉及APR 库的 1, 5, 6, 9 10,11 等功能。本次我们重点了解如何搭建 apr 库的基本环境,编译构建;项目集成。以及其网络库存,进程间处理库。

apr 运行环境搭建

 为方便工程测试,我们将依赖的源码包编译为静态连接库,在项目中设置依赖变量,可以保证只用编译
一次apr库;
 
1. 下载源码包: 

    https://apr.apache.org/	 

2.  执行构建

    为了方便测试,我创建了一个编译目录,
    如下 是创建的目录结构:
    
    ├── build_dep.sh
    ├── build.sh
    ├── CMakeLists.txt
    ├── depend
    │   └── compiled
    ├── log
    └── src
        ├── apr-1.5.2
        ├── common
        └── proc
    
    
    下载的源码,解压后,放在src/apr-1.5.2目录下;
    在当前路径下执行,buil_dep.sh 脚本,会将apr库编译安装在 depend/compiled/ubuntu_64/目录下。
   
    以下时编译安装脚本。
        
    #!/bin/sh
    set -x
    curDir=`pwd`
    mkdir -p depend/compiled/ubuntu_64/
    echo "build apr-1.5.2"
    cd src/apr-1.5.2/
    ./configure --prefix=${curDir}/depend/compiled/ubuntu_64
    make && make install
    
    

3. 设置项目依赖路径

   编译安装完毕后,可以设置路径依赖,我们采用cmake 构建项目工程;
   需要在构建是指定依赖路径:
   -DDEP_ROOT=/XXX/depend/compiled/ubuntu_64

4. 添加引用

    需要在项目中设置apr 引用和依赖
    
    include_directories(${DEP_ROOT}/include/apr-1)
    link_direcoteries(${DEP_ROOT}/lib)
    target_link_libraries(xxx apr-1)

5. 执行构建
   mkdir -p build && cd build && cmake .. && make

apr 库使用的基本结构及编程风格

apr 并不是一个框架库,优点是 可以很方便的和其它库联合使用

apr库 编程的代码基本结构(skeleton code)

 一个基本的apr库骨架代码如下: 会调用apr_initialize() 初始化, 调研apr_terminate() 终结。
/*   pseudo code of libapr. error checks omitted  */  
apr_status_t rv;
apr_foo_t *foo;
rv = apr_foo_create(&foo, args...);/* create a @foo object by @args */
rv = apr_foo_do_something(foo, args...); /* do something with @foo */
apr_foo_destroy(foo); /* destroy the @foo object. Sometimes, this is done implicitly by 
                       * destroying related memory pool. Please see below */

apr 的编程风格

  * 命名规则很简单,清晰
  * 数据类型大部分对用户隐藏,(可通过API操作属性;未实现完全的类型)
  * 大部分的返回结果为 apr_status_t. 返回结果会作为参数适用。
  * 基于内存池

apr 内存池

大部分的libapr API 依赖于内存池。通过内存池,可以很方便的管理内存块。 如果你有很多内存快,需要申请和释放,管理起来就会比较麻烦,可能导致内存泄漏。apr 的内存池库可以解决此问题。apr  所有的内存申请,都是有内存池负责申请,调用 apr_pool_destroy() 即可释放掉所有的内存。好处有二:
  其一: 预防内存泄漏
  其二: 分配内存块的开销相对降低。
  
  内存池迫使我们基于会话编程;一个内存池,可以看成一个会话上下文,一些具有相同生命周期的对象集合。你可以使用一个会话上下文来管理一个对象集合。
  基本思想是: 在会话开始的时候,创建一个内存池; 你不需要关心他们的生命周期,最后,会话结束的时候,释放掉内存池即可。
   
 备注:

内存池操作的基本API

 /* excerpted from apr_pools.h */
 
 /* 内存创建 */
 APR_DECLARE(apr_status_t) apr_pool_create(apr_pool_t **newpool,
                                      apr_pool_t *parent);
   
  /* 内存申请 */                                      
 APR_DECLARE(void *) apr_palloc(apr_pool_t *p, apr_size_t size);
 
 
 /* 内存池销毁 */ 
 APR_DECLARE(void) apr_pool_destroy(apr_pool_t *p);
申请,释放 一块内存空间
  /* excerpted from mp-sample.c */
       apr_pool_t *mp;
      /* create a memory pool. */
      apr_pool_create(&mp, NULL);
      
      /* allocate memory chunks from the memory pool */
      char *buf1;
      buf1 = apr_palloc(mp, MEM_ALLOC_SIZE);    
 
     /* destroy the memory pool. These chunks above are freed by this */
      apr_pool_destroy(mp);
几个内存池使用建议:
 1. apr_palloc() 并没有最大的内存限制,内存池主要是基与小的内存块设计的,初始化的大小是 8kbyte; 若需要使用较大的内存块,比如几M ,建议不要使用内存池。
 2. 默认,内存池管理 不会主动将申请的内存返回给操作系统,如果程序长时间运行,可能会有问题。建议限定内存池的上限。
 
     #define YOUR_POOL_MAX_FREE_SIZE 32 

     apr_pool_t *mp;
     apr_pool_create(&mp, NULL);
     apr_allocator_t* pa = apr_pool_allocator_get(mp); 
     if(pa){
             apr_allocator_max_free_set(pa, YOUR_POOL_MAX_FREE_SIZE);  
      
     }
     
 3. 以上例子 给出了如何申请和释放内存;libapr 封装了内存的申请和释放实现逻辑。
   apr_pool_create()/ apr_pool_destroy(mp); 成对出现。
   rv = apr_initialize();/ apr_terminate(); 成对出现。

内存池的清理及销毁相关API

  场景1:
  
  apr_pool_clear()
  与apr_pool_destroy()类似,但内存池依然可以使用;一个典型的场景如下:
   
  /* sample code about apr_pool_clear() */
  apr_pool_t *mp;
  apr_pool_create(&mp, NULL);
  for (i = 0; i < n; ++i) {
       do_operation(..., mp);
       apr_pool_clear(mp);
   }
   

   场景2:  我们需要在内存池 clear/destroy 时执行特定的操作,可以通过注册callback函数实现。在回调函数里,你可以实现任何收尾处理代码。
 
   apr_pool_cleanup_register()

apr内存池构建结构

  apr的内存池,是基于树结构的。每个内存池拥有一个父的内存池,
  apr_pool_create() API,第二个参数,是其父内存池的地址,传递为NLL,则当前的内存池,最为根节点。
  当调用apr_pool_destroy()时,内存池的子节点也会被销毁, 当调用apr_pool_clear() 时,内存池可用,但其子节点的内存池被销毁。当一个内存池的子节点
  销毁时,其 cleanup 函数会被调用。
使用建议
  传递 NULL 作为内存池的 cleanup 回调函数 ( BUG )
   
  应 像下面的代码 传递  apr_pool_cleanup_null 
  
  /* pseudo code about memory pool typical bug */
  
  /* apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, NULL); THIS IS A BUG */

  /* FIXED */
  apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, apr_pool_cleanup_null);

apr 进程处理库

 为了便于理解,我们基于一个功能来学习apr 进程库。
 我们的任务是: 通过apr进程库,创建一个子进程,在子进程中执行一个shell命令,然后 在父进程中,将该子进程执行的结果,在终端输出。
 
 我们需要学习的知识:
 
 如何利用apr 库 创建一个子进程;
 
 子进程执行完毕后,父进程如何获取子进程执行的结果。

apr 进程库:

进程属性对象

   /* excerpted from apr_thread_proc.h */
   
    APR_DECLARE(apr_status_t) apr_procattr_create(apr_procattr_t **new_attr, apr_pool_t *cont);
   
    第一个参数: 结果参数;
    第二个参数: 进程使用的内存池对象 
   
    apr_procattr_t 结构比较复杂,提供了get/set方法; 在 apr_thread_proc.h 中定义;

detach API

    APR_DECLARE(apr_status_t) apr_threadattr_detach_set(apr_threadattr_t *attr, apr_int32_t on);
    通过此API,可以设置子进程为分离的进程(detached) ; 不同的操作系统(detached)的含义不同;
    在windows 系统里,当子进程为命令行应用,并且不需要看控制台的时候,设置为分离模式;在 linux 操作系统里,如果子进程为一个服务进程则建议设置为分离模式。
    默认的模式是: non-detachable;
    
    typedef enum {
      APR_SHELLCMD,           /**< use the shell to invoke the program */
      APR_PROGRAM,            /**< invoke the program directly, no copied env */
      APR_PROGRAM_ENV,        /**< invoke the program, replicating our environment */
      APR_PROGRAM_PATH,       /**< find program on PATH, use our environment */
      APR_SHELLCMD_ENV        /**< use the shell to invoke the program,  replicating our environment
                               */
    } apr_cmdtype_e;
   
     apr_cmdtype_e  主要作用:
     
      (1)设置执行进程是否是使用shell; 
     
               APR_SHELLCMD 和 APR_SHELLCMD_ENV 两种模式,是使用 shell 来 创建一个进程;可与system(3)做比较。
               与system(3) 和 fork 或 exec 比较,有时被认为一种容易的交互方式;
               在 libapr 建议 不用 shell 来执行一个进程。理由是: 虽然利用 shell 来执行任务 好像是易于扩展,但实际存在安全漏洞。 除非       
               你很清楚自己在执行什么,否则不要用 APR_SHELLCMD 或 APR_SHELLCMD_ENV。
      
       (2)另一个不同 主要是,环境变量。
            传递环境变量有两种方式: 
           (a)使用 APR_PROGRAM_ENV 或 APR_SHELLCMD_ENV. 
               子进程会获得父进程环境变量的一个副本;子进程修改任务环境变量的值并不会影响父进程。
           (b)通过设置 apr_proc_create() 函数的 参数来传递。
           
        (3)最后一个不同主要是和 PATH 环境变量有关。
             APR_PROGRAM_PATH 是唯一的,只用通过使用 APR_PROGRAM_PATH 这种模式,才可以用一个 程序的名称 代替 程序的路径。
             比如: 可以使用 "ls" 启动一个子进程。libapr 会 从PATH的环境变量里搜索 其确切的路径。否则我们就传递一个绝对路径,如 "/bin/ls" ;
             作者不推荐使用 可能会存在安全隐患。

进程创建API

创建进程函数
 /* excerpted from apr_thread_proc.h */
 APR_DECLARE(apr_status_t) apr_proc_create(apr_proc_t *new_proc,
                                      const char *progname,
                                      const char * const *args,
                                      const char * const *env, 
                                      apr_procattr_t *attr, 
                                      apr_pool_t *pool);
  
   
 函数说明:
 第一个参数: 子进程返回值,内存由apr 库创建;
 第二个参数: 子进程程序的名称,建议传递绝对路径。
 第三个参数: 子进程运行的参数列表;构造子进程参数列表方法如下:
 
    /* pseudo code of args to apr_proc_create() */
       int argc = 0;
       const char* argv[32];    /* 32 is a magic number. enough size for the number of arguments list */
       argv[argc++] = progname; /* program path of the command to run */
       argv[argc++] = "-i";
       argv[argc++] = "foo";
       argv[argc++] = "--longopt";
       argv[argc++] = "bar";
       argv[argc++] = NULL;    /* The final element should be NULL as sentinel */
       
       该参数列表,第一个参数 为 程序运行的路径,最后一个参数最为哨兵,传递NULL。
 
 第四个参数: 传递给子进程环境变量列表,
             和第三个参数类似,参数列表的最后一个值为NULL,最为哨兵。
 第五个参数: apr_procattr_t; 进程属性对象,在1中介绍;通过apr_proattr_create()API创建
 第六个参数: 内存池地址;

获取子进程执行状态。

 /* excerpted from apr_thread_proc.h */
 APR_DECLARE(apr_status_t) apr_proc_wait(apr_proc_t *proc,
                                    int *exitcode, apr_exit_why_e *exitwhy,
                                    apr_wait_how_e waithow);

函数说明:
第一个参数: 之前通过apr_proc_create()创建的进程属性对象;
第二,三个参数: 结果参数;
第四个参数:等待子进程的方式:
         APR_WAIT  当前进程会阻塞,直到子进程终止;
         APR_NOWAIT 非阻塞;    

父子进程间通信的一种方式 pipe

管道时一种内部进程通信的方式。在父子进程间通信,比较方便。通过管道,父进程可以传递字符流到子进程的标准输入;同样,父进程也可以通过管道收到子进程发的字符流。子进程可以将输出结果写入标准输出或标准错误。子进程可以不 care 管道,只需要从标准 输入/输出/错误里读写; 相对的,从父进程的视角来看,管道就像一个文件对象。父进程只需要调用 apr_file_read() 或  apr_file_write() 从其管道 来发送或接收 数据  ##### 管道处理函数
/* excerpted from apr_thread_proc.h */
APR_DECLARE(apr_status_t) apr_procattr_io_set(apr_procattr_t *attr, 
                                         apr_int32_t in, apr_int32_t out,
                                         apr_int32_t err);
函数参数说明:
第一个参数:进程属性对象;
第2,3,4 参数 分别对应改进程的标准  输入/输出/错误。

后记

apr库 涉及内容比较多,是一个循序渐进的过程。我是刚入门学习,水平有限,欢迎大家交流指证。

我的计划如下: 熟悉apr库的使用方法 -> 应用实战 -> 研究其源代码 -> 自己编写类似的工具箱

有兴趣的同学欢迎加我,相互交流。

未完待续。。。。

相关链接:

[1]apr官网 [2]apr 使用教程

Post Directory