D-Bus 完整教程 / 05 - D-Bus 方法调用
第 05 章:D-Bus 方法调用
5.1 方法调用的基本流程
D-Bus 方法调用遵循 请求-响应 模型,类似于 HTTP 的 GET/POST:
客户端 服务端
│ │
│── Method Call ──────────────────→│
│ (serial, dest, path, │
│ interface, member, args) │
│ │
│←── Method Return ───────────────│
│ (reply_serial, args) │
│ │
│ 或 │
│←── Error ────────────────────────│
│ (reply_serial, error_name, │
│ error_message) │
关键规则:
- 每个 Method Call 有唯一的
serial(递增整数) - 对应的 Return/Error 必须携带
reply_serial与之匹配 - 一个连接可以同时有多个未完成的调用(多路复用)
5.2 同步调用
同步调用会阻塞等待回复,适合简单场景。
5.2.1 命令行同步调用
# 同步调用 ListNames,等待返回
busctl call \
org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus \
ListNames
# 带参数的同步调用
busctl call \
org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager \
GetSession \
s "auto"
5.2.2 Python 同步调用
#!/usr/bin/env python3
"""D-Bus 同步方法调用示例"""
import dbus
bus = dbus.SessionBus()
# 获取代理对象
proxy = bus.get_object(
'org.freedesktop.DBus',
'/org/freedesktop/DBus'
)
# 获取接口
iface = dbus.Interface(proxy, 'org.freedesktop.DBus')
# 同步调用
names = iface.ListNames()
print("总线上的名称:")
for name in names:
print(f" {name}")
# 使用 Properties 接口获取属性
props_iface = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties')
version = props_iface.Get('org.freedesktop.DBus', 'Version')
print(f"\nD-Bus 版本: {version}")
5.2.3 GDBus (C) 同步调用
#include <gio/gio.h>
#include <stdio.h>
int main(void) {
GError *error = NULL;
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!conn) {
fprintf(stderr, "连接失败: %s\n", error->message);
g_error_free(error);
return 1;
}
/* 同步调用 ListNames */
GVariant *result = g_dbus_connection_call_sync(
conn,
"org.freedesktop.DBus", /* bus name */
"/org/freedesktop/DBus", /* object path */
"org.freedesktop.DBus", /* interface */
"ListNames", /* method */
NULL, /* parameters */
G_VARIANT_TYPE("(as)"), /* reply type */
G_DBUS_CALL_FLAGS_NONE,
5000, /* timeout ms */
NULL, /* cancellable */
&error
);
if (error) {
fprintf(stderr, "调用失败: %s\n", error->message);
g_error_free(error);
} else {
GVariantIter *iter;
const char *name;
g_variant_get(result, "(as)", &iter);
printf("总线上的名称:\n");
while (g_variant_iter_next(iter, "&s", &name)) {
printf(" %s\n", name);
}
g_variant_iter_free(iter);
g_variant_unref(result);
}
g_object_unref(conn);
return 0;
}
编译运行:
gcc -o method-sync method-sync.c $(pkg-config --cflags --libs gio-2.0)
./method-sync
5.3 异步调用
异步调用不会阻塞,通过回调函数处理结果,适合 GUI 或高并发服务。
5.3.1 Python 异步调用
#!/usr/bin/env python3
"""D-Bus 异步方法调用示例(使用 GLib 主循环)"""
import dbus
import dbus.mainloop.glib
from gi.repository import GLib
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
loop = GLib.MainLoop()
def on_reply(names):
print("异步收到名称列表:")
for name in names:
print(f" {name}")
loop.quit()
def on_error(error):
print(f"调用出错: {error}")
loop.quit()
proxy = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
iface = dbus.Interface(proxy, 'org.freedesktop.DBus')
# 异步调用
iface.ListNames(
reply_handler=on_reply,
error_handler=on_error
)
print("等待异步结果...")
loop.run()
5.3.2 GDBus (C) 异步调用
#include <gio/gio.h>
static void on_list_names_ready(GObject *source, GAsyncResult *res, gpointer user_data) {
GError *error = NULL;
GVariant *result = g_dbus_connection_call_finish(
G_DBUS_CONNECTION(source), res, &error
);
if (error) {
g_printerr("调用失败: %s\n", error->message);
g_error_free(error);
} else {
GVariantIter *iter;
const char *name;
g_variant_get(result, "(as)", &iter);
g_print("总线上的名称:\n");
while (g_variant_iter_next(iter, "&s", &name)) {
g_print(" %s\n", name);
}
g_variant_iter_free(iter);
g_variant_unref(result);
}
g_main_loop_quit((GMainLoop *)user_data);
}
int main(void) {
GMainLoop *loop = g_main_loop_new(NULL, FALSE);
GError *error = NULL;
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!conn) {
g_printerr("连接失败: %s\n", error->message);
g_error_free(error);
return 1;
}
/* 异步调用 */
g_dbus_connection_call(
conn,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"ListNames",
NULL, /* parameters */
G_VARIANT_TYPE("(as)"),
G_DBUS_CALL_FLAGS_NONE,
5000, /* timeout ms */
NULL, /* cancellable */
on_list_names_ready, /* callback */
loop /* user_data */
);
g_print("等待异步结果...\n");
g_main_loop_run(loop);
g_object_unref(conn);
g_main_loop_unref(loop);
return 0;
}
5.4 超时处理
5.4.1 超时机制
| 配置项 | 默认值 | 说明 |
|---|---|---|
| Method Call 超时 | 25 秒 | 客户端等待回复的最大时间 |
| dbus-daemon 转发超时 | 无限制 | 守护进程不会主动截断 |
| busctl 超时 | 25 秒 | 可通过 --timeout= 修改 |
# 设置 busctl 超时
busctl call --timeout=60s \
org.example.SlowService \
/org/example \
org.example.Interface \
LongRunningMethod
5.4.2 NoReply 方法
某些方法不需要回复(fire-and-forget),通过注解标记:
<method name="Exit">
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
</method>
客户端必须设置 NO_REPLY_EXPECTED 标志:
# 使用 gdbus 调用 NoReply 方法
gdbus call --session \
--dest org.example.Service \
--object-path /org/example \
--method org.example.Interface.Exit
5.4.3 超时编程处理
#!/usr/bin/env python3
"""D-Bus 调用超时处理"""
import dbus
from dbus.exceptions import DBusException
bus = dbus.SessionBus()
try:
proxy = bus.get_object(
'org.freedesktop.login1',
'/org/freedesktop/login1',
introspect=False
)
iface = dbus.Interface(proxy, 'org.freedesktop.login1.Manager')
# dbus-python 默认超时为 25 秒
# 可以通过设置 timeout 参数调整
result = iface.CanHibernate(timeout=10) # 10 秒超时
print(f"可以休眠: {result}")
except DBusException as e:
if 'Timed out' in str(e):
print("调用超时,服务可能无响应")
elif 'ServiceUnknown' in str(e):
print("服务不可用,可能未安装")
else:
print(f"D-Bus 错误: {e}")
5.5 错误处理
5.5.1 D-Bus 错误名称
D-Bus 错误使用 反向域名 格式的错误名称:
| 错误名称 | 说明 |
|---|---|
org.freedesktop.DBus.Error.UnknownMethod | 方法不存在 |
org.freedesktop.DBus.Error.UnknownObject | 对象不存在 |
org.freedesktop.DBus.Error.UnknownInterface | 接口不存在 |
org.freedesktop.DBus.Error.ServiceUnknown | 服务不可用 |
org.freedesktop.DBus.Error.AccessDenied | 权限不足 |
org.freedesktop.DBus.Error.InvalidArgs | 参数无效 |
org.freedesktop.DBus.Error.NoReply | 超时无回复 |
org.freedesktop.DBus.Error.FileNotFound | 文件未找到 |
5.5.2 命令行错误处理
# 尝试调用不存在的方法
busctl call \
org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus \
NonExistentMethod
# 输出:
# Call failed: Method "NonExistentMethod" with signature "" on interface "org.freedesktop.DBus" doesn't exist
# 尝试调用需要授权的方法
busctl call \
org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager \
PowerOff \
b true
# 可能输出:
# Call failed: Access denied
5.5.3 GDBus 错误处理
GError *error = NULL;
GVariant *result = g_dbus_connection_call_sync(..., &error);
if (error) {
if (g_dbus_error_is_remote_error(error)) {
gchar *remote_error = g_dbus_error_get_remote_error(error);
g_print("远程错误: %s\n", remote_error);
g_free(remote_error);
} else {
g_print("本地错误: %s\n", error->message);
}
g_error_free(error);
}
5.5.4 Python 错误处理
import dbus
from dbus.exceptions import DBusException
try:
result = iface.SomeMethod()
except DBusException as e:
# 错误名称在 e.get_dbus_name()
error_name = e.get_dbus_name()
error_msg = str(e)
if error_name == 'org.freedesktop.DBus.Error.AccessDenied':
print("权限不足,需要管理员权限")
elif error_name == 'org.freedesktop.DBus.Error.ServiceUnknown':
print("服务未安装或未启动")
elif error_name == 'org.freedesktop.DBus.Error.InvalidArgs':
print(f"参数错误: {error_msg}")
else:
print(f"未知错误 [{error_name}]: {error_msg}")
5.6 D-Bus 类型系统
D-Bus 使用严格的类型系统,所有参数必须明确类型。
5.6.1 基本类型
| 符号 | 类型 | 大小 | C 类型 | Python 类型 |
|---|---|---|---|---|
y | BYTE | 1 byte | guint8 | dbus.Byte |
b | BOOLEAN | 4 bytes | gboolean | dbus.Boolean |
n | INT16 | 2 bytes | gint16 | dbus.Int16 |
q | UINT16 | 2 bytes | guint16 | dbus.UInt16 |
i | INT32 | 4 bytes | gint32 | dbus.Int32 |
u | UINT32 | 4 bytes | guint32 | dbus.UInt32 |
x | INT64 | 8 bytes | gint64 | dbus.Int64 |
t | UINT64 | 8 bytes | guint64 | dbus.UInt64 |
d | DOUBLE | 8 bytes | gdouble | dbus.Double |
s | STRING | 变长 | const gchar* | str |
o | OBJECT_PATH | 变长 | const gchar* | dbus.ObjectPath |
v | VARIANT | 变长 | GVariant* | dbus.Variant |
5.6.2 容器类型
| 符号 | 类型 | 示例签名 | 说明 |
|---|---|---|---|
a<T> | ARRAY | as, ai | 同类型元素数组 |
(...) | STRUCT | (si), (sib) | 异类型元素元组 |
a{KV} | DICT | a{sv}, a{ss} | 键值映射 |
h | UNIX_FD | - | 文件描述符传递 |
5.6.3 类型签名示例
| 签名 | 含义 | 实际值示例 |
|---|---|---|
s | 字符串 | "hello" |
as | 字符串数组 | ["a", "b", "c"] |
a{sv} | 键值字典(string→variant) | {"name": "test", "count": 42} |
(si) | 结构体(string, int32) | ("hello", 42) |
a(si) | 结构体数组 | [("a", 1), ("b", 2)] |
a{sa{sv}} | 嵌套字典 | {"iface": {"prop": "val"}} |
5.6.4 Variant 类型
VARIANT(v)是 D-Bus 最灵活的类型——它可以包含任意类型的值。
# 发送 Variant 参数
dbus-send --session --dest=org.example.Service \
--type=method_call --print-reply \
/org/example \
org.example.Interface.SetValue \
variant:string:"hello"
# variant:int32:42
# variant:array:string:"a","b","c"
注意:
a{sv}(string→variant 字典)是 D-Bus 生态中最常用的复合类型。GNOME、systemd、Flatpak 等大量使用它来传递灵活的配置数据。
5.7 实战场景
场景 1:通过 D-Bus 控制媒体播放器
# 查看 MPRIS 播放器
busctl --user list | grep mpris
# 获取播放器名称
PLAYER="org.mpris.MediaPlayer2.spotify"
# 播放/暂停
busctl call --user \
$PLAYER \
/org/mpris/MediaPlayer2 \
org.mpris.MediaPlayer2.Player \
PlayPause
# 获取当前曲目信息
busctl get-property --user \
$PLAYER \
/org/mpris/MediaPlayer2 \
org.mpris.MediaPlayer2.Player \
Metadata
场景 2:发送桌面通知
busctl call --user \
org.freedesktop.Notifications \
/org/freedesktop/Notifications \
org.freedesktop.Notifications \
Notify \
s "我的应用" \
u 0 \
s "" \
s "通知标题" \
s "这是通知内容" \
as 0 \
a{sv} 0 \
i 5000
场景 3:查询网络状态
#!/usr/bin/env python3
"""通过 D-Bus 查询 NetworkManager 状态"""
import dbus
bus = dbus.SystemBus()
nm = bus.get_object(
'org.freedesktop.NetworkManager',
'/org/freedesktop/NetworkManager'
)
props = dbus.Interface(nm, 'org.freedesktop.DBus.Properties')
# 获取网络状态
state = props.Get('org.freedesktop.NetworkManager', 'State')
# 70 = 已连接全局网络
print(f"网络状态: {state}")
# 获取活动连接
active = props.Get('org.freedesktop.NetworkManager', 'ActiveConnections')
print(f"活动连接: {len(active)} 个")
# 获取设备列表
nm_iface = dbus.Interface(nm, 'org.freedesktop.NetworkManager')
devices = nm_iface.GetDevices()
print(f"网络设备: {len(devices)} 个")
5.8 常见错误排查
| 症状 | 原因 | 解决方案 |
|---|---|---|
ServiceUnknown | 服务未运行 | 检查服务状态,配置 D-Bus 激活 |
AccessDenied | 策略拒绝 | 检查 /etc/dbus-1/system.d/ 策略文件 |
InvalidArgs | 类型不匹配 | 检查参数类型签名 |
NoReply | 服务无响应 | 增加超时,检查服务是否死锁 |
UnknownMethod | 方法名错误 | 使用 busctl introspect 确认 |
本章小结
| 概念 | 说明 |
|---|---|
| Method Call | 请求-响应调用,每个调用有唯一 serial |
| 同步调用 | 阻塞等待回复,适合简单场景 |
| 异步调用 | 回调处理回复,适合 GUI 和高并发 |
| 超时 | 默认 25 秒,可自定义 |
| 错误名称 | 反向域名格式,如 org.freedesktop.DBus.Error.AccessDenied |
| 类型系统 | 严格类型,a{sv} 为最常用复合类型 |