APR分析-总体篇node
1、何为APR?程序员
Apache Server通过这么多年的发展后,将一些通用的运行时接口封装起来提供给你们,这就是Apache Portable Run-time libraries, APR。shell
2、APR的目录组织apache
从www.apache.org上下载apr-1.1.1.tar.gz到本地解压后,发现APR的目录结构很清晰。编程
1) 全部的头文件都放在$(APR)/include目录中;数组
2) 全部功能接口的实现都放在各自的独立目录下,如threadproc、mmap等;缓存
3) 此外就是相关平台构建工具文件如Makefile.in等。曾经看过ACE的代码,ACE的全部源文件(.cpp)都放在一个目录下,显得很混乱。APR给个人第一印象还不错。服务器
4) 进入各功能接口子目录,以threadproc为例,在其下面的子目录有5个,分别为beos、netware、os2、unix和win32。从APR的名字也能够理解,每一个子目录下都存放着各个平台的独特实现源文件。网络
3、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。
4、应用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文件采用的是$(apr_path)/test目录下的proc_child.c文件。编译运行一切OK!
5、GO ON
分析APR的过程也是我学习Unix高级系统机制的过程,有时间我会继续APR分析的。
APR分析-设计篇
APR附带一个简短的设计文档,文字言简意赅,其中不少的设计思想都值得咱们所借鉴,主要从三个方面谈。
1、类型
1) APR提供并建议用户使用APR自定义的数据类型,好处不少,好比便于代码移植,避免数据间进行没必要要的类型转换(若是你不使用APR自定义的数据类型,你在使用某些APR提供的接口时,就须要进行一些参数的类型转换);自定义数据类型的名字更加具备自描述性,提升代码可读性。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_int64_t;
typedef unsigned long long apr_uint64_t;
这些都是在apr.h中定义的,而apr.h在UNIX平台是经过configure程序生成的,在不一样平台APR自定义类型的实际类型是彻底有可能不一致的。
2) 还有一点值得提的是在APR的设计文档中,它称“dso、mmap、process、thread”等为“base types”。很难用中文理解之,估计是指apr_mmap_t这些类型吧。权且这么理解吧^_^
3) 另外的一个特色就是大多APR类型中都包含一个apr_pool_t类型的字段,该字段用于分配APR内部使用的内存,任何APR函数须要内存均可以经过它分配。若是你建立一个新的类型,你最好在该类型中加入一个apr_pool_t类型的字段,不然全部操做该类型的APR函数都须要一个apr_pool_t类型的参数。
2、函数
1) 理解APR的函数设计对阅读APR代码颇有帮助。看了APR代码你会发现不少相似APR_DECLARE(apr_hash_t *) apr_hash_make(apr_pool_t*pool)带APR_DECLARE宏的函数声明,究竟是什么意思呢?为何要加一个APR_DECLARE呢?在apr.h中有这样的解释:“APR的固定个数参数公共函数的声明形式APR_DECLARE(rettype) apr_func(args);而非固定个数参数的公共函数的声明形式为APR_DECLARE_NONSTD(rettype) apr_func(args, ...);”。在Unix上的apr.h中有这两个宏的定义:
#defineAPR_DECLARE(type) type
#define APR_DECLARE_NONSTD(type) type
在apr.h文件中解释了这么作就是为了在不一样平台上编译时使用“the most appropriate calling convention”,这里的“calling convention”是一术语,翻译过来叫“调用约定”。[注1]
常见的调用约定有:stdcall、cdecl、fastcall、thiscall和naked call,其中cdecl调用约定又称为C调用约定,是C语言缺省的调用约定。
2) 若是你想新增APR函数,APR建议你最好能按以下作,这样会和APR提供的函数保持最好的一致性:
a) 输出参数为第一个参数;
b) 若是某个函数须要内部分配内存,则将一个apr_pool_t参数放在最后。
3、错误处理
大型的系统程序的错误处理是十分重要的,APR做为一通用的库接口集合详细的说明了使用APR时如何进行错误处理。
1) 错误处理的第一步就是“错误码和状态码分类”。APR的函数大部分都返回apr_status_t类型的错误码,这是一个int型,在apr_errno.h中定义,和它在一块儿定义的还有apr所用的全部错误码和状态码。APR定义了5种错误码类型,它们分别为“0”[注2]、APR_OS_START_ERROR、APR_OS_START_STATUS、APR_OS_START_USEERR和APR_OS_START_SYSERR,它们每一个都拥有本身独自的偏移量。
2) 如何定义错误捕捉策略?
因为APR是可移植的,这样就可能遇到这样一个问题:不一样平台错误码的不一致。如何处理呢?APR给咱们提供了2种策略:
a) 跨多平台返回相同的错误码
这种策略的缺点是转换费时且在转换时有错误码损耗。好比Windows操做系统定义了成百上千错误码,而POSIX才定义了50错误码,若是都转换为规范统一的错误码,势必会有错误码含义丢失,有可能得不到拥有真正含义的错误码。执行流程如:
make syscall that fails
convert to common errorcode
return common errorcode
-------------------------------------------------------------------
decide execution based on common error code
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++这样的中、高级语言中,使用“调用约定”来讲明这两个问题。
[注2] 特殊“0”
每一个平台都有0,可是都没有实际的定义,0又的确是一个errnovalue的offset,可是它是“匿名的”,它不像EEXIST那样有着能够“自描述”的名字。
APR分析-进程篇
APR进程封装源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的proc.c文件内容,其相应头文件为$(APR_HOME)/include/apr_thread_proc.h。
1、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);[注1]
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);
2、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)调用后,咱们能够用下面的图来表示这些字段的状态:[注3]
parent_in ----------------------------------------------
/|/
------------------------------------------
filedes[0] "in_pipe" filedes[1]
------------------------------------------
/|/
child_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,定义见[注2]
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,用于检索容许该用户所使用的权限。
3、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 stdouterr*/
apr_file_t *err;
} apr_proc_t;
结构中有很清晰明了的注释,这里就再也不说了。
建立一个新的进程的接口为apr_proc_create,其参数也都很简单。前面说过apr_proc_create先fork出一个子进程,众所周知fork后子进程是父进程的复制品[注4],而后子进程再经过exec函数加载新的程序映像,并开始执行新的程序。这里分析一下apr_proc_create的执行流程,其伪码以下:
apr_proc_create
{
if (attr->errchk)
对attr作有效性检查,让错误尽可能发生在parentprocess中,而不是留给child process; ----(1)
fork子进程;
{ /* 在子进程中 */
清理一些没必要要的从父进程继承下来的描述符等,为
exec提供一个“干净的”环境;------(2)
关闭attr->parent_in、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的设计,这里仅仅做简单说明。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后子进程继承了这些。为了关掉一些没必要要的描述符和更好的和父进程通信,子进程做了一些重定向的工做,这里用2副图来表示重定向先后的差异:(图中显示的是子进程关闭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]
------------------|
为了能更好的体现出“对话通道”的概念,这里再画出父进程再关闭ttr->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只是用另外一个新程序替换了当前进程的正文、数据、堆和栈段。这里不详述这几种函数的差异,在参考资料中有相关描述[注5]。
4、总结
简单分析了一下APR的进程封装,APR的源代码注释很详尽,不少细节能够直接察看源码。
[注1]
#define HANDLE_RTVAL(func, rv) do { /
if (rv != APR_SUCCESS) { /
printf("%s executes error!/n", #func); /
return rv; /
} /
} while(0)
[注2]
typedef enum {
APR_SHELLCMD, /*use the shell to invoke the program */
APR_PROGRAM, /* invoke the program directly, no copied env */
APR_PROGRAM_ENV, /* invoke theprogram, replicating our environment */
APR_PROGRAM_PATH, /* find program on PATH,use our environment */
APR_SHELLCMD_ENV /* use the shell toinvoke the program, replicating our environment */
} apr_cmdtype_e;
[注3]
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使用。
[注4]
fork后子进程和父进程的同和异
同:
子进程从父进程那继承了
-- 父进程已打开的文件描述符;
-- 实际用户ID、实际组ID、有效用户ID、有效组ID;
-- 添加组ID;
-- 进程组ID;
-- 对话期ID;
-- 控制终端;
-- 设置用户ID标志和设置组ID标志;
-- 当前工做目录;
-- 根目录;
-- 文件方式建立屏蔽字;
-- 信号屏蔽和排列;
-- 对任一打开文件描述符的在执行时关闭标志;
-- 环境;
-- 链接的共享存储段;
-- 资源限制。
异:
-- fork的返回值;
-- 进程ID;
-- 不一样的父进程ID;
-- 子进程的tms_utime, tms_stime, tms_cutime以及tme_ustime设置为0;
-- 父进程设置的锁,子进程不继承;
-- 子进程的未决告警被清除;
-- 子进程的未决信号集设置为空集。
[注5]
这里引用《Unix环境高级编程》中关于如何区分和记忆exec函数族的方法:“这六个exec函数的参数很难记忆。函数名中的字符会给咱们一些帮助。字母p表示该函数取filename做为参数,而且用PATH环境变量寻找可执行文件。字母l表示该函数取一个参数列表,它与字母v互斥。v表示该函数取一个argv[]。最后,字母e表示该函数取envp[] 数组,而不使用当前环境。”
参考资料:
1、《Unix环境高级编程》
2、《Unix系统编程》
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)。
1、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_poll_create(pool, NULL);
for (i = 0; i < n; ++i) {
apr_pool_clear(subpool);
... ...
do_operation(..., subpool);
}
apr_pool_destroy(subpool);
2、深刻APR Pool
到目前为止咱们已经知道了该如何“很好的”使用APR Pool,接下来咱们来深刻APRPool的内部,看究竟有什么“奥秘”。
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_initialize又是APR program调用的第一个function,其在apr_general.h中声明,在misc/unix/start.c中实现)。
apr_pool_initialize的伪码以下(这里先不考虑多线程的状况):
static apr_byte_t apr_pools_initialized = 0;
static apr_pool_t *global_pool = NULL;
static apr_allocator_t *global_allocator = NULL;
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;
... ...[注1]
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大小内存时,allocaotr_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;
constchar *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;
也许如今你仍然不能看清楚APRPool的结构,无需着急,咱们继续往下分析。
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。若是APR Pool中存在多个Pool,其节点结构关系以下:
/-child-->
/ --------Pool_level1-a
/ / parent /|/ |
/|/_ | | sibling
global_pool | |
/ | /|/
/-child-> Pool_level1-b
/|/ |
-parent------
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掉内存了。
3、总结
本文并未说明APR Pool有哪些优势或缺点(除了概述中的一些Rules),仅是把其前因后果弄清。
[注1]
在本文中出现的"......"的符号表示与多线程相关的字段和代码的省略。
APR分析-信号篇
1、信号介绍
1、Signal“历史久远”,在最初的Unix系统上就能看到它“伟岸”的身影。它的引入用来进行User Mode进程间的交互,系统内核也能够利用它通知User Mode进程发生了哪些系统事件。从最开始引入到如今,信号只是作了很小的一些改动(不可靠信号模型到可靠信号模型)。
2、信号服务于两个目的:
1) 通知某进程某特定事件发生了;
2) 强制其通知进程执行相应的信号处理程序。
2、基础概念
1、信号的一个特性就是能够在任什么时候候发给某一进程,而无需知道该进程的状态。若是该进程当前并未处于执行态,则该信号被内核Save起来,直到该进程恢复执行才传递给它;若是一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消它才被传递给进程。
2、系统内核严格区分信号传送的两个阶段:
1) Signal Generation : 系统内核更新目标进程描述结构来表示一个信号已经被发送出去。
2) Signal Delivery : 内核强制目标进程对信号作出反应,或执行相关信号处理函数,或改变进程执行状态。
信号的诞生和传输咱们能够这样理解:把信号做为“消费品”,其Generation状态就是“消费品诞生”,其Delivery状态就是理解为“被消费了”。这样势必存在这样的一个状况:“消费品诞生了,可是尚未被消费掉”,在信号模型中,这样的状态被称为“pending”(悬而未决)。
任什么时候候一个进程只能有一个这样的某类型的pending信号,同一进程的其余同类型的pending信号将不排队,将被简单的discard(丢弃)掉。
3、如何消费一个signal
1) 忽略该信号;[注1]
2) 响应该信号,执行一特定的信号处理函数;
3) 响应该信号,执行系统默认的处理函数。包括:Terminate、Dump、Ignore、Stop、Continue等。
这里有特殊:SIGKILL和SIGSTOP两个信号不能忽略、不能捕捉、不能阻塞,而只是执行系统默认处理函数。
3、APR Signal封装
APR Signal源代码的位置在$(APR_HOME)//threadproc目录下,本篇blog着重分析unix子目录下的signals.c文件内容,其相应头文件为$(APR_HOME)/include/apr_signal.h。
1、apr_signal函数
Unix信号机制提供的最简单最多见的接口是signal函数,用来设置某特定信号的处理函数。可是因为早期版本和后期版本处理信号方式的不一样,致使如今直接使用signal函数在不一样的平台上可能获得不一样的结果。
早期版本处理方式:进程每次处理信号后,随即将信号的处理动做重置为默认值。
后期版本处理方式:进程每次处理信号后,信号的处理动做不被重置为默认值。
咱们举例测试一下:分别在Solaris9 、Cygwin和RedHat Linux 9上。
例子:
E.G 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("insiguser1_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
...
..
E.G 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;
#ifdefSA_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一块儿使用。关于信号集和sigprocmask函数将在下面详述。
2、apr_signal_block和apr_signal_unblock
这两个函数分别负责阻塞和取消阻塞内核传递某信号给目标进程。其主要利用的就是sigprocmask函数来实现的。每一个进程都有其对应的信号屏蔽字,它让目标进程可以通知内核“哪些传给个人信号该阻塞,哪些畅通无阻”。在《Unix高级环境编程》中做者有这么一段说明“若是在调用sigprocmask后有任何未决的、再也不阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。”能理解这句我想信号屏蔽字这块儿也就没什么问题了。在Unix高级环境编程》中做者举了一个很不错的例子,讲解的也很详细。这里想举例说明的是:若是屡次调用SET_BLOCK的sigprocmask设置屏蔽字,结果是什么呢?
E.G 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)) {
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 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");
}
exit(0);
}
//output:
1st to wait 30 seconds
^/
SIGQUIT pending
SIGUSR1 unpending
2nd to wait 30 seconds -- 这以后发送kill -USR128821
SIGQUIT pending
SIGUSR1 pending
第一次输出SIGUSR1unpending是由于并未发送USR1信号,因此天然为unpending状态;我想说的是第二次从新sigprocmask时咱们仅加入了SIGUSR1,并未显示加入SIGQUIT,以后察看pending信号中SIGQUIT仍然为pending状态,这说明两次SET_BLOCK的sigprocmask调用是"或"的关系,第二次SET_BLOCK的sigprocmask调用不会将第一次SET_BLOCK的sigprocmask调用设置的阻塞信号变为非阻塞的。
4、总结
信号简单而强大,若是想深刻了解signal的实现,参考资料中的第二本书会给你满意的答案。
5、参考资料:
1、《Unix高级环境编程》
2、《深刻理解Linux内核》
[注1]
忽略信号和阻塞信号
前者至关于一个消费行为,该信号的状态为“已消费”,然后者只是将信号作缓存,等待阻塞打开,再交给进程消费,其状态为“未消费”,也至关于处于pending状态。
APR分析-文件IO篇
APR File I/O源代码的位置在$(APR_HOME)/file_io目录下,本篇blog着重分析unix子目录下的相关.c文件内容,其相应头文件为$(APR_HOME)/include/apr_file_io.h和apr_file_info.h。
1、APR File I/O介绍
APR用了"不小的篇幅"来"描述"文件I/O,在$(APR_HOME)/file_io/unix目录下,你会看到多个.c文件,每一个.c都是一类文件I/O操做。好比:
open.c -- 封装了文件的打开、关闭、更名和删除等操做;
readwrite.c -- 顾名思义,它里面包含了文件的读写操做;
pipe.c -- 包含了pipe相关操做。
还有许多这里很少说,因为文件I/O操做复杂,咱们下面将仅挑出最经常使用的文件I/O操做进行分析。
2、基本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;
intbufpos; /* Read/Write position in buffer */
unsigned long dataRead; /* amountof valid data read into buffer */
intdirection; /*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
};
在这个数据结构中有些字段的含义一目了然,如filedes、fname、is_pipe等,而有些呢即便看了注释也不可以立刻了解其真正的含义,这就须要在阅读源码时来体会。
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);
其中fname、flag和perm三个参数你应该很眼熟吧:)。每一个封装都有自定义的一些标志宏,这里也不例外,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操做次数,会带来性能上明显的改善。这里将用图示说明缓冲区与文件的对应关系,以帮助理解APR缓冲I/O:
thefile->filePtr
|
0 /|/ 文件末尾
-----------------------------------------------
/////////////////// <---- thefile->filedes (文件)
-----------------------------------------------
/ /
/ /
/ /
0|/_ _/| APR_FILE_BUFSIZE
-----------------------------------------------
//////////////////////// (缓冲区)
//////////
-----------------------------------------------
/|/ /|/ /|/
| | |
| | thefile->dataRead
| thefile->bufpos
thefile->buffer
说明:"//////"-- 表示从文件读到缓冲区的数据;
"//////" --表示从用户已从缓冲区读出的数据。
thefile->bufpos : 缓冲区中的读写位置
thefile->dataRead: 标识缓冲区从文件读取的数据的大小
thefile->fileptr: 标识文件自己被读到什么位置
读写切换:若是先读后写,则每次写的时候都要从新定位文件指针到上次读的结尾处;若是先写后读,则每次读前都要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 = read(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中内存的工做,这里不详述了。
3、总结
复杂的文件I/O,让咱们经过三言两语就说完了。你们慢慢体会,看看世界著名开源项目的源代码,收获是颇丰的,不妨尝试一下。
APR分析-高级IO篇
1、记录锁或(区域锁)[注1]
我见过的对记录锁讲解最详细的书就是《Unix高级环境编程》,特别是关于进程、文件描述符和记录锁三者之间关系的讲解更是让人受益不浅,有此书的朋友必定不要放过哟。这里将其中的三原则摘录到这:
关于记录锁的自动继承和释放有三条规则:
(1) 锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所创建的锁所有释放;第二重意思就不很明显,任什么时候候关闭一个描述符时,则该进程经过这一描述符能够存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
(2) 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程获得一把锁,而后调用fork,那么对于父进程得到的锁而言,子进程被视为另外一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以得到它本身的锁。这与锁的做用是相一致的。锁的做用是阻止多个进程同时写同一个文件(或同一文件区域)。若是子进程继承父进程的锁,则父、子进程就能够同时写同一个文件。
(3) 在执行exec后,新程序能够继承原执行程序的锁。
话归正题谈APR的记录锁,平心而论APR的提供的加索和解锁接口并无什么独到的地方,APR之因此将之封装起来,无非是为了提供一个统一的跨平台接口,而且不破坏APR总体代码风格的一致性。APR记录锁源码位置在$(APR_HOME)/file_io/unix目录下flock.c,头文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock仅提供对整个文件的加锁和解锁,而并不支持对文件中任意范围数据的加锁和解锁。至于该锁是建议锁(advisory lock)仍是强制锁(mandatory lock),须要看具体的平台的实现了。两个函数均利用fcntl实现记录锁功能(前提是所在平台支持fcntl,因为fcntl是POSIX标准,绝大多数平台都支持)。代码中有一处值得鉴赏:
while ((rc = fcntl(thefile->filedes, fc, &l)) < 0&& errno == EINTR)
continue;
这里这么作的缘由就是考虑到fcntl的调用可能被某信号中断,一旦中断咱们去要重启fcntl函数。
2、I/O多路复用[注2]
在经典的《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)
做者同时对这5种I/O模型做了很详细的对比分析,很值得一看。这里所说的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的实现(That is 使用poll来实现apr_poll及apr_pollset_xx相关,与之对应的是使用select来实现apr_poll及apr_pollset_xx相关)来分析:在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 */
};
把数据结构定义贴出来便于后面分析时参照理解。
假设咱们像这样apr_pollset_create(&mypollset,10, p, 0)调用,那么在apr_pollset_create后,咱们能够用图示来表示mypollset变量的状态:
mypollset
-------
nalloc ----> 10 /* 该mypollset的“容量”,在create的时候由参数指定 */
-------
nelts ----> 0 /* 刚初始化,mypollset中并无任何element,以后每add一次,nelts就+1 */
-------
---------------------------------------------
pollset ---------> pollset[0] | pollset[1] |...|pollset[nalloc-1]
---------------------------------------------
-------
-----------------------------------------------------
query_set ---------> query_set[0] | query_set[1] |...|query_set[nalloc-1]
-----------------------------------------------------
-------
---------------------------------------------------------
result_set ---------> result_set[0] | result_set[1] |...|result_set[nalloc-1]
---------------------------------------------------------
-------
pollset、query_set和result_set这几个集合的关系经过下图说明:
apr_pollfd_t *descriptor ---> [pollset_add]--------> query_set ------ [pollset_poll] -----> result_set (输出)
| /|/
------------------->pollset ------ [pollset_poll] --------------------
apr_pollset_xx系列是改版后APR I/O复用新增的接口集,它以apr_pollset_t做为其管理的基本单位,其中apr_pollset_poll用于监视pollset中的全部descriptor(s)。而apr_poll则是旧版的APR I/O复用接口,它一样能够实现apr_pollset_poll的功能,只是它的基本管理单位是apr_pollfd_t,其相关函数还包括apr_poll_setup、apr_poll_socket_add等在apr-1.1.1版中已看不到的几个接口。新版本中建议使用apr_pollset_poll,起码APR的测试用例(testpoll.c)是这么作的。
select实现的思路与poll实现的思路是一致的,只是apr_pollset_t的结构不一样,缘由不言自明。
3、总结
因为APR对高级I/O的封装很“薄”,因此基本上没有太多很精致的东西。
4、参考资料
1、《Unix高级环境编程》
2、《Unix网络编程卷1、2》
[注1]
对于Unix,“记录”这个定语也是误用,由于Unix内核根本没有使用文件记录这种概念。一个更适合的术语多是“区域锁”,由于它锁定的只是文件的一个区域(也多是整个文件)-- 摘自《Unix高级环境编程》。
[注2]
在《Unix网络编程卷1》译者译为"多路复用",在《Unix高级环境编程》中译者译为"多路转接",我更倾向于前者。I/O多路复用其英文为"I/OMultiplexing"。
APR分析-共享内存篇
APR共享内存封装的源代码的位置在$(APR_HOME)/shmem目录下,本篇blog着重分析unix子目录下的shm.c文件内容,其相应头文件为$(APR_HOME)/include/apr_shm.h。
1、共享内存简单小结
共享内存是最快的IPC方式,由于一旦这样的共享内存段映射到各个进程的地址空间,这些进程间经过共享内存的数据传递就不须要内核的帮忙了。Stevens的解释是“各进程不是经过执行任何进入内核的系统调用来传递数据,显然内核的责任仅仅是创建各进程地址空间与共享内存的映射,固然像处理页面故障这一类的底层活仍是要作的”。相比之下,管道和消息队列交换数据时都须要内核来中转数据,速度就相对较慢。
Unix“历史悠久”,因此在历史上不一样版本的Unix提供了不一样的支持共享内存的方式,我想这也是Stevens在《Unix网络编程第2卷》中花费三章来说解共享内存的缘由吧。你也不妨先看看shm.c中的代码,代码用条件宏分割不一样Share Memory的实现。
2、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
}
}
apr_shm_create函数代码很长,之因此这样是由于其支持多种建立Share Memory的方式,在上面的伪代码中共用条件宏分隔了6种方式,这6种方式将在下面分析。能够看出shmem主要分为"匿名的"和"有名的",其中"有名的"都是经过filename来标识(或经过ftok转换filename而获得的shmid来标识)。
其中不一样版本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作标识。
3、总结
内核架起了多个进程间共享数据的纽带--共享内存。经过上面的叙述你会发现共享内存的建立其实并不困难,真正困难的是共享内存的管理[注1],在正规的软件公司像内存/共享内存管理这样的重要底层功能都是封装成库形式的,固然内存管理的内容不是这篇blog重点涉及的内容。
4、参考资料:
1、《Unix网络编程第2卷》
2、《Unix环境高级编程》
[注1] SIGSEGV和SIGBUS
涉及共享内存的管理就不能不提到访问共享内存对象。谈到访问共享内存对象就要留神“SIGSEGV和SIGBUS”这两个信号。
系统分配内存页来承载内存映射区,因为内存页大小是固定的,因此存在多余的页空间空闲,好比待映射文件大小为5000 bytes,内存映射区大小也为5000 bytes。而一个内存页大小4096,系统势必要分配两页来承载,这时空闲的有效空间为从5000-8191,若是进程访问这段地址空间也不会发生错误。可是要超出8191,就会收到SIGSEGV信号,致使程序中止。关于SIGBUS信号的来历,这里也举例说明:若待映射文件大小为5000 bytes,咱们在mmap时指定内存映射区size = 15000 > 5000,这时内核真正的共享区承载体大小只有8192(能包容映射文件大小便可),此时在[0,8191]内访问均没问题,但在[8192,14999]之间会获得SIGBUS信号;超出15000访问时会触发SIGSEGV信号。
APR分析-环篇
在大学的时候学的不是计算机专业,但大三的时候我所学的专业曾开过一门好像叫“计算机软件开发基础”的课,使用的是清华的一本教材,课程的内容包括数据结构。说实话听过几节课,那个老师讲的还不错,只是因为课程目标所限,没讲那么深罢了。固然我接触数据结构要早于这门课的开课时间。早在大一下学期就开始到计算机专业旁听“数据结构”,再说一次实话,虽号称名校名专业,可是那个老师的讲课水平却不敢恭维。
言归正传! 简单说说环(RING):环是一个首尾相连的双向链表,也就是咱们所说的循环链表。对应清华的那本经典的《数据结构》一书中线性表一章的内容,按照书中分类其属于线性表中的链式存储的一种。环是很常见也很实用的数据结构,相信在这个世界上环的实现不止成千上万,可是APR RING(按照APR RING源代码中的注释所说,APR RING的实现源自4.4BSD)倒是其中较独特的一个,其最大的特色是其全部对RING的操做都由一组宏(大约30个左右)来实现。在这里不能逐个分析,仅说说一些让人印象深入的方面吧。
1、如何使用APR RING?
咱们先来点感性认识! 下面是一个典型的使用APRRING的样例:
假设环节点的结构以下:
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;
structelem_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都指向了本身。因此判断APRRING是否为空只须要判断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。
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进程同步机制的一个典型使用方法就是:“Createthe mutex in the Parent, Attach to it in the Child”。APR提供接口apr_proc_mutex_child_init在子进程中re-open themutex。
5、小结
APR提供多种锁机制,因此使用的时候要根据具体应用状况细心选择。
APR分析-线程篇
APR线程的源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的thread.c文件内容,其相应头文件为$(APR_HOME)/include/apr_threadproc.h。
1、线程基础
《深刻理解计算机系统》(如下称CS.APP)一书中对线程基础概念的讲解让我眼前豁然开朗,这里不妨引述一下:
(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) 线程不像进程那样有着“父-子”体系,同一个进程内的线程都是“对等的”,主线程与其余线程不一样之处就在于其是进程建立的第一个线程。
2、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;
};
这个类型封装了线程的属性,不一样的线程属性会致使线程的行为有所不一样。Pthread提供多种线程属性设置接口,但是APR并未所有提供,必要时我以为能够本身来调用Pthread接口。APR提供的属性设置接口包括设置线程的可分离性、线程栈大小和栈Guard区域属性。
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用来等待某个线程结束并释放其资源。
3、小结
基本的线程管理接口相对较简单,关键是对线程概念的理解。接下来的“线程同步”则是件比较有趣的话题。
APR分析-网络IO篇
APR网络I/O的源代码的位置在$(APR_HOME)/network_io目录下,本篇blog着重分析unix子目录下的各.c文件内容,其相应头文件为$(APR_HOME)/include/apr_network_io.h。
以程序员的视角来看待网络,这样咱们能够忽略一些网络的基础概念。下面将按部就班地接触网络,并说明APR是如何支持这些网络概念的。
1、IP地址 -- 主机通讯
咱们熟知的而且天天工做于其上的因特网是一个世界范围的主机的集合,这个主机集合被映射为一个32位(目前)或者64位(未来)IP地址;而IP地址又被映射为一组因特网域名;一个网络中的主机上的进程能经过一个链接(connection)和任何其余网络中的主机上的进程通讯。
1、IP地址存储
在现在的IPV4协议中咱们通常使用一个unsignedint来存储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, soAPR 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地址使用的是一个unsignedint整形数来表示。这样就存在着一个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,至于使用哪一个就看你的喜爱了^_^。
2、SOCKET -- 进程通讯
前面提到过经过一个链接(connection)能够链接两个internet不一样或相同主机上的不一样进程,这个链接是点对点的。而从Unix内核角度来看,SOCKET则是链接的一个端点。每一个SOCKET都有一个地址,其地址由主机IP地址和通信端口号组成。一个链接有两个端点,这样一个链接就能够由一个SOCKET对惟一表示了。这个SOCKET对是这个样子的(cliaddr:cliport, servaddr:servport)。
那么在应用程序中咱们如何获取和使用这一互联网上的进程通信利器呢?每一个平台都为应用程序提供了一套SOCKET编程接口,APR又在不一样平台提供的接口之上进行了封装,使代码能够在不一样平台上编译运行,并且易用性也有所提升。
1、SOCKET描述符
SOCKET属于系统资源,咱们必须经过系统调用来申请该资源。SOCKET资源的申请相似于FILE,在使用文件时咱们经过调用open函数获取文件描述符,相似咱们也可经过调用下面的接口来获取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;
#ifndef 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 pollsetis used */
apr_pollset_t *pollset;
#endif
};
该结构中的socketdes字段实际上是真正存储由socket函数返回的SOCKET描述符的,其余字段都是为APR本身所使用的,这些字段在Bind、Connect等过程当中使用。另外须要说起的就是要分清SOCKET描述符和SOCKET地址(IP地址,端口号),前者是系统资源,然后者用来描述一个链接的一个端点的地址。SOCKET描述符能够表明任意的SOCKET地址,也能够绑定到某个固定的SOCKET地址上(在后面有说明)。咱们若是不显式将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 */
#defineAPR_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_RCVBUF 128 /**< Receive buffer */
#define APR_SO_DISCONNECTED 256 /**< Disconnected*/
... ...
另外从上面这些属性值(都是2的n次方)能够看出SOCKET也是使用一个属性控制字段中的“位”来控制SOCKET属性的。
再有APR提供一个宏apr_is_option_set来判断一个SOCKET是否拥有某个属性。
3、Connect、Bind、Listen、Accept -- 创建链接
这里不详述C/S模型了,只是说说APR支持C/S模型的一些接口。
(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) 注[1]
if (rc != APR_SUCCESS){
return rc;
}
if (rc == -1 && errno != EISCONN) {
returnerrno; --------- (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描述符占用了。在服务器编程中Bind几乎成为了“必选”之调用,由于通常服务器程序都有本身的“名气很大”的SOCKET地址,如TELNET服务端口号23等。apr_socket_bind也并未作太多的工做,只是简单的调用了bind系统接口,并设置了apr_socket_t结构的几个local_addr字段。
(3) apr_socket_listen
按照《Unix网络编程 Vol1》的说法,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 = read(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可用。这里我以为好像有个问题,想象一下若是上一次SOCKET的状态为APR_INCOMPLETE_READ,那么从新调用apr_socket_read后在SOCKET属性中去掉APR_INCOMPLETE_READ,而后进入apr_wait_for_io_or_timeout过程,一旦apr_wait_for_io_or_timeout失败,那么就直接返回了。而实际上SOCKET仍然应该处于APR_INCOMPLETE_READ状态,而下次再调用apr_socket_read就直接进入一轮完整数据的读取过程了,不知道这种情形是否可否发生。
(d) 将(*len)设置为实际从SOCKET Buffer中读取的字节数,并根据这一实际数据与要求数据做比较来设置sock->options。
(2) apr_socket_send
apr_socket_send负责发送数据到SOCKET Buffer,其实现的方式与apr_socket_recv大同小异,这里就不分析了。
3、小结
APR Network I/O中还有对Multicast的支持,因为平时不常接触,这里不分析了。
注[1]:
/* in errno.h */
#define EISCONN 133 /* Socket is already connected */
#define EALREADY 149 /* operation already in progress */
#define EINPROGRESS 150 /* operation now in progress */
APR分析-线程同步篇
线程同步的源代码的位置在$(APR_HOME)/locks目录下,本篇blog着重分析unix子目录下的thread_mutex.c、thread_rwlock.c和thread_cond.c文件的内容,其相应头文件为(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。
因为APR的封装过于“浅显”,实际上也并无多少值得分析的“靓点”。因此本篇其实是在讨论线程同步的3种运行模型。
1、互斥量
互斥量是线程同步中最基本的同步方式。互斥量用于保护代码中的临界区,以保证在任一时刻只有一个线程或进程访问临界区。
1、互斥量的初始化
在POSIX Thread中提供两种互斥量的初始化方式,以下:
(1) 静态初始化
互斥量首先是一个变量,Pthread提供预约义的值来支持互斥量的静态初始化。举例以下:
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
既然是静态初始化,那么必然要求上面的mutex变量须要静态分配。在APR中并不支持apr_thread_mutex_t的使用预约值的静态初始化(但能够变通的利用下面的方式进行静态分配的mutex的初始化)。
(2) 动态初始化
除了上面的状况,若是mutex变量在堆上或在共享内存中分配的话,咱们就须要调用一个初始化函数来动态初始化该变量了。在Pthread中的对应接口为pthread_mutex_init。APR封装了这一接口,咱们能够使用下面方式在APR中初始化一个apr_thread_mutex_t变量。
apr_thread_mutex_t *mutex = NULL;
apr_pool_t *pool = NULL;
apr_status_t stat;
stat =apr_pool_create(&pool, NULL);
if (stat !=APR_SUCCESS) {
printf("error in pool %d/n", stat);
} else {
printf("ok in pool/n");
}
stat =apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, pool);
if (stat !=APR_SUCCESS) {
printf("error %d in mutex/n", stat);
} else {
printf("ok in mutex/n");
}
2、互斥锁的软弱性所在
互斥锁之软弱性在于其是一种协做性锁,其运做时对各线程有必定的要求,即“全部要访问临界区的线程必须首先获取这个互斥锁,离开临界区后释放该锁”,一旦某一线程不遵循该要求,那么这个互斥锁就形同虚设了。以下面的例子:
举例:咱们有两个线程,一个线程A遵循要求,每次访问临界区均先获取锁,而后将临界区的变量x按偶数值递增,另外一个线程B不遵循要求直接修改x值,这样即便在线程A获取锁的状况下仍能修改临界区的变量x。
static apr_thread_mutex_t *mutex = NULL;
staticint x = 0;
staticapr_thread_t *t1 = NULL;
staticapr_thread_t *t2 = NULL;
static void * APR_THREAD_FUNC thread_func1(apr_thread_t *thd, void*data)
{
apr_time_t now;
apr_time_exp_t xt;
while (1) {
apr_thread_mutex_lock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: own the lock, time[%02d:%02d:%02d]/n", xt.tm_hour,xt.tm_min,
xt.tm_sec);
printf("[threadA]: x = %d/n", x);
if (x % 2 || x == 0) {
x += 2;
} else {
printf("[threadA]: Warning: x变量值被破坏,现从新修正之/n");
x += 1;
}
apr_thread_mutex_unlock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: release the lock, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min,
xt.tm_sec);
sleep(2);
}
return NULL;
}
static void * APR_THREAD_FUNC thread_func2(apr_thread_t *thd, void*data)
{
apr_time_t now;
apr_time_exp_t xt;
while (1) {
x ++;
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadB]: modify the var, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min, xt.tm_sec);
sleep(2);
}
return NULL;
}
int main(int argc, const char * const * argv, const char * const*env)
{
apr_app_initialize(&argc, &argv, &env);
apr_status_t stat;
//...
/*
* 建立线程
*/
stat =apr_thread_create(&t1, NULL, thread_func1, NULL, pool);
stat =apr_thread_create(&t2, NULL, thread_func2, NULL, pool);
//...
apr_terminate();
return 0;
}
//output
... ...
[threadA]: own the lock, time[10:10:15]
[threadB]: modify the var, time[10:10:15]
[threadA]: x = 10
[threadA]: Warning: x变量值被破坏,现从新修正之
[threadA]: release the lock, time[10:10:15]
固然这个例子不必定很精确的代表threadB在threadA拥有互斥量的时候修改了x值。
2、条件变量
互斥量通常用于被设计被短期持有的锁,一旦咱们不能肯定等待输入的时间时,咱们能够使用条件变量来完成同步。咱们曾经说过I/O复用,在咱们调用poll或者select的时候实际上就是在内核与用户进程之间达成了一个协议,即当某个I/O描述符事件发生的时候内核通知用户进程而且将处于挂起状态的用户进程唤醒。而这里咱们所说的条件变量让对等的线程间达成协议,即“某一线程发现某一条件知足时必须发信号给阻塞在该条件上的线程,将后者唤醒”。这样咱们就有了两种角色的线程,分别为
(1) 给条件变量发送信号的线程
其流程大体为:
{
获取条件变量关联锁;
修改条件为真;
调用apr_thread_cond_signal通知阻塞线程条件知足了;------(a)
释放变量关联锁;
}
(2) 在条件变量上等待的线程
其流程大体为:
{
获取条件变量关联锁;
while (条件为假) {--------------------- (c)
调用apr_thread_cond_wait阻塞在条件变量上等待;------(b)
}
修改条件;
释放变量关联锁;
}
上面两个流程中,理解三点最关键:
a) apr_thread_cond_signal中调用的pthread_cond_signal保证至少有一个阻塞在条件变量上的线程恢复;在《Unix网络编程 Vol2》中也谈过这里存在着一个race。即在发送cond信号的同时,该发送线程仍然持有条件变量关联锁,那么那个恢复线程的apr_thread_cond_wait返回时仍然拿不到这把锁就会再次挂起。这里的这个race要看各个平台实现是如何处理的了。
b) apr_thread_cond_wait中调用的pthread_cond_wait原子的将调用线程挂起,并释放其持有的条件变量关联锁;
c) 这里之因此使用while反复测试条件,是防止“伪唤醒”的存在,即条件并未知足就被唤醒。因此不管怎样,唤醒后我都须要从新测试一下条件,保证该条件的的确确知足了。
条件变量在解决“生产者-消费者”问题中有很好的应用,在我之前的一篇blog中也说过这个问题。
3、读写锁
前面说过,互斥量把想进入临界区而又试图获取互斥量的全部线程都阻塞住了。读写锁则改进了互斥量的这种霸道行为,它区分读临界区数据和修改临界区数据两种状况。这样若是有线程持有读锁的话,这时再有线程想读临界区的数据也是能够再获取读锁的。读锁和写锁的分配规则在《Unix网络编程 Vol2》中有详细说明,这里不详述。
4、小结
三种同步方式如何选择?场合不一样选择也不一样。互斥量在于彻底同步的临界区访问;条件变量在解决“生产者-消费者”模型问题上有独到之处;读写锁则在区分对临界区读写的时候使用。