您的位置:首页 > 运维架构 > Apache

libapr (apache portable runtime) 编程指南

2015-08-05 11:09 483 查看
这个指南主要介绍如何使用libapr(apache portable runtime)。

版权所有,Copyright (C) 2005 INOUE Seiichiro <inoue&ariel-networks.com>,翻译:成彦

原文地址:http://dev.ariel-networks.com/apr/apr-tutorial/html/apr-tutorial.html

转载时请保留版权信息。

1.libapr编程指南说明

2.libapr骨架代码

3.内存池(apr_pool_t)

4.错误状态(apr_status_t)

5.文件处理

6.文件锁



1.libapr编程指南说明

2.libapr骨架代码

当你开始学习一个新的开发库或框架的编程时,我相信首先写一个“骨架代码”是一个好的主意。“骨架代码”是最小的源程序,但你能编译和执行它(尽管他们经常毫无用处)。

幸运的是,libapr的骨架代码比其他流行的框架简单很多。让我们看一下apr-skeleton.c。我们在初始化的时候调用了apr_initialize(),在结束的时候调用了apr_terminate(),仅此而已。正如你所想的,这些代码表面上什么都没有做。

libapr并非一个框架库。因此,libapr不会帮助你去设计源程序的整个结构,凡事有利必有弊,这样的好处是可以容易地将libapr用到其他已经存在的代码中,不好之处在于当你使用libapr的时候,你必须自己设计代码的整个结构。

这儿,我们给出一些libapr编程的风格和规则:

* 命名规则非常简单和清楚。

* 不透明类型被广泛地使用。(又名不完整类型)

* 返回结果或是结果输出参数大多数返回类型为apr_status_t。

* 内存池规则。

我们可以在下面的代码中看到这些风格。

/* 摘自mp-sample.c */

apr_status_t rv;

apr_pool_t *mp;

rv = apr_pool_create(&mp, NULL);

我会在后***体描述这段代码的含义,这儿我们只留意其代码风格,最容易发现的是apr_前缀,apr_前缀表示该符号在libapr命名空间里。你也会发现_t后缀,它表示该符号是一个类型名。

apr_tool_t是不透明类型,这意味着其结构不是公共的,用面向对象的术语来说,也就是所有的成员变量都是私有的,你不能直接访问不透明类型的结构中的成员。并且在公共的头文件中,你也看不到不透明类型的定义。你对这个类型仅仅能做的事就是调用诸如apr_foo_bar()等这些API函数。非常重要的一点,你不能直接为不透明类型的实例分配内存(译者注:因为编译器只看到了其声明却没有发现其定义,因此不知道不透明类型占用内存的大小,用sizeof也无济于事),而只能调用相应的构造API。只有libapr知道如何去构造和析构这些对象。

apr_pool_create()函数返回的类型是apr_status_t,apr_status_t既是状态码又是错误码,它被广泛地作为大多数API函数的返回类型。我们也可以从函数的输出参数中得到结果,这样的输出参数被称为结果参数,用结果参数的方式得到返回结果在libapr中很常见。

通常情况下,如果你看到了apr_foo_t类型,那么你将会看到与之关联的apr_foo_bar()函数。下面是一段很典型的伪代码片断:

/* libapr伪代码,省略了错误的检测 */

apr_status_t rv;

apr_foo_t *foo;

rv = apr_foo_create(&foo, args...); /* 借助@args来创建@foo */

rv = apr_foo_do_something(foo, args...); /* 使用@foo */

apr_foo_destroy(foo); /* 销毁@foo对象。后面你将会发现,有时这个操作被与之关联的内存池隐含完成了 */



3.内存池(apr_pool_t)

大多数libapr的API都依赖于内存池,借助内存池,简化了内存块的管理。想像一下没有内存池系统的情况:你申请一些内存块就必需逐个释放它们,例如如果你申请了10个内存块,你必需释放10次,否则,你将遭受内存泄露的错误。内存池解决了这个令人感到繁琐的问题,在申请一个内存池之后,你可以从内存池中申请多个内存块,释放它们的时候,你所需要做的就是销毁内存池,这样你就可以释放所有的内存块了。这有两个优点,第一,它可以预防内存泄露的错误;第二,分配内存块的开销相对变低了。从某种意义上说,内存池迫使你遵循面向会话编程,一个内存池就是一种会话内容,这样,处于同一个内存池中的对象就有相同的生命周期,你可以通过控制会话内容来控制对象。在一个会话的开始,你创建了一个内存池,接着,你在内存池中创建了一些对象,你不需要去关心这些对象的生命周期,最后,在会话结束的时候,你只需要将那个内存池销毁就可以了。

