Apache APR 常用工具与 API 详解
Apache APR 常用工具与 API 详解:APR 以 C 库形态提供,无独立命令行工具,开发期依赖 apr-1-config/apu-1-config/pkg-config 三个工具;按模块速查内存池、字符串、文件 I/O、UUID、错误处理、锁与线程、网络套接字 API,均附实测代码示例与运行回显,全程实测。
Apache APR 常用工具与 API 详解
目标:速查 Apache APR 1.7.6 / apr-util 1.6.5 的常用工具与核心 API。APR 以 C 库形态提供,没有独立命令行工具,开发期依赖 apr-1-config / apu-1-config / pkg-config 三个参数查询工具;本文按模块分类速查内存池、字符串、文件 I/O、UUID、错误处理、锁与线程、网络套接字等 API,每类均附已在 Ubuntu 22.04.5 实测的代码示例与运行回显。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(VERSION_CODENAME=jammy)
- 架构:x86_64(内核 6.8.0-138-generic)
- 编译器:gcc 11.4.0
- APR 版本:apr-1.7.6 + apr-util-1.6.5(安装前缀
/usr/local/apr,头文件目录/usr/local/apr/include/apr-1,共 76 个头文件) - 示例工程:
/root/apr-demo/ - 演示账号:root
二、工具概览
2.1 开发期工具
APR 无 apr 命令行可执行程序,全部使用通过以下三个工具与你的构建系统对接:
| 工具 | 位置 | 作用 |
|---|---|---|
| apr-1-config | /usr/local/apr/bin/apr-1-config | 查询 APR 编译/链接参数(cflags、includes、link-ld 等) |
| apu-1-config | /usr/local/apr/bin/apu-1-config | 查询 apr-util 链接参数(uuid、dbd、xml 等 API 的库) |
| pkg-config | 系统(配 PKG_CONFIG_PATH=/usr/local/apr/lib/pkgconfig) | 以 .pc 元数据方式查询同一套参数 |
2.2 模块 API 总览
| 模块 | 头文件 | 代表 API | 依赖库 |
|---|---|---|---|
| 全局初始化 | apr_general.h | apr_initialize / apr_terminate / apr_strerror | libapr-1 |
| 内存池 | apr_pools.h | apr_pool_create / apr_pool_destroy / apr_palloc | libapr-1 |
| 字符串 | apr_strings.h | apr_pstrcat / apr_psprintf / apr_pstrdup | libapr-1 |
| 文件 I/O | apr_file_io.h / apr_file_info.h | apr_file_open / read / write / close | libapr-1 |
| UUID | apr_uuid.h | apr_uuid_get / apr_uuid_format / apr_uuid_parse | libaprutil-1 |
| 错误码 | apr_errno.h | APR_SUCCESS / APR_ENOENT / apr_strerror | libapr-1 |
| 锁与线程 | apr_thread_mutex.h / apr_thread_proc.h | apr_thread_mutex_create / apr_thread_create | libapr-1 |
| 网络套接字 | apr_network_io.h | apr_socket_create / bind / listen / connect / accept | libapr-1 |
apr-util 还提供 apr_base64、apr_date、apr_dbd、apr_dbm、apr_memcache、apr_reslist、apr_uri 等扩展头文件,按需引入。
三、前置条件
已按《Apache APR 安装指南》完成安装并注册共享库。编译模板(参数全部由工具查询,不手工拼写):
gcc app.c -o app $(/usr/local/apr/bin/apr-1-config --cflags --cppflags --includes --link-ld --libs)
涉及 apr-util API(如 UUID)时追加 apu-1-config 输出:
gcc app.c -o app $(/usr/local/apr/bin/apr-1-config --cflags --cppflags --includes --link-ld) $(/usr/local/apr/bin/apu-1-config --link-ld --libs)
四、内存池 API(apr_pools.h)
内存池是 APR 一切资源管理的基石:从池上分配的内存随池销毁一次性释放,杜绝逐个 free 与泄漏。固定开场三步:apr_initialize 全局初始化、atexit(apr_terminate) 收尾、apr_pool_create 建根池。
| API | 签名要点 | 说明 |
|---|---|---|
apr_initialize() | 无参,返回 apr_status_t | 全局初始化(进程一次) |
apr_terminate() | 无参 | 全局清理(配合 atexit 注册) |
apr_pool_create(&pool, NULL) | 宏,第二参为父池(NULL 即根池) | 创建内存池(内部走 apr_pool_create_ex) |
apr_palloc(pool, size) | 返回 void * | 池上分配内存 |
apr_pool_destroy(pool) | 无返回值 | 销毁池并回收其上全部内存 |
apr_pool_create_unmanaged_ex(...) | apr_pools.h 中声明 | 创建不受父池管理的不托管池 |
实测示例(demo_pool.c 核心片段):
rv = apr_pool_create(&pool, NULL);
char *msg = apr_pstrcat(pool, "Hello, ", "APR ", "pool!", NULL);
void *buf = apr_palloc(pool, 128);
apr_pool_destroy(pool);
完整源码、编译命令与运行回显见《Apache APR 构建并运行示例应用指南》第四章,运行输出(实测):
Hello, APR pool!
psprintf demo: 2 + 3 = 5
pstrdup equals msg: yes
apr_palloc(128) OK, pool addr=0x5641976153b8
五、字符串 API(apr_strings.h)
字符串 API 全部「池上分配」,返回值无需 free:
| API | 签名要点 | 说明 |
|---|---|---|
apr_pstrdup(pool, s) | apr_strings.h 中声明 | 池上复制字符串 |
apr_pstrcat(pool, ...) | 变参,以 NULL 结尾 | 池上拼接多个字符串 |
apr_psprintf(pool, fmt, ...) | printf 风格 | 池上格式化生成字符串 |
实测示例(demo_pool.c):
char *msg = apr_pstrcat(pool, "Hello, ", "APR ", "pool!", NULL);
char *line = apr_psprintf(pool, "psprintf demo: %d + %d = %d", 2, 3, 5);
char *copy = apr_pstrdup(pool, msg);
实测输出:
Hello, APR pool!
psprintf demo: 2 + 3 = 5
pstrdup equals msg: yes
⚠️ 注意:
apr_pstrcat是变参函数,最后一个参数必须是 NULL,否则行为未定义。
六、文件 I/O API(apr_file_io.h / apr_file_info.h)
6.1 核心 API
| API | 签名要点 | 说明 |
|---|---|---|
apr_file_open(&fp, fname, flag, perm, pool) | apr_file_io.h 中声明(flag 为 apr_int32_t,perm 为 apr_fileperms_t) | 打开/创建文件 |
apr_file_read(fp, buf, &nbytes) | 第三参出入参:传入容量、返回实际读到的字节数 | 读文件 |
apr_file_write(fp, buf, &nbytes) | 同上,返回实际写入字节数 | 写文件 |
apr_file_close(fp) | 无返回值 | 关闭文件 |
apr_file_info_get(&finfo, wanted, fp) | 配合 apr_finfo_t 取元信息 | 取文件大小等 |
apr_file_rename(from, to, pool) | 无返回值 | 重命名 |
apr_file_remove(path, pool) | 无返回值 | 删除文件 |
6.2 打开标志与权限
打开标志推荐新写法 APR_FOPEN_*;旧写法 APR_READ / APR_WRITE / APR_CREATE / APR_APPEND / APR_TRUNCATE 在 apr_file_io.h 中以兼容别名形式 define 为对应 APR_FOPEN_*,两者等价。权限常量 APR_OS_DEFAULT(即 APR_FPROT_OS_DEFAULT,头文件中已标注 @deprecated,新代码推荐 APR_FPROT_OS_DEFAULT)表示沿用系统默认权限。常用标志组合:
| 标志组合 | 语义 |
|---|---|
APR_CREATE | APR_WRITE | APR_TRUNCATE | 创建或截断写入(等同 C 的 "w") |
APR_READ | 只读(等同 "r") |
APR_CREATE | APR_WRITE | APR_APPEND | 追加写入(等同 "a") |
6.3 实测示例(demo_file.c 核心片段)
/* 以创建|写入|截断方式打开并写入 */
rv = apr_file_open(&fp, path, APR_CREATE | APR_WRITE | APR_TRUNCATE,
APR_OS_DEFAULT, pool);
const char *text = "Apache APR file I/O demo.\n第二行:UTF-8 中文内容。\n";
apr_size_t len = strlen(text);
rv = apr_file_write(fp, text, &len); /* len 返回实际写入 60 */
/* 取文件大小并读回 */
apr_finfo_t finfo;
apr_file_info_get(&finfo, APR_FINFO_SIZE, fp);
apr_size_t nread = sizeof(buf) - 1;
rv = apr_file_read(fp, buf, &nread); /* nread 返回实际读取字节数 */
实测输出:
apr_file_write wrote 60 bytes
file size: 60 bytes
read 60 bytes:
Apache APR file I/O demo.
第二行:UTF-8 中文内容。
old path gone after rename (rv=2)
七、UUID API(apr_uuid.h)
UUID 属于 apr-util,编译时需追加 apu-1-config 的链接参数(libaprutil-1):
| API | 签名要点 | 说明 |
|---|---|---|
apr_uuid_get(&uuid) | APU_DECLARE(void),填 apr_uuid_t | 生成新 UUID(二进制结构) |
apr_uuid_format(buffer, &uuid) | buffer 至少 37 字节 | 格式化为 36 字符标准字符串 |
apr_uuid_parse(&uuid, uuid_str) | 返回 apr_status_t | 把字符串解析回二进制结构 |
APR_UUID_FORMATTED_LENGTH | 宏,值为 36 | 格式化字符串长度(不含结尾 0) |
实测示例(demo_uuid.c 核心片段):
apr_uuid_t uuid, parsed;
char buf[APR_UUID_FORMATTED_LENGTH + 1]; /* 36 字符 + 结尾 0 */
apr_uuid_get(&uuid);
apr_uuid_format(buf, &uuid);
rv = apr_uuid_parse(&parsed, buf); /* 解析回结构,可再 format 验证一致 */
实测运行两次(UUID 各不相同,parse+format 往返一致):
# ./demo_uuid
uuid#1: e4b33c28-c5d6-11f1-b461-8781a5106fe2
uuid#2: e4b33d90-c5d6-11f1-b461-8781a5106fe2
parse+format roundtrip: match (rv=0, len=36)
# ./demo_uuid
uuid#1: e4b3c67a-c5d6-11f1-a321-193684b9e0dc
uuid#2: e4b3c80a-c5d6-11f1-a321-193684b9e0dc
parse+format roundtrip: match (rv=0, len=36)
八、错误处理约定(apr_errno.h)
APR 全部 API 返回 apr_status_t:0 表示成功,非 0 为错误码(优先映射 OS 错误码)。统一用 apr_strerror 转人话:
| 常量/函数 | 值/签名要点 | 说明 |
|---|---|---|
APR_SUCCESS | 宏,值为 0 | 成功 |
APR_ENOENT | Linux 上映射系统错误码 ENOENT | 文件不存在(实测 rv=2) |
apr_strerror(rv, errbuf, sizeof(errbuf)) | 填充错误描述缓冲区 | 状态码转可读描述 |
标准错误处理写法(demo_pool.c,实测):
rv = apr_initialize();
if (rv != APR_SUCCESS) {
char errbuf[256];
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_initialize failed: %s\n", errbuf);
return 1;
}
九、锁与线程 API(apr_thread_proc.h / apr_thread_mutex.h)
9.1 核心 API
| API | 头文件位置 | 说明 |
|---|---|---|
apr_thread_create(&thread, attr, func, data, pool) | apr_thread_proc.h | 创建线程;func 签名 void *(APR_THREAD_FUNC *)(apr_thread_t *, void *) |
apr_thread_join(&rv, thread) | apr_thread_proc.h | 等待线程结束并取回返回状态 |
apr_proc_create(...) | apr_thread_proc.h | 创建子进程 |
apr_thread_mutex_create(&mutex, flags, pool) | apr_thread_mutex.h | 创建互斥锁(常用 APR_THREAD_MUTEX_DEFAULT) |
apr_thread_mutex_lock / unlock / trylock | apr_thread_mutex.h | 加锁 / 解锁 / 尝试加锁 |
apr_thread_mutex_destroy(mutex) | apr_thread_mutex.h | 销毁锁 |
9.2 实测示例:4 线程 + 互斥锁(demo_mutex.c)
4 个线程各对共享计数器累加 10000 次,互斥锁保护下结果精确无竞争。完整源码:
/* demo_mutex.c:apr_thread_create 多线程 + apr_thread_mutex 互斥锁示例 */
#include <stdio.h>
#include <stdlib.h>
#include <apr_general.h>
#include <apr_pools.h>
#include <apr_thread_proc.h>
#include <apr_thread_mutex.h>
#define WORKER_COUNT 4
#define INC_PER_WORKER 10000
static apr_thread_mutex_t *lock;
static long long counter = 0;
static void *APR_THREAD_FUNC worker(apr_thread_t *thd, void *data)
{
int i;
for (i = 0; i < INC_PER_WORKER; i++) {
apr_thread_mutex_lock(lock);
counter++;
apr_thread_mutex_unlock(lock);
}
return NULL;
}
int main(int argc, const char *argv[])
{
apr_initialize();
atexit(apr_terminate);
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
/* 创建默认类型互斥锁 */
apr_thread_mutex_create(&lock, APR_THREAD_MUTEX_DEFAULT, pool);
/* 创建 4 个工作线程,各累加 10000 次 */
apr_thread_t *threads[WORKER_COUNT];
for (int i = 0; i < WORKER_COUNT; i++) {
apr_status_t rv = apr_thread_create(&threads[i], NULL, worker, NULL, pool);
if (rv != APR_SUCCESS) {
fprintf(stderr, "apr_thread_create failed\n");
return 1;
}
}
/* join 等待全部线程结束 */
for (int i = 0; i < WORKER_COUNT; i++) {
apr_status_t rv;
apr_thread_join(&rv, threads[i]);
}
printf("expect %d workers x %d inc = %d\n", WORKER_COUNT, INC_PER_WORKER,
WORKER_COUNT * INC_PER_WORKER);
printf("counter = %lld (%s)\n", counter,
counter == (long long)WORKER_COUNT * INC_PER_WORKER ? "mutex works" : "RACE!");
apr_thread_mutex_destroy(lock);
apr_pool_destroy(pool);
return 0;
}
编译与连续三次运行(实测回显,退出码 0):
cd /root/apr-demo
gcc demo_mutex.c -o demo_mutex $(/usr/local/apr/bin/apr-1-config --cflags --cppflags --includes --link-ld --libs)
./demo_mutex
# ./demo_mutex
expect 4 workers x 10000 inc = 40000
counter = 40000 (mutex works)
# ./demo_mutex
expect 4 workers x 10000 inc = 40000
counter = 40000 (mutex works)
# ./demo_mutex
expect 4 workers x 10000 inc = 40000
counter = 40000 (mutex works)
十、网络 API(apr_network_io.h)
10.1 核心 API
| API | 头文件位置 | 说明 |
|---|---|---|
apr_sockaddr_info_get(&sa, host, family, port, flags, pool) | apr_network_io.h | 解析主机名/地址为 apr_sockaddr_t |
apr_socket_create(&sock, family, type, protocol, pool) | apr_network_io.h | 创建套接字(type 用系统 SOCK_STREAM,protocol 用 APR_PROTO_TCP) |
apr_socket_bind(sock, sa) | apr_network_io.h | 绑定地址 |
apr_socket_listen(sock, backlog) | apr_network_io.h | 监听 |
apr_socket_accept(&newsock, sock, pool) | apr_network_io.h | 接受连接 |
apr_socket_connect(sock, sa) | apr_network_io.h | 发起连接 |
apr_socket_send / recv | apr_network_io.h | 收发数据(第三参 apr_size_t 出入参,同文件 I/O) |
apr_socket_opt_set(sock, opt, on) | apr_network_io.h | 设置选项(如 APR_SO_REUSEADDR) |
apr_socket_close(sock) | apr_network_io.h | 关闭套接字 |
10.2 实测示例:回环 TCP 收发(demo_net.c)
同一进程内完成 bind/listen(服务端)、connect(客户端)、accept、双向收发,监听 127.0.0.1:18080。完整源码:
/* demo_net.c:apr_socket 回环 TCP 通信示例(同一进程内 listen/connect/accept/send/recv) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <apr_general.h>
#include <apr_pools.h>
#include <apr_network_io.h>
#define TEST_PORT 18080
int main(int argc, const char *argv[])
{
apr_initialize();
atexit(apr_terminate);
apr_pool_t *pool;
apr_pool_create(&pool, NULL);
apr_status_t rv;
char errbuf[256];
/* 1) 构造 127.0.0.1:18080 地址,创建监听 socket 并 bind/listen */
apr_sockaddr_t *sa;
rv = apr_sockaddr_info_get(&sa, "127.0.0.1", APR_INET, TEST_PORT, 0, pool);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_sockaddr_info_get failed: %s\n", errbuf);
return 1;
}
apr_socket_t *lsn, *conn, *cli;
rv = apr_socket_create(&lsn, sa->family, SOCK_STREAM, APR_PROTO_TCP, pool);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_socket_create(listen) failed: %s\n", errbuf);
return 1;
}
apr_socket_opt_set(lsn, APR_SO_REUSEADDR, 1);
rv = apr_socket_bind(lsn, sa);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_socket_bind failed: %s\n", errbuf);
return 1;
}
rv = apr_socket_listen(lsn, 5);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_socket_listen failed: %s\n", errbuf);
return 1;
}
/* 2) 创建客户端 socket 并 connect 监听端口 */
apr_socket_create(&cli, sa->family, SOCK_STREAM, APR_PROTO_TCP, pool);
rv = apr_socket_connect(cli, sa);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_socket_connect failed: %s\n", errbuf);
return 1;
}
printf("client connected to 127.0.0.1:%d\n", TEST_PORT);
/* 3) 服务端 accept 取得连接;客户端发送一条数据 */
rv = apr_socket_accept(&conn, lsn, pool);
if (rv != APR_SUCCESS) {
apr_strerror(rv, errbuf, sizeof(errbuf));
fprintf(stderr, "apr_socket_accept failed: %s\n", errbuf);
return 1;
}
const char *msg = "hello from apr_socket client";
apr_size_t len = strlen(msg);
rv = apr_socket_send(cli, msg, &len);
printf("client sent %ld bytes\n", (long)len);
/* 4) 服务端接收并打印 */
char buf[128];
apr_size_t nrecv = sizeof(buf) - 1;
rv = apr_socket_recv(conn, buf, &nrecv);
buf[nrecv] = 0;
printf("server recv %ld bytes: %s\n", (long)nrecv, buf);
/* 5) 服务端回写应答,客户端接收 */
const char *reply = "ack from apr_socket server";
len = strlen(reply);
apr_socket_send(conn, reply, &len);
nrecv = sizeof(buf) - 1;
rv = apr_socket_recv(cli, buf, &nrecv);
buf[nrecv] = 0;
printf("client recv %ld bytes: %s\n", (long)nrecv, buf);
apr_socket_close(cli);
apr_socket_close(conn);
apr_socket_close(lsn);
apr_pool_destroy(pool);
return 0;
}
编译与连续两次运行(实测回显,退出码 0;hello from apr_socket client 为 28 字节、ack from apr_socket server 为 26 字节):
cd /root/apr-demo
gcc demo_net.c -o demo_net $(/usr/local/apr/bin/apr-1-config --cflags --cppflags --includes --link-ld --libs)
./demo_net
# ./demo_net
client connected to 127.0.0.1:18080
client sent 28 bytes
server recv 28 bytes: hello from apr_socket client
client recv 26 bytes: ack from apr_socket server
# ./demo_net
client connected to 127.0.0.1:18080
client sent 28 bytes
server recv 28 bytes: hello from apr_socket client
client recv 26 bytes: ack from apr_socket server
十一、注意事项
- 任何使用 APR 的程序都要先
apr_initialize()并注册atexit(apr_terminate),再创建内存池;线程与 socket 等 API 都要求传入关联的池。 - 字符串与内存分配一律走池(apr_pstrcat / apr_palloc),不要与 malloc/free 混用同一块内存。
- UUID、DBD、DBM、XML 等 API 在 libaprutil-1 中,编译时必须追加
apu-1-config --link-ld --libs,否则链接期 undefined reference。 apr_file_read/apr_socket_recv的字节数参数是出入参:传入缓冲区容量、返回实际字节数,循环读取时要更新剩余容量。- 打开标志新代码统一用
APR_FOPEN_*(APR_READ等旧名是兼容别名);权限常量推荐APR_FPROT_OS_DEFAULT(APR_OS_DEFAULT已标注 @deprecated)。
十二、常见问题(FAQ)
12.1 链接报 undefined reference to apr_uuid_get
Q: 编译 UUID 示例报符号未定义?
A: 该符号在 libaprutil-1 中,只链 libapr-1 不够。链接参数追加 apu-1-config 输出即可:
gcc demo_uuid.c -o demo_uuid $(/usr/local/apr/bin/apr-1-config --cflags --cppflags --includes --link-ld) $(/usr/local/apr/bin/apu-1-config --link-ld --libs)
12.2 运行报 error while loading shared libraries: libapr-1.so.0
Q: 编译通过、运行时报共享库找不到?
A: 运行期库路径未注册。一句话解决:
echo "/usr/local/apr/lib" > /etc/ld.so.conf.d/apr.conf && ldconfig
完整「ldd 定位 → 注册 → 验证」排障实录见《Apache APR 构建并运行示例应用指南》第八章。
12.3 apr_pstrcat 拼接结果乱码或崩溃
Q: 用 apr_pstrcat 拼接后字符串内容异常甚至崩溃?
A: 最常见原因是漏掉了结尾的 NULL 参数。apr_pstrcat 是变参函数,依赖 NULL 哨兵判断参数结束:
char *msg = apr_pstrcat(pool, "Hello, ", "APR ", "pool!", NULL); /* 末参必须是 NULL */
12.4 线程示例的 counter 为什么恰好是 40000
Q: 多线程累加结果每次都是 40000,没有竞争吗?
A: 有竞争,但被互斥锁串行化了:每次 counter++ 都在 apr_thread_mutex_lock 与 apr_thread_mutex_unlock 之间完成,4 线程 × 10000 次精确等于 40000。若去掉锁,结果将小于 40000 且每次不同(这也是验证锁生效的对照实验方法)。
12.5 socket 示例为什么能先 connect 再 accept
Q: 同一进程里客户端 connect 在服务端 accept 之前执行,为什么没有失败?
A: TCP 三次握手由内核协议栈完成:apr_socket_listen 之后内核即用 backlog 队列(示例中为 5)缓存已完成握手的连接,apr_socket_connect 立即成功;apr_socket_accept 随后从队列取出连接。这是演示 APR socket API 全流程的简化写法,生产代码中服务端与客户端通常分属两个进程。
十三、参考链接
- APR 1.7 API 文档(按模块索引):https://apr.apache.org/docs/apr/1.7/modules.html
- apr-util 1.6 API 文档:https://apr.apache.org/docs/apr-util/1.6/modules.html
- 内存池设计说明:https://apr.apache.org/docs/apr/1.7/group__apr__pools.html
- 安装与编译参数:《Apache APR 安装指南》《Apache APR 配置参数详解》
- 示例完整编译与排障:《Apache APR 构建并运行示例应用指南》