Apache APR 常用工具与 API 详解

Apache APR 常用工具与 API 详解:APR 以 C 库形态提供,无独立命令行工具,开发期依赖 apr-1-config/apu-1-config/pkg-config 三个工具;按模块速查内存池、字符串、文件 I/O、UUID、错误处理、锁与线程、网络套接字 API,均附实测代码示例与运行回显,全程实测。

Apache APRApache APR2026/10/12

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.hapr_initialize / apr_terminate / apr_strerrorlibapr-1
内存池apr_pools.hapr_pool_create / apr_pool_destroy / apr_palloclibapr-1
字符串apr_strings.hapr_pstrcat / apr_psprintf / apr_pstrduplibapr-1
文件 I/Oapr_file_io.h / apr_file_info.hapr_file_open / read / write / closelibapr-1
UUIDapr_uuid.hapr_uuid_get / apr_uuid_format / apr_uuid_parselibaprutil-1
错误码apr_errno.hAPR_SUCCESS / APR_ENOENT / apr_strerrorlibapr-1
锁与线程apr_thread_mutex.h / apr_thread_proc.hapr_thread_mutex_create / apr_thread_createlibapr-1
网络套接字apr_network_io.hapr_socket_create / bind / listen / connect / acceptlibapr-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_ENOENTLinux 上映射系统错误码 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 / trylockapr_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 / recvapr_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 全流程的简化写法,生产代码中服务端与客户端通常分属两个进程。

十三、参考链接