注:通常,对象生命周期控制是程序开发最困难的部分,因此,针对这个问题还存在有一些技术,例如智能指针,垃圾回收机制等等。需要注意,同时使用这些技术有一定的难度,内存池也是这其中的一项技术,所以你不得不非常小心的使用它们。

注:在将来,libapr的内存池将变得不再那么重要。参见http://mail-archives.apache.org/mod_mbox/apr-dev/200502.mbox/%3c1f1d9820502241330123f955f@mail.gmail.com%3e.

下面有三个基本的API函数:

/* 摘自 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);

我们使用apr_pool_create()函数创建一个内存池,这个内存池将一直存活,直到你调用apr_pool_destroy()函数以后被销毁。apr_pool_create()的第一个参数是一个结果输出参数,是一个新创建的apr_pool_t类型的内存池对象。通过调用apr_palloc()来申请指定大小的内存块,具体使用方法见mp-sample.c。

/* 摘自 mp-sample.c */

apr_pool_t *mp;

/* 创建内存池 */

apr_pool_create(&mp, NULL);

/* 从内存池中分配内存块 */

char *buf1;

buf1 = apr_palloc(mp, MEM_ALLOC_SIZE);

简单地说,我们可以像使用malloc(3)这样使用apr_palloc(),也可以调用apr_pcalloc(),正如你猜到的,apr_pcalloc类似于calloc(3),apr_pcalloc返回一个被0填充了的内存块。假如你使用了malloc(3)/calloc(3),你需要调用free(3)来释放分配了的内存。但是在内存池中,你必不需要释放每个内存块,你只需要对该内存池调用apr_poll_destroy()函数从而释放所有的内存块。

注:使用apr_palloc()申请内存,其内存块的大小没有限制,然而,在内存池中申请大内存并不是什么好主意。内存池本质上是为了更小的内存块而设计的,实际上,初始的内存池的大小是8000字节。如果你需要申请超过几兆字节的内存块时,那么就不要使用内存池。

注:默认情况下,内存池管理器从不将申请到的内存归还给系统。如果程序要运行很长时间,这将是一个问题,推荐像下面的代码那样指定一个上限:

/* 设置上限,让内存池管理器释放内存,将内存返回给系统的示例代码 */

#define YOUR_POOL_MAX_FREE_SIZE 32 /* apr_pool max free list size */

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

}

这儿有两个API函数需要知道,一个是apr_pool_clear(),另一个是apr_pool_cleanup_register(),apr_pool_clear()类似于apr_pool_destroy(),不同的是内存池将一直存在。示例代码如下:

/* 使用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);

}

apr_pool_destroy(mp);

do_operation()里使用了内存池,分配了一些内存块。假如在do_operation()之外不需要这些内存块了,可以调用apr_pool_clear()函数,这样能缩小内存的使用大小。如果你熟悉系统的栈内存的话,你会觉得内存池与栈内存一样,调用apr_palloc只是如同移动SP(栈指针),调用apr_pool_clear()如同重置SP,两者都是轻量级的操作。

使用apr_pool_cleanup_register()函数,可以在内存池清空/销毁上设定一个钩子(回调)函数,在内存池清空或是销毁后调用这个函数,在这个回调函数中,你可以实现任何在内存池上的结束代码。

关于内存池的最后一个主题是子池,每个内存池都可以有一个父内存池,因此,内存池构造了树。apr_pool_create()的第二个参数表示父内存池,当这个参数为NULL时,新创建的内存池将变为一个根内存池,可以在这个根内存池上创建子内存池。在这个树中对一个内存池调用apr_pool_destroy()函数,则该内存池的子内存池也将被销毁;当对该内存池调用apr_pool_clear()函数,则这个内存池存在但是它的子内存池将被销毁,上面提及到的那些清除函数,在子内存池销毁时被调用。

注:当将NULL值做为清除回调函数时将会产生一个bug,你必须像下面的代码那样传入apr_pool_cleanup_null:

/* 关于内存池典型bug的伪代码 */

/* apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, NULL); 这将产生一个bug */

/* 修正: */

apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, apr_pool_cleanup_null);

4.错误状态(apr_status_t)

大多数libapr函数会返回apr_status_t值,apr_status_t值可以是APR_SUCCESS或其他值,APR_SUCCESS表示成功,典型的代码如下:

/* 测试apr_status_s值的伪代码 */

apr_status_t rv;

rv = apr_pool_create(&mp, NULL);

if (rv != APR_SUCCESS) {

ERROR-HANDLING;

}

