D-Bus 进程间通信原理详解:总线、寻址与消息模型(三部曲 01 · 理论)
📌 本文是《GDBus 三部曲》第 01 篇(理论篇):只讲协议与概念,不写业务代码。
读者假设:零D-Bus基础、有 C/Linux 基础。
读完本文 → 02《GDBus C 语言开发实战:从一份 XML 到一个能跑的服务》
1. GDBus、D-Bus 和 gdbus 的关系
| 名称 | 说明 |
|---|---|
| D-Bus | Linux/Unix 常用的进程间通信协议 |
| dbus-daemon | D-Bus 总线守护进程,负责消息转发、连接管理、权限控制 |
| GDBus | GLib/GIO 提供的 D-Bus 编程 API |
| gdbus | 命令行观察、调试工具 |
| gdbus-codegen | 根据 XML 接口描述生成 C 代码 |
GDBusConnection、GDBusProxy、GDBusInterfaceSkeleton 等具体 API 类是 02 的主角,这里先不展开。
整体结构
2. D-Bus 的主要用途
D-Bus 主要用于进程之间通信,例如:
- 桌面应用之间通信
- 用户态后台服务通信
- 系统服务通信:systemd、NetworkManager、BlueZ、UDisks、login1 等
- 应用间事件通知
适合以下场景
- 客户端请求服务执行操作
- 服务端返回结果
- 服务端广播状态变化
- 客户端监听系统事件
- 多个客户端共享同一个系统服务
不太适合
- 高频大数据传输
- 传输大文件
- 极低延迟的实时通信
- 大量图像、音视频数据传输
D-Bus 更适合发送控制命令、状态、事件和元数据。
3. D-Bus 的核心对象模型
D-Bus 中最重要的一组概念是:
- Bus- 总线
- Connection- 连接
- Name- 名称
- Object Path- 对象路径
- Interface- 接口
- Method- 方法
- Signal- 信号
- Property- 属性
一次方法调用通常由以下信息共同决定
| 信息 | 示例 |
|---|---|
| Bus | Session Bus |
| Destination | com.example.Calculator |
| Object Path | /com/example/Calculator |
| Interface | com.example.Calculator |
| Method | Add |
| Parameters | (2, 3) |
4. Bus:总线
4.1 Session Bus
Session Bus 是用户会话级总线。
适合:
- 桌面程序
- GUI 应用
- 用户级后台服务
- 同一用户下的多个进程
- 用户级通知
查看地址:
echo"$DBUS_SESSION_BUS_ADDRESS"# 典型输出:# unix:path=/run/user/1000/bus使用 gdbus 观察:
gdbus introspect\--session\--destorg.example.Service\--object-path /org/example/Object4.2 System Bus
System Bus 是系统级总线。
适合:
- 系统服务
- 网络管理
- 蓝牙管理
- 电源管理
- 硬件管理
- systemd
- 多用户共享的系统功能
使用 gdbus 观察:
gdbus introspect\--system\--destorg.freedesktop.NetworkManager\--object-path /org/freedesktop/NetworkManager4.3 Session Bus 和 System Bus 的区别
| 对比项 | Session Bus | System Bus |
|---|---|---|
| 生命周期 | 用户登录会话 | 系统运行期间 |
| 典型用户 | 桌面应用 | 系统服务 |
| 权限 | 一般较宽松 | 通常受到严格限制 |
| 典型服务 | 桌面组件 | systemd、NetworkManager |
| 守护进程运行身份 | 普通用户 | dbus 专用用户(服务多为 root 或专用用户) |
| 是否需要 policy | 通常较少 | 通常需要 |
5. Connection:连接
一个进程要参与 D-Bus 通信,首先要连接到总线,这条连接就是 Connection。
- 连接建立时,进程向 dbus-daemon 发送
Hello(),总线分配一个唯一名称(如:1.42) - 通常一个进程一条连接,每条连接都有独立的唯一名称
- 连接是后续一切通信的载体:方法调用、信号收发、属性访问都通过它进行
如何在代码里建立连接(g_bus_get_sync等 API)属于动手内容,见 02。
6. Name:D-Bus 名称
D-Bus 中存在两种名称。
6.1 Well-known name
稳定、可读的服务名称。
示例:
com.example.Calculatororg.freedesktop.NetworkManagerorg.freedesktop.systemd1
客户端通常通过 well-known name 找服务。
6.2 Unique name
每个连接被总线分配的唯一名称。
示例:
:1.42:1.103
特点:
- 每次连接可能不同
- 进程重启后会变化
- 可以唯一标识当前连接
- 适合识别某个具体客户端
可以类比为:
- well-known name:服务的逻辑名称
- unique name:某次实际连接的临时标识
查看当前总线上的名字:
busctl--userlist busctl list7. Object Path:对象路径
对象路径类似文件系统路径,用于标识服务中的对象。
示例:
/com/example/Calculator/com/example/Device/0/org/freedesktop/NetworkManager
规则:
- 必须以
/开头 - 由多个路径段组成
- 每个路径段只能包含字母、数字和下划线
- 不能使用空格和连字符
- 路径区分大小写
不合法:
com/example/Object/com/example/my-object
合法:
/com/example/Object/com/example/MyObject/com/example/Device_0
一个服务可以导出多个对象:
服务名:com.example.DeviceManager 对象: /com/example/DeviceManager /com/example/Device/0 /com/example/Device/1 /com/example/Device/28. Interface:接口
接口是方法、信号和属性的集合。
一个对象可以实现多个接口:
/com/example/Calculator ├── com.example.Calculator ├── org.freedesktop.DBus.Introspectable ├── org.freedesktop.DBus.Properties └── org.freedesktop.DBus.Peer常见标准接口:
| 接口 | 用途 |
|---|---|
| org.freedesktop.DBus.Introspectable | 查看对象接口信息(Introspect) |
| org.freedesktop.DBus.Properties | 访问属性(Get/Set/GetAll/PropertiesChanged) |
| org.freedesktop.DBus.ObjectManager | 管理多个对象(GetManagedObjects 等) |
| org.freedesktop.DBus.Peer | Ping(探活)、GetMachineId(获取机器标识) |
接口名通常使用反向域名:
com.company.product.Managercom.company.product.Deviceorg.example.Application
9. Method:方法
方法是请求-响应模型:
Client Service | | | -------- Method Call ------> | | | | <------ Method Return ------- | | |方法可以有:
- 输入参数
- 输出参数
- D-Bus 错误
- 超时
- 取消操作
10. Signal:信号
Signal 是事件通知模型。信号同样经过 dbus-daemon 分发:客户端先向总线注册 match rule(匹配规则,说明想收谁的什么信号),之后 daemon 按规则转发。
特点:
- 通常没有返回值(发后即忘)
- 一个服务发送,多个已订阅的客户端都能收到
- 客户端需要先订阅(match rule)
- 适合状态变化、事件通知
示例:
TemperatureChanged(temperature)DeviceAdded(object_path)StateChanged(old_state, new_state)
11. Property:属性
属性用于表示对象状态。
标准接口:
org.freedesktop.DBus.Properties
包含:
Get()Set()GetAll()PropertiesChanged()
例如:
Version: stringEnabled: booleanState: stringTemperature: double
属性访问实际上也是 D-Bus 方法调用:
org.freedesktop.DBus.Properties.Getorg.freedesktop.DBus.Properties.Setorg.freedesktop.DBus.Properties.GetAll
12. Introspection:接口自描述
D-Bus 支持通过 XML 描述对象接口。
使用:
gdbus introspect\--session\--destcom.example.Calculator\--object-path /com/example/Calculator执行后得到:
<!DOCTYPEnodePUBLIC"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN""http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"><nodename="/com/example/Calculator"><interfacename="com.example.Calculator"><methodname="Add"><argname="a"type="i"direction="in"/><argname="b"type="i"direction="in"/><argname="result"type="i"direction="out"/></method><signalname="ResultChanged"><argname="result"type="i"/></signal><propertyname="Version"type="s"access="read"/></interface></node>解读:
- 接口:
com.example.Calculator - 方法
Add:两个i(int32)入参,一个i出参 - 信号
ResultChanged:一个i参数 - 属性
Version:类型s(string),只读
type字段就是 D-Bus 的签名字母(见 §15)。Introspection 是调试 D-Bus 的第一步:先看接口长什么样,再决定怎么调。
13. 一条 Method Call 的旅程
把前面的概念串起来,看一次Add(2, 3)的完整旅程:
- Client 连接总线,获得唯一名称(如
:1.42) - Client 组装消息:destination=
com.example.Calculator、path=/com/example/Calculator、interface=com.example.Calculator、method=Add、参数(2, 3) - 消息经 unix socket 发给dbus-daemon
- daemon 按 destination 查路由:well-known name → 某条连接,转发过去
- Service 收到,按 path → interface → method 分发到具体函数,算出 5
- Service 回Method Return(通过 reply 序号与请求配对),原路经 daemon 回 Client
- Client 收到返回值
(5,)
出错的三种情况:
- Service 返回Error消息(方法内部失败)
- 目标名字不存在 → daemon 直接回错(
ServiceUnknown等) - 等不到回复 →超时(D-Bus 默认 25 秒)
Signal 的旅程不同:没有 destination、没有回复,消息发到总线后由 daemon 按 match rule 扇出给所有订阅者——一对多、发后即忘。
14. 常用观察工具
理论篇只介绍只读观察类工具;写代码、发信号等动手操作见 02。
常用命令:
gdbus introspect—— 查看对象的接口gdbus call—— 调用一次方法gdbus monitor—— 监听某个服务收发的消息busctl list—— 查看总线上有哪些名字dbus-monitor—— 更底层的总线抓包
14.1 gdbus introspect
gdbus introspect\--system\--destorg.freedesktop.systemd1\--object-path /org/freedesktop/systemd1查看根节点:
gdbus introspect\--system\--destorg.freedesktop.systemd1\--object-path /14.2 gdbus call
基本格式:
gdbus call\--session\--dest<服务名>\--object-path<对象路径>\--method<接口名.方法名>\<参数1>\<参数2>示例:
gdbus call\--session\--destcom.example.Calculator\--object-path /com/example/Calculator\--methodcom.example.Calculator.Add\2\3返回:
(5,)⚠️引号坑:
gdbus call的参数按 GVariant 语法解析。字符串必须写成"'hello'"——直接裸敲hello会报解析错误。这是新手最常撞的第一个坑。
调用 systemd 方法:
gdbus call\--system\--destorg.freedesktop.systemd1\--object-path /org/freedesktop/systemd1\--methodorg.freedesktop.systemd1.Manager.GetUnit\ssh.service返回:
(objectpath '/org/freedesktop/systemd1/unit/ssh_2eservice',)注意.被转义成了_2e——这是路径转义,不是乱码。
14.3 gdbus monitor / dbus-monitor
监听指定服务:
gdbus monitor\--session\--destcom.example.Calculator监听系统服务:
gdbus monitor\--system\--destorg.freedesktop.NetworkManager低层监听:
dbus-monitor--sessiondbus-monitor--system15. D-Bus 类型系统和 Signature
D-Bus 使用 Signature(签名)描述数据类型。
15.1 基本类型
| Signature | 类型 | C/GLib 类型 |
|---|---|---|
| y | uint8 | guchar |
| b | boolean | gboolean |
| n | int16 | gint16 |
| q | uint16 | guint16 |
| i | int32 | gint32 |
| u | uint32 | guint32 |
| x | int64 | gint64 |
| t | uint64 | guint64 |
| d | double | gdouble |
| s | string | gchar* |
| o | object path | gchar*(对象路径) |
| g | signature | gchar*(签名) |
| h | unix fd | GUnixFDList*(文件描述符传递,进阶,见 03) |
| v | variant | GVariant* |
15.2 容器类型
| Signature | 含义 |
|---|---|
| aT | 类型为 T 的数组 |
| (T1T2…) | 结构体 |
| {KV} | 字典项(K 必须是基本类型) |
| a{sv} | string → variant 字典 |
示例:
s—— 一个字符串(ii)—— 两个 int32 组成的结构体(is)—— 一个 int32 和一个字符串组成的结构体as—— 字符串数组a{sv}—— string → variant 字典a{ss}—— string → string 字典
⚠️拼接签名 vs 单一类型:方法有两个参数(int32 和 string)时,消息体的线上签名写作
is(拼接)。但在 GDBus/GVariant 的 C API 里,参数永远打包成一个 tuple,写作(is)。GVariant 的类型串必须是单一完整类型,裸写is是非法的——直接传给g_variant_new会断言崩溃。
15.3 方法参数是一个 tuple
D-Bus 方法参数整体打包为一个 tuple,所以Add(2, 3)的参数签名是(ii):
GVariant*parameters=g_variant_new("(ii)",2,3);GVariant 的创建与解析(数组、字典、嵌套结构)属于动手内容,见 02;深度操作见 03。
16. 系列导航
📌《GDBus 三部曲》
- 01 理论(本文):协议、概念、类型系统、观察工具
- 02 实战:《GDBus C 语言开发实战:从一份 XML 到一个能跑的服务》——环境安装、XML 定义接口、gdbus-codegen、服务端/客户端完整代码、编译运行、权限配置
- 03 疑难手册:断线重连、GVariant 深度操作、ObjectManager 等——按问题查询,不通读