1.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 ?Cladm
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。
1.5 APR的可移植性
正如前面所描述,APR的目前的首要目标就是设计为一个跨平台的通用库,因此在APR的整个设计过程中无不体现了可移植的思想,APR附带一个简短的设计文档,文字言简意赅,其中很多的移植设计思想都值得我们所借鉴,主要从四个方面谈。
1.5.1APR类型
为了支持可移植性,APR中的一个策略就是尽量使用APR自定义的类型来代替平台相关类型。这样的好处很多,比如便于代码移植,避免数据间进行不必要的类型转换(假如你不使用APR自定义的数据类型,你在使用某些APR提供的接口时,就需要进行一些参数的类型转换);自定义数据类型的名字更加具有自描述性,提高代码可读性。APR提供的基本自定义数据类型包括apr_byte_t,apr_int16_t,apr_uint16_t,apr_size_t等等。通常情况下这些类型都定义在apr.h中,不过你找遍整个APR包也不会找到apr.h这个文件,不过include目录下倒是存在类似于apr.h的apr.h.in和apr.hw,这两个文件是生成apr.h的模版,在apr.h.in中通用APR类型定义如下:
typedef unsigned char apr_byte_t;
typedef @short_value@ apr_int16_t;
typedef unsigned @short_value@ apr_uint16_t;
typedef @int_value@ apr_int32_t;
typedef unsigned @int_value@ apr_uint32_t;
typedef @long_value@ apr_int64_t;
typedef unsigned @long_value@ apr_uint64_t;
typedef @size_t_value@ apr_size_t;
typedef @ssize_t_value@ apr_ssize_t;
typedef @off_t_value@ apr_off_t;
typedef @socklen_t_value@ apr_socklen_t;
@xxx@变量的值是可变的,不同的平台其值可能不一样。其值由configure配置过程自动生成,configue脚本中设置@xxx@变量的部分大致如下:
AC_CHECK_SIZEOF(char, 1)
AC_CHECK_SIZEOF(short, 2)
AC_CHECK_SIZEOF(int, 4)
AC_CHECK_SIZEOF(long, 4)
AC_CHECK_SIZEOF(long long, 8)
if test "$ac_cv_sizeof_short" = "2"; then
short_value=short
fi
if test "$ac_cv_sizeof_int" = "4"; then
int_value=int
fi
if test "$ac_cv_sizeof_int" = "8"; then
int64_value="int"
long_value=int
elif test "$ac_cv_sizeof_long" = "8"; then
int64_value="long"
long_value=long
elif test "$ac_cv_sizeof_long_long" = "8"; then
int64_value="long long"
long_value="long long"
elif test "$ac_cv_sizeof_longlong" = "8"; then
int64_value="__int64"
long_value="__int64"
else
AC_ERROR([could not detect a 64-bit integer type])
fi
if test "$ac_cv_type_size_t" = "yes"; then
size_t_value="size_t"
else
size_t_value="apr_int32_t"
fi
if test "$ac_cv_type_ssize_t" = "yes"; then
ssize_t_value="ssize_t"
else
ssize_t_value="apr_int32_t"
fi
.. ..
Configure的具体的细节并不是本书描述的细节,假如你想了解更多细节,你可以去阅读GNU的AutoConf、AutoMake等使用手册。
不同的操作系统中各个变量的值如下表所示:
变量类型
硬件平台
I686
I386
alpha
IA64
M68k
MIPS
Sparc
Spar64
apr_byte_t
1
1
1
1
1
1
1
1
apr_int16_t
2
2
2
2
2
2
2
2
apr_uint16_t
2
2
2
2
2
2
2
2
apr_int32_t
4
4
4
4
4
4
4
4
apr_uint32_t
4
4
4
4
4
4
4
4
apr_int64_t
4
4
8
8
4
4
4
4
apr_uint64_t
4
4
8
8
4
4
4
4
不过不同的操作系统中,定义各不相同,在Red Hat 9.0 Linux中,生成的定义如下:
typedef short apr_int16_t; //16位整数
typedef unsigned short apr_uint16_t; //16位无符号整数
typedef int apr_int32_t; //32位整数
typedef unsigned int apr_uint32_t; //32位无符号整数
typedef long long apr_int64_t; //64位整数
typedef unsigned long long apr_uint64_t; //64位无符号整数
typedef size_t apr_size_t; //
typedef ssize_t apr_ssize_t;
typedef off64_t apr_off_t;
typedef socklen_t apr_socklen_t; //套接字长度
通用数据类型的另外一个定义之处就是文件apr_portable.h中,APR中提供了通用的数据类型以及对应的操作系统依靠类型如下表:
通用类型
含义
Win32类型
BEOS类型
UNIX
apr_os_file_t
文件类型
HANDLE
int
Int
apr_os_dir_t
目录类型
HANDLE
dir
DIR
apr_os_sock_t
套接字类型
SOCKET
int
int
apr_os_proc_mutex_t
进程互斥锁
HANDLE
apr_os_proc_mutex_t
pthread_mutex_t
apr_os_thread_t
线程类型
HANDLE
thread_id
pthread_t
apr_os_proc_t
进程类型
HANDLE
thread_id
pid_t
apr_os_threadkey_t
线程key类型
DWord
int
pthread_key_t
apr_os_imp_time_t
FILETIME
strUCt timeval
struct timeval
apr_os_eXP_time_t
SYSTEMTIME
struct tm
tm
apr_os_dso_handle_t
DSO加载
HANDLE
image_id
void*
apr_os_shm_t
共享内存
HANDLE
void*
void*
一旦定义了这些通用的数据类型,APR不再使用系统类型,而是上述的APR类型。不过由于系统底层仍然使用系统类型,因此在使用通用类型的时候一项必须的工作就是用实际的类型来真正替代通用类型,比如apr_os_file_t,假如是Win32平台,则必须转换为HANDLE。对于上面表格的每一个通用数据类型,Apache都提供两个函数支持这种转换:
APR_DECLARE(apr_status_t) apr_os_XXX_get(…);
APR_DECLARE(apr_status_t) apr_os_XXX_put(…);
get函数用于将通用的数据类型转换为特定操作系统类型;而put函数则是将特定操作系统类型转换为通用数据类型。比如对于file类型,则对应的函数为:
APR_DECLARE(apr_status_t) apr_os_file_get(apr_os_file_t *thefile,
apr_file_t *file);
APR_DECLARE(apr_status_t) apr_os_file_put(apr_file_t **file,
apr_os_file_t *thefile,
apr_int32_t flags, apr_pool_t *cont);
前者将通用的文件类型apr_os_file_t转换为特定操作系统类型apr_file_t,后者则是将apr_file_t转换为apr_os_file_t。
在后面的分析中我们可以看到,对于每一个组件类型,比如apr_file_t中都会包含系统定义类型,APR类型都是围绕系统类型扩充起来的,比如apr_file_t,在Unix中为:
struct apr_file_t
{
int filedes; //UNIX下的实际的文件系统类型
… …
}
而在Window中则是:
struct apr_file_t
{
HANDLE filedes; //Window下的实际的文件系统类型
……
}
因此apr_os_file_get函数无非就是返回结构中的文件系统类型,而apr_os_file_put函数则无非就是根据系统文件类型创建apr_file_t类型。
类似的例子还有apr_os_thread_get和apr_os_thread_put等等。
1.5.2函数
APR中函数可以分为两大类:内部函数和外部接口函数。顾名思义,内部函数仅限于APR内部使用,外部无法调用;而外部结构函数则作为API结构,由外部程序调用。
1.5.2.1内部函数
APR中所有的内部函数都以static进行修饰。通常理解static只是指静态存储的概念,事实上在里面static包含了两方面的含义。
1)、在固定地址上的分配,这意味着变量是在一个非凡的静态区域上创建的,而不是每次函数调用的时候在堆栈上动态创建的,这是static的静态存储的概念。
2)、另一方面,static能够控制变量和函数对于连接器的可见性。一个static变量或者函数,对于特定的编译单元来说总是本地范围的,这个范围在C语言中通常是指当前文件,超过这个范围的文件或者函数是不可以看到static变量和函数的,因此编译器也无法访问到这些变量和函数,它们对编译器是不可见的。因此内部函数是不答应被直接调用的,任何直接调用都导致“尚未定义”的错误。不过潜在的好处就是,内部函数的修改不影响API接口。
static的这两种用法APR中都存在,但是第二种用法较多。
1.5.2.2外部API函数
对于APR用户而言,它们能够调用的只能是APR提供的API。要识别APR中提供的API非常的简单,假如函数是外部API,那么它的返回值总是用APR_DECLARE或者APR_DECLARE_NONSTD进行包装,比如:
APR_DECLARE(apr_hash_t *) apr_hash_make(apr_pool_t *pool);
APR_DECLARE(int) apr_fnmatch_test(const char *pattern);
APR_DECLARE和APR_DECLARE_NONSTD是两个宏定义,它们在apr.h中定义如下:
#define APR_DECLARE(type) type
#define APR_DECLARE_NONSTD(type) type
APR_DECLARE和APR_DECLARE_NONSTD到底是什么意思呢?为什么要将返回类型封装为宏呢?在apr.h中有这样的解释:
/**
* The public APR functions are declared with APR_DECLARE(), so they may
* use the most appropriate calling convention. Public APR functions with
* variable arguments must use APR_DECLARE_NONSTD().
*
* @remark Both the declaration and implementations must use the same macro.
* @example
*/
/** APR_DECLARE(rettype) apr_func(args)
* @see APR_DECLARE_NONSTD @see APR_DECLARE_DATA
* @remark Note that when APR compiles the library itself, it passes the
* symbol -DAPR_DECLARE_EXPORT to the compiler on some platforms (e.g. Win32)
* to export public symbols from the dynamic library build.\n
* The user must define the APR_DECLARE_STATIC when compiling to target
* the static APR library on some platforms (e.g. Win32.) The public symbols
* are neither exported nor imported when APR_DECLARE_STATIC is defined.\n
* By default, compiling an application and including the APR public
* headers, without defining APR_DECLARE_STATIC, will prepare the code to be
* linked to the dynamic library.
*/
#define APR_DECLARE(type) type
/**
* The public APR functions using variable arguments are declared with
* APR_DECLARE_NONSTD(), as they must follow the C language calling convention.
* @see APR_DECLARE @see APR_DECLARE_DATA
* @remark Both the declaration and implementations must use the same macro.
* @example
*/
/** APR_DECLARE_NONSTD(rettype) apr_func(args, ...);
*/
#define APR_DECLARE_NONSTD(type) type
从上面的解释中我们可以看出“APR的固定个数参数公共函数的声明形式APR_DECLARE(rettype) apr_func(args);而非固定个数参数的公共函数的声明形式为APR_DECLARE_NONSTD(rettype) apr_func(args, ...);”。
在apr.h文件中解释了这么做就是为了在不同平台上编译时使用“the most appropriate calling convention”,这里的“calling convention”是一术语,翻译过来叫“调用约定”。 我们知道函数调用是通过栈操作来完成的,在栈操作过程中需要函数的调用者和被调用者在下面的两个问题上做出协调,达成协议:
a) 当参数个数多于一个时,按照什么顺序把参数压入堆栈
b) 函数调用后,由谁来把堆栈恢复原来状态
c) 产生函数修饰名的方法
在像C/C++这样的中、高级语言中,使用“调用约定”来说明这两个问题。常见的调用约定有:__stdcall、__cdecl、__fastcall、thiscall和naked call。
__stdcall调用约定:函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈。
__cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。注重:对于可变参数的成员函数,始终使用__cdecl的转换方式。
__fastcall调用约定规定通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍然自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。
thiscall仅仅应用于"C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是要害词,因此不能被程序员指定。
naked call采用上述调用约定时,假如必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
另外不同的调用约定对于函数内部修饰名处理的方法也不一样。所谓修饰名是C或者C++函数在内部编译和链接的时候产生的唯一的标识名称。
对于C语言而言,__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为_functionname@number,例如 :function(int a, int b),其修饰名为:_function@8
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number。
假如是C++,不同调用约定处理要稍微复杂一点。由于Apache是基于C语言开发,因此本处不再描述。
1.5.2.3内存池参数
关于函数的最后一个问题就是它的参数,假如函数内部需要分配空间,那么你就可以看到参数的参数中肯定包含一个apr_pool_t参数,比如:
APR_DECLARE(apr_status_t) apr_shm_attach(apr_shm_t **m,
const char *filename,
apr_pool_t *pool);
由于Apache服务器所具有的一些特性,APR中并没有使用普通的malloc/free内存治理策略,而是使用了自行设计的内存池治理策略。APR中所有的需要的内存都不再直接使用malloc分配,然后首先分配一块足够大的内存块,然后每次需要的时候再从中获取;当内存不再使用的时候也不是直接调用free,而是直接归还给内存池。只有当内存池本身被释放的时候,这些内存才真正的被free给操作系统。Apache中使用apr_pool_t描述一个内存池,因此毫无疑问,由于这种非凡的内存分配策略,对于任何一个函数,假如你需要使用内存,那么你就应该指定内存所源自的内存池。这就是为什么大部分函数参数中都具有apr_pool_t的原因。关于内存池的具体细节,我们在第二章具体讨论。
关于作者
张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感爱好的朋友可以通过flydish1234 at sina.com.cn与之联系!
假如你觉得本文不错,请点击文后的“推荐本文”链接!!