libapr定义了一些诸如APR_EINVAL的错误状态和APR_STATUS_IS_ENOMEM()的错误检测宏,在处理代码的可移植性上这些宏非常有用,例如APR_STATUS_IS_EAGAIN()宏。以前,EAGAIN和EWOULDBLOCK这两个错误码表示相同的含义,用APR_STATUS_IS_EAGAIN()宏可以区分它们从而解决这个问题。

但是,仅仅靠这些去处理大量操作系统上可能出现的所有的错误情况是不太可能的,libapr并没有彻底改变错误处理的规则。libapr非常简单:

* 成功时,返回APR_SUCCESS

* 对于libapr层上的错误,返回APR_FOO错误码

* 对于一般的操作系统错误,返回APR_FOO错误码

* 对于大多数依赖于操作系统的错误,返回错误系统错误码

我推荐你遵循下面简单的规则:

* 用APR_SUCCESS与返回值比较;

* 如果你想知道更详细的错误,则用其他错误码跟返回值相比较

你应该知道apr_strerror()函数,你可以用这个函数像下面那样显示错误描述:

/* apr_strerror()伪代码 */

apr_status_t rv;

rv = apr_foo_bar();

if (rv != APR_SUCCESS) {

char errbuf[256];

apr_strerror(rv, buf, sizeof(buf));

puts(errbuf); /* 显示错误描述 */

}

5.文件处理

当我们要处理文件时,我们必需先调用apr_file_open()函数,这儿是它的原型声明:

/* 摘自apr_file_io.h */

APR_DECLARE(apr_status_t) apr_file_open(apr_file_t **newf, const char *fname,

apr_int32_t flag, apr_fileperms_t perm,

apr_pool_t *pool);

第一个参数是结果参数,其类型为apr_file_t**,即通过调用apr_file_open()来创建一个apr_file_t对象。第二

个参数是文件路径,第三个参数是位标志,在apr_file_io.h文件中定义。第四个参数是对新创建的文件设置的访问权限,其值是也是位标志,在apr_file_io.h中定义。例如,如果你想创建一个访问权限为0600的文件,只有文件拥有者可以读写它,那么你可以指定其值为APR_UREAD|APR_UWRITE。在通常情况下,文件访问权限的值你可以设置为APR_OS_DEFAULT。第五个参数是要使用的内存池,毋庸置疑,你得使用apr_pool_create()函数去事先创建它。

打开文件后,我们就可以使用其他的API函数来处理文件了,这些API函数可以在apr_file_io.h文件中找到。基本的有apr_file_read()和apr_file_write()函数,如你所想,apr_file_read()使我们可以从文件中读取一些东西,而apr_file_write()允许我们向文件写入一些东西,其函数原型声明如下:

/* 摘自apr_file_io.h */

APR_DECLARE(apr_status_t) apr_file_read(apr_file_t *thefile, void *buf,

apr_size_t *nbytes);

APR_DECLARE(apr_status_t) apr_file_write(apr_file_t *thefile, const void *buf,

apr_size_t *nbytes);

这两个函数的第三个参数均为输入输出参数,意思是说,在调用函数时我们指定它的值为输入值的长度,而在函数返回时它的值为结果的长度。具体而言,apr_file_read()返回的是读取字节的长度,apr_file_write()返回的是写入字节的长度。这儿有一个例子:

/* 关于apr_file_write()的伪代码 */

strcpy(outbuf, "123456789");

apr_size_t outlen = strlen(outbuf);

rv = apr_file_write(fp, outbuf, &outlen);

printf("apr_file_write() rv = %d, nbytes = %d\n", rv, outlen);

这个例子中,在调用apr_file_write()之前,变量'outlen'的值是9,通过使用&outlen这样的形式作为第三个参数,告诉apr_file_write()函数可写的长度是9,当apr_file_write()函数返回时,'outlen'的值变为实际写入的长度。如果是本地文件,它的值常常是9,理论上,它的值可以变小(例如当磁盘满的时候)。

我们只能调用apr_file_close()函数去关闭文件,除此之外,也可以通过销毁apr_file_open()函数相关的内存池来实现文件的关闭。我比较喜欢显式地调用apr_file_close()函数去关闭文件,不过这仅仅是我的意见。

注:在不同的libapr版本之间存在着一些源程序的兼容性问题,apr_file_open()的第三个参数自libapr 1.1.0版本这后加上了APR_FOPEN_前缀,但是之前的版本却没有这个前缀。可以使用APR_FOPEN_CREATE代替APR_CREATE,请查阅你使用的libapr版本的apr_file_io.h文件。同样地,apr_file_open()的第四个参数自libapr 1.1.0版本之后,添加了APR_FPROT_前缀。

