openwrt ubus 深入分析
🔍 ubus 深入分析
1️⃣ 架构概述
ubus 采用传统的客户端-服务器(C/S) 模型,其核心组件包括:
- ubusd 守护进程:作为服务器端,负责管理所有客户端连接、对象注册以及消息路由。
- 客户端库 (libubus):提供给应用程序使用的编程接口,方便进程创建客户端、注册对象和方法,以及调用远程方法。
- 命令行工具 (ubus):用于通过 shell 与 ubus 交互,例如列出注册的对象、调用方法等。
下图展示了 ubus 的整体架构和通信流程:
这种设计使得两个客户端之间的所有通信都必须通过 ubusd 中转。客户端之间不直接建立连接。
2️⃣ 核心数据结构
理解 ubus 的核心数据结构对于深入其工作原理至关重要。
2.1 struct ubus_context
代表一个 ubus 客户端连接上下文,每个连接到 ubusd 的客户端都维护一个此结构。
struct ubus_context {struct list_head requests; // 请求列表struct avl_tree objects; // 注册的对象树struct list_head pending; // 挂起的请求struct uloop_fd sock; // 与ubusd通信的socketuint32_t local_id; // ubusd分配的客户端IDuint16_t request_seq; // 请求序列号int stack_depth;void (*connection_lost)(struct ubus_context *ctx); // 连接断开回调// ... 其他字段
};
2.2 struct ubus_object
代表一个在 ubus 上注册的对象,该对象包含多个可供调用的方法。
struct ubus_object {struct avl_node avl; // 用于插入ubus_context的objects树const char *name; // 对象名 (e.g., "network")uint32_t id; // 由ubusd分配的对象IDconst char *path;struct ubus_object_type *type;ubus_state_handler_t subscribe_cb;bool has_subscribers;const struct ubus_method *methods; // 方法数组int n_methods; // 方法数量// ... 其他字段
};
2.3 struct ubus_method
定义了对象提供的一个具体方法及其处理函数和策略。
struct ubus_method {const char *name; // 方法名 (e.g., "restart")ubus_handler_t handler; // 方法处理回调函数unsigned long mask;const struct blobmsg_policy *policy; // 参数策略(定义期望的参数)int n_policy; // 策略条数
};
2.4 struct blobmsg_policy
用于定义方法参数的策略,指导消息的解析和验证。
struct blobmsg_policy {const char *name; // 参数名enum blobmsg_type type; // 参数类型 (e.g., BLOBMSG_TYPE_STRING)
};
2.5 struct ubus_request_data
代表一个正在处理的调用请求,通常在处理函数中用于回复调用者。
struct ubus_request_data {uint32_t object;uint32_t peer;uint16_t seq;/* internal use */bool deferred;int fd;
};
这些核心数据结构共同协作,管理客户端连接、对象注册、方法调用以及消息传递。
3️⃣ 实现机制
3.1 消息格式与序列化
ubus 使用 blobmsg(Binary Large Object Message)格式来序列化数据,这是一种二进制的、类似 JSON 格式的编码方式。消息通常包含一个消息头(struct ubus_msghdr
)和负载数据(blobmsg 格式的有效载荷)。
3.2 通信流程
- 连接管理:
- 客户端通过
ubus_connect()
或ubus_auto_connect()
连接到ubusd
的 Unix Domain Socket(默认路径为/var/run/ubus.sock
)。 - 连接成功后,
ubusd
会为客户端分配一个唯一的local_id
。
- 客户端通过
- 对象注册:
- 服务提供者客户端使用
ubus_add_object()
将其ubus_object
注册到ubusd
。 ubusd
收到注册请求后,会为对象分配一个唯一 ID,并将其存入数据库(avl_tree),以便后续查找。
- 服务提供者客户端使用
- 方法调用:
- 查找对象:调用者首先向
ubusd
发送UBUS_MSG_LOOKUP
消息,查找指定名称的对象 ID。 - 调用方法:获得对象 ID 后,调用者发送
UBUS_MSG_INVOKE
消息,其中包含对象 ID、方法名和参数(blobmsg 格式)。 - 路由与转发:
ubusd
根据对象 ID 找到对应的客户端连接,将调用请求转发给该客户端。 - 处理与回复:服务提供者客户端收到请求后,解析参数,找到对应的
ubus_method
并执行其handler
函数。处理完成后,使用ubus_send_reply()
将结果返回给ubusd
,再由ubusd
转发给原始调用者。
- 查找对象:调用者首先向
- 事件机制:
除了 RPC 调用,ubus 还提供了发布-订阅模式的事件机制。- 客户端可以使用
ubus_register_event_handler()
订阅特定事件。 - 任何客户端都可以使用
ubus_send_event()
发布一个事件,ubusd
会将该事件通知给所有订阅者。
- 客户端可以使用
4️⃣ 一个简单的 ubus 实例
下面是一个简单的 ubus 服务端和客户端示例,演示如何注册一个对象并提供方法。
4.1 服务端代码 (ubus_echo_server.c
)
这个服务端注册了一个名为 “echo” 的对象,该对象提供了一个名为 “repeat” 的方法。该方法接收一个字符串和一个数字,然后将该字符串重复数字指定的次数并返回。
#include <libubox/blobmsg_json.h>
#include <libubus.h>
#include <stdio.h>
#include <string.h>static struct ubus_context *ctx;/* 定义方法参数的策略 */
enum {ECHO_MSG,ECHO_TIMES,__ECHO_MAX
};static const struct blobmsg_policy echo_policy[] = {[ECHO_MSG] = { .name = "msg", .type = BLOBMSG_TYPE_STRING },[ECHO_TIMES] = { .name = "times", .type = BLOBMSG_TYPE_INT32 },
};/* echo方法的处理函数 */
static int ubus_echo_repeat(struct ubus_context *ctx, struct ubus_object *obj,struct ubus_request_data *req, const char *method,struct blob_attr *msg) {struct blob_attr *tb[__ECHO_MAX];char *received_msg = NULL;int times = 0;struct blob_buf b = {};char result_str[1024] = {0}; // 简单起见,固定大小int i;/* 解析传入的参数 */blobmsg_parse(echo_policy, __ECHO_MAX, tb, blob_data(msg), blob_len(msg));if (!tb[ECHO_MSG] || !tb[ECHO_TIMES]) {fprintf(stderr, "Error: Missing required parameters (msg and times).\n");return UBUS_STATUS_INVALID_ARGUMENT;}received_msg = blobmsg_get_string(tb[ECHO_MSG]);times = blobmsg_get_u32(tb[ECHO_TIMES]);if (times <= 0) {fprintf(stderr, "Error: 'times' must be a positive integer.\n");return UBUS_STATUS_INVALID_ARGUMENT;}/* 构建结果字符串 */result_str[0] = '\0';for (i = 0; i < times && strlen(result_str) < sizeof(result_str) - strlen(received_msg); i++) {strcat(result_str, received_msg);}/* 准备回复数据 */blob_buf_init(&b, 0);blobmsg_add_string(&b, "original_message", received_msg);blobmsg_add_u32(&b, "repeated_times", times);blobmsg_add_string(&b, "result", result_str);/* 发送回复 */ubus_send_reply(ctx, req, b.head);/* 释放blob_buf资源 */blob_buf_free(&b);return UBUS_STATUS_OK;
}/* 定义对象的方法列表 */
static struct ubus_method echo_methods[] = {UBUS_METHOD("repeat", ubus_echo_repeat, echo_policy),
};/* 定义对象类型 */
static struct ubus_object_type echo_obj_type =UBUS_OBJECT_TYPE("echo", echo_methods);/* 定义对象本身 */
static struct ubus_object echo_obj = {.name = "echo",.type = &echo_obj_type,.methods = echo_methods,.n_methods = ARRAY_SIZE(echo_methods),
};/* 连接断开回调 */
static void connection_lost(struct ubus_context *ctx) {fprintf(stderr, "UBUS connection lost. Exiting.\n");// 可以考虑在这里添加重连逻辑exit(1);
}int main(int argc, char *argv[]) {const char *ubus_socket = NULL; // 默认NULL表示使用默认路径int ret;/* 初始化uloop(ubus依赖的事件循环) */uloop_init();/* 连接到ubusd */ctx = ubus_connect(ubus_socket);if (!ctx) {fprintf(stderr, "Failed to connect to ubusd.\n");return 1;}ctx->connection_lost = connection_lost;/* 将socket fd加入到事件循环中监听 */ubus_add_uloop(ctx);/* 向ubusd注册我们的echo对象 */ret = ubus_add_object(ctx, &echo_obj);if (ret) {fprintf(stderr, "Failed to add object: %s\n", ubus_strerror(ret));ubus_free(ctx);return 1;}printf("Echo server object registered successfully.\n");/* 进入事件处理循环 */uloop_run();/* 清理资源 */ubus_free(ctx);uloop_done();return 0;
}
4.2 客户端代码 (ubus_echo_client.c
)
这个客户端通过命令行参数获取要发送的消息和重复次数,然后调用服务端的 “echo” 对象的 “repeat” 方法。
#include <libubox/blobmsg_json.h>
#include <libubus.h>
#include <stdio.h>static struct ubus_context *ctx;
static uint32_t echo_obj_id;
static int callback_received = 0;/* 调用结果的回调函数 */
static void echo_call_callback(struct ubus_request *req, int type, struct blob_attr *msg) {struct blob_buf *b = (struct blob_buf *)req->priv;if (!msg) {fprintf(stderr, "Error: Received empty response.\n");callback_received = 1;return;}/* 将收到的blobmsg属性解析成JSON字符串并打印 */char *json_str = blobmsg_format_json(msg, true);if (json_str) {printf("Server response:\n%s\n", json_str);free(json_str);} else {fprintf(stderr, "Error formatting response.\n");}callback_received = 1;
}int main(int argc, char *argv[]) {const char *ubus_socket = NULL;int ret;struct blob_buf b = {};unsigned int times_to_repeat;if (argc != 3) {fprintf(stderr, "Usage: %s <message> <times>\n", argv[0]);return 1;}times_to_repeat = atoi(argv[2]);if (times_to_repeat <= 0) {fprintf(stderr, "Error: 'times' must be a positive integer.\n");return 1;}/* 初始化uloop */uloop_init();/* 连接到ubusd */ctx = ubus_connect(ubus_socket);if (!ctx) {fprintf(stderr, "Failed to connect to ubusd.\n");return 1;}/* 查找echo对象的ID */ret = ubus_lookup_id(ctx, "echo", &echo_obj_id);if (ret || !echo_obj_id) {fprintf(stderr, "Failed to lookup 'echo' object: %s\n", ubus_strerror(ret));ubus_free(ctx);return 1;}/* 准备调用参数 */blob_buf_init(&b, 0);blobmsg_add_string(&b, "msg", argv[1]);blobmsg_add_u32(&b, "times", times_to_repeat);/* 发起异步调用 */ret = ubus_invoke(ctx, echo_obj_id, "repeat", b.head, echo_call_callback, &b, 3000);if (ret) {fprintf(stderr, "Failed to invoke method: %s\n", ubus_strerror(ret));blob_buf_free(&b);ubus_free(ctx);return 1;}/* 等待回调函数被执行(收到回复) */while (!callback_received) {ubus_handle_event(ctx); // 处理ubus事件}/* 清理资源 */blob_buf_free(&b);ubus_free(ctx);uloop_done();return 0;
}
4.3 编译和运行
-
编译:
在 OpenWRT 环境中,通常需要将这些源文件加入到你的软件包的 Makefile 中。如果是手动编译,需要链接ubus
、ubox
、blobmsg_json
等库:# 服务端 $(CC) -o ubus_echo_server ubus_echo_server.c -lubus -lubox -lblobmsg_json # 客户端 $(CC) -o ubus_echo_client ubus_echo_client.c -lubus -lubox -lblobmsg_json
-
运行:
- 首先确保
ubusd
正在运行(在 OpenWRT 上通常是默认运行的)。 - 然后先运行服务端:
./ubus_echo_server
- 在另一个终端运行客户端进行测试:
./ubus_echo_client "Hello " 5
- 客户端应该会输出从服务器返回的 JSON 响应。
- 首先确保
5️⃣ 常用工具与调试手段
5.1 命令行工具 ubus
ubus 自带的命令行工具是与系统交互和调试的利器。
命令 | 用途 | 示例 |
---|---|---|
list | 列出所有已注册的对象 | ubus list |
list -v | 详细列出所有对象及其方法(和方法的参数签名) | ubus list -v |
call | 调用一个对象的方法 | ubus call network.interface.wan status |
call (带参数) | 调用方法并传入参数(JSON格式) | ubus call echo repeat '{"msg":"hi", "times":3}' |
listen | 监听所有或特定的 ubus 事件 | ubus listen & |
send | 发送一个事件 | ubus send my_event '{"data": "value"}' |
wait_for | 等待一个或多个对象注册完成 | ubus wait_for echo network |
5.2 调试手段
-
查看系统状态:
ubus list -v
:这是最常用的命令,可以查看系统当前注册了哪些服务,以及每个服务提供了哪些方法和参数。如果您的服务没有出现在列表中,说明注册失败。
-
详细日志:
- 编译时开启调试:在编译 ubus 相关代码时,可以启用调试信息(通常通过定义
DEBUG
宏),这样会在运行时输出更多细节到 stderr 或 syslog。 - 使用
logread
:在 OpenWRT 上,使用logread
命令查看系统日志,许多 ubus 相关的错误和信息会记录在这里。
- 编译时开启调试:在编译 ubus 相关代码时,可以启用调试信息(通常通过定义
-
ubus listen
:- 在一个终端运行
ubus listen
可以实时观察到系统上所有的 ubus 事件,这对于调试基于事件的程序非常有帮助。
- 在一个终端运行
-
strace
/ltrace
:- 使用
strace
跟踪您的客户端或服务端程序的系统调用,查看 socket 连接、读写等操作是否正常。 - 使用
ltrace
跟踪库函数的调用,可以帮助判断程序逻辑。
- 使用
-
手动调用测试:
- 使用
ubus call
命令手动调用您编写的服务方法,传入不同的参数,检查返回值是否符合预期。这是验证服务功能最直接的方式。
- 使用
-
libubox
提供的ustream
和uloop
调试:- 如果问题涉及事件循环或 socket 读写,可以尝试在代码中添加更详细的日志,跟踪
uloop
事件的处理过程。
- 如果问题涉及事件循环或 socket 读写,可以尝试在代码中添加更详细的日志,跟踪
6️⃣ 应用场景与局限性
6.1 典型应用场景
- 系统配置与管理:例如
network
、service
、uci
、system
等对象提供了管理系统网络、服务、配置和重启关机等功能。 - 状态查询:查询网络接口状态、无线状态、DHCP租约等。
- 事件通知:系统服务(如
netifd
)通过事件通知其他进程网络接口的变化、DHCP事件等。 - 小型 RPC 调用:适合在系统内多个守护进程之间进行轻量级的命令调用和数据交换。
6.2 局限性
局限性 | 描述 | 建议 |
---|---|---|
数据量限制 | 单次消息传输不建议超过 ~60KB,否则可能工作不正常。 | 传输大量数据应考虑其他机制,如文件或共享内存。 |
非高并发 | 并非为高并发场景设计,多线程支持不佳。 | 避免在多线程中并发调用同一上下文的方法。 |
递归调用风险 | A调用B,B又调用C的递归调用容易导致问题(如全局变量冲突)。 | 尽量避免复杂的嵌套调用,设计扁平化的接口。 |
仅限本地 | 基于 Unix Socket,只能用于同一台机器上的进程间通信。 | 跨机器通信需选用网络通信机制,如 HTTP/gRPC。 |
💎 总结
ubus 是 OpenWRT 生态系统的核心通信枢纽,其设计的精髓在于轻量和简单。它通过清晰的 C/S 架构、基于 blobmsg 的序列化以及 对象-方法 的抽象模型,为嵌入式环境下的进程间通信提供了一个高效可靠的解决方案。
虽然它在处理大数据量和高并发方面存在局限,但这恰恰符合其针对嵌入式设备的定位。理解和掌握 ubus,对于进行 OpenWRT 平台下的开发、系统管理和故障排查都至关重要。通过本文介绍的原理、代码实例和工具,希望你能更好地运用这一技术。