注:这儿还有一个兼容性的问题是关于文件路径分割符的,Unix(POSIX)系统使用斜线('/'),MS-Windows使用反斜线('\')作为分割符。如果你要写一个能在Unix和MS-Windows系统上都能运行的应用程序,我推荐你统一地在文件路径中使用斜线('/')作为分割符,因为MS-Windows中可以接受识别它。

注:特别注意apr_file_gets()的用法,没有APR_BUFFERED的apr_file_gets()调用而将很大的影响效率,这是因为apr_file_gets()内部将对每一个字节都会调用apr_file_read()。记住要使用apr_file_gets()时,打开文件必须加上APR_BUFFERED标志。

在以下的情况我不推荐你使用APR_BUFFERED标志:

1.当你使用mmap映射一个文件时(因为当mmap一个APR_BUFFERED文件时会产生错误)

2.没有读/写(例如仅仅是为了锁住一个文件)

3.你能确定你使用一个大内存缓冲区来读/写文件。

注:当使用APR_BUFFERED标志打开了一个文件并且对一个文件调用apr_file_trunc()函数时,你必须在调

用apr_file_trunc()之前调用apr_file_flush()之前,否则,文件将被损坏。

注: 当使用APR_BUFFERED标志打开了一个文件并且这个文件被多个线程共享,那么APR_XTHREAD标志也是必需的。不幸的是,APR_XTHREAD标志在Windows系统有另一面的影响,我的经验告诉我不要在Windows系统中使用APR_XTHREAD标志。

可以得到文件的各种信息,例如大小,时间,拥有者,访问权限等等,这些信息存放在apr_finfo_t结构体中,apr_finfo_t在文件apr_file_info.h中有定义。这里有两个相关的API函数:

/* 摘自 apr_file_io.h */

APR_DECLARE(apr_status_t) apr_file_info_get(apr_finfo_t *finfo, apr_int32_t wanted, apr_file_t *thefile);

/* 摘自 apr_file_info.h */

APR_DECLARE(apr_status_t) apr_stat(apr_finfo_t *finfo, const char *fname, apr_int32_t wanted, apr_pool_t *pool);

apr_file_info_get()函数要求apr_file_t对象,apr_stat()函数要求文件路径。如果已经打开了文件并且生成了apr_file_t对象,那么最好使用apr_file_info_get();否则,可以调用apr_stat()。与其他类型不同,apr_finfo_t是一个完整类型,这两个api函数并没有生成该对象,我们只能显式地创建它并为其分配内存。典型地,它在当前栈中创建,这是因为我们想知道一些诸如文件大小和时间的属性。注意如apr_finfo_t::fname等一些内存,如果是创建在内存池中的,这将产生一些错误。请查看finfo-sample.c文件,了解它的用法。

有一些文件处理函数是在文件名基础上运行的,例如apr_file_remove()和apr_file_copy(),它们在apr_file_io.h和arp_file_info.h文件中能找到。

注:一些API函数带有"wanted"参数, 通过指定该参数的位标志得到文件的属性。 这些函数是apr_dir_read(), apr_stat(), apr_lstat()和apr_file_info_get()。注意"wanted"参数的值将会超出操作系统的文件系统的支持,在这种情况下,该API函数将返回APR_INCOMPLETE。

6.文件锁

当我们想在进程间锁定一个文件时,在Unix系统上有很多混杂的关于文件锁定的API函数供使用,而现在在libapr中我们可以统一地使用apr_file_lock()函数。libapr这样仅提供一个简单的API来实现文件锁显得特别有用。

/* 摘自 apr_file_io.h */

APR_DECLARE(apr_status_t) apr_file_lock(apr_file_t *thefile, int type);

APR_DECLARE(apr_status_t) apr_file_unlock(apr_file_t *thefile);

apr_file_lock()有两个参数,一个是apr_file_t对象,另一个指定锁类型的标志。锁类型要么是APR_FLOCK_SHARED,要么是APR_FLOCK_EXCLUSIVE。前者表示是一个可读锁,后者表示是一个可写锁。对一个文件进行解锁,我们可以调用apr_file_unlock函数,或调用apr_file_close()来实现文件的解锁。

另外,我们可以使用或位标志APR_FLOCK_NONBLOCK,没有APR_FLOCK_NONBLOCK标志,调用apr_file_lock则会发生阻塞,使用APR_FLOCK_NONBLOCK后,如果apr_file_lock()不能锁文件,则会返回一个错误码APR_EAGAIN。

用APR_SUCCESS与apr_file_lock()函数的返回值进行比较,如果此函数的返回值是APR_SUCCESS,则文件被成功的锁定;否则,锁定文件失败。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: