D-Bus详解
D-Bus详解
什么是D-Bus
D-Bus(Desktop-Bus)是一个IPC(Inter-Process Communication)通信机制,已经被各主流Linux发行版采用。其技术规范(specification)最早发布于2006年。相较于传统意义上的IPC机制(例如PIPE/FIFO/Socket/共享内存/SysvIpc),D-Bus提供了更高层次的抽象:
- 方法调用(Method Call):用来实现跨进程的方法(函数)调用,配合代码生成工具,可以做到让进程间的函数调用和普通的函数调用几无区别
- 信号(Signal):发布订阅(Pub-Sub)模式的通信机制,发送进程注册并发送(广播)信号,接收进程订阅自己感兴趣的信号。
- 属性(Property):可类比C++类中成员变量的Getter-Setter,如果进程A提供了一个
属性
,那么其他进程可以通过D-Bus来读取、写入该属性。
D-Bus制定技术规范的同时,也提供了一个参考实现,主要包括2部分:
- libdbus,进程可以通过libdbus提供的函数来接入到D-Bus,例如其会提供注册方法、调用方法、发布信号等等函数。根据官方手册友情提示:
If you use this low-level API directly, you're signing up for some pain.
所以后来又有人折腾出其他的binding,例如sd-bus、gdbus、Qt D-Bus
- dbus-daemon,D-Bus的中心节点,实际上进程A和进程B通过D-Bus通信时,并不是通过Socket直接连接,而是进程A <-> dbus-daemon <->。dbus-daemon提供服务注册、消息路由等功能。
基本概念
Bus
一个系统上可以存在任意多总线,以Ubuntu为例:
- System bus 系统总线 开机创建,有且只能有一条,Unix domain socket路径为/run/dbus/system_bus_socket
- Session bus 用户总线 每个用户登录后会创建专有的D-Bus总线,Unix domain socket路径为/run/user/$UID/bus
不同Bus之间是逻辑隔离的,相互直接无法通信。
Bus Name
Bus name是服务注册到总线的名称,Bus name又分为两类:
Unique name 以:
开始,Unique name是dbus-daemon分配的,类似于:1.30
,技术规范明确要求其永不重复
Well Known name 为了方便人脑记忆引入的名字。为了命名不冲突,技术规范推荐使用逆转域名方式命名,例如org.freedesktop.DBus
Object Path
一个进程可以包含多个对象,D-Bus用Object path来标识对象,一般以/org/freedesktop/DBus
这种形式命名。
Interface
Interface包含方法(Method)、属性(Property)和信号(Signal),以org.freedesktop.DBus这种形式命名。
Method
方法,程序可以通过D-Bus提供的机制,调用另外一个进程提供的Method,即RPC的概念
Signal
信号,D-Bus中Pub-Sub通信模型的具体实现。
Property
属性,类似于C++类中成员变量的Getter-Setter,可以定义为只读、只写、读写类型。
Signature
D-Bus技术规范规定使用String类型来编码方法、信号的签名(参数)。例如i
代表int32_t类型,a
代表数组等等。
概念小结
接触D-Bus时,所有概念中,最难理解的就是路由寻址相关的。其中主要有几个原因:
- 寻址概念多且边界不是很清晰
- 容易混淆,例如bus name和interface,本身是两个不相干的概念,但是实践中经常用同样的字符串
通过和Web来做一个类比,可以让这些概念更加具象一点,下面已网址https://github.com/freedesktop/dbus/commits/master?after=13e7b14e195e60b6a068166eb8872fa56c6328de+34&branch=master&qualified_name=refs%2Fheads%2Fmaster
举例
D-Bus | Web | 类比内容 |
---|---|---|
Bus | 因特网、专网 | 因特网 |
Bus name (Unique) | IP地址 | 20.205.243.166 |
Bus name (Well known) | 域名 | http://github.com |
Object path | URL路径 | /freedesktop/dbus/commits/ |
Interface | URL | master |
Method | URL | master |
Signature | 参数 | master?after=13e7b...34&branch=master |
综上,路由寻址层级大体如下:
Bus -> Bus Name -> Object path -> Interface -> Method
busctl使用
busctl是systemd提供的一个D-Bus总线调试工具。通过busctl命令的输出,可对D-Bus中各种抽象的概念有个感性的认识。
busctl list
busctl list会打印出总线中已注册的服务(注:manpage这里写的是peer,暂且称为服务
),命令执行结果截取一段如下:
$ busctl list --no-pager --system
NAME PID PROCESS USER CONNECTION UNIT SESSION DESCRIPTION
:1.10 1067 udisksd root :1.10 udisks2.service - -
:1.14 1122 ModemManager root :1.14 ModemManager.service - -
:1.16 1155 gdm3 root :1.16 gdm.service - -
:1.22 1064 systemd-logind root :1.22 systemd-logind.service - -
:1.25 1198 unattended-upgr root :1.25 unattended-upgrades.service - -
:1.2633 1382069 networkd-dispat root :1.2633 networkd-dispatcher.service - -
org.bluez 1283814 bluetoothd root :1.7917 bluetooth.service - -
org.debian.apt - - - (activatable) - - -
org.freedesktop.Accounts 1023 accounts-daemon root :1.7 accounts-daemon.service - -
org.freedesktop.Avahi 1027 avahi-daemon avahi :1.3 avahi-daemon.service - -
org.freedesktop.DBus 1 systemd root - init.scope - -
org.freedesktop.NetworkManager 1034 NetworkManager root :1.11 NetworkManager.service - -
org.freedesktop.UDisks2 1067 udisksd root :1.10 udisks2.service - -
org.freedesktop.thermald - - - (activatable) - - -
表格字段说明:
NAME Bus name
PID 服务对应的进程ID
PROCESS 服务对应的进程名
USER 对应启动服务的用户
CONNECTION 服务与dbus-daemon的连接信息,及Unique name。服务和连接是多对1的关系,一个CONNECTION
可以注册多个Bus name,但是Bus name只能对应一个CONNECTION
。
$ busctl --user |grep 1.43
:1.43 5089 gnome-shell yk :1.43 user@1000.service - -
org.gnome.Panel 5089 gnome-shell yk :1.43 user@1000.service - -
org.gnome.ScreenSaver 5089 gnome-shell yk :1.43 user@1000.service - -
org.gnome.Shell 5089 gnome-shell yk :1.43 user@1000.service - -
...省略N行...
值得注意的是,org.freedesktop.thermald服务显示为activatable
,意思是现在服务可以被激活,这个可以被激活的信息是怎么来的呢,这里就又牵涉到了systemd。在thermald.service中,有这么一行配置BusName=org.freedesktop.thermald
,也就是通过启动thermald.service,可以就会注册名为org.freedesktop.thermald
D-Bus服务。
$ cat /usr/lib/systemd/system/thermald.service
[Unit]
Description=Thermal Daemon Service
ConditionVirtualization=no
[Service]
Type=dbus
SuccessExitStatus=1
BusName=org.freedesktop.thermald
ExecStart=/usr/sbin/thermald --systemd --dbus-enable --adaptive
UNIT 对应服务启动的systemd unit
SESSION 对应systemd的中的login session
busctl introspect
introspect(内省反射)可用来查看服务里的接口定义,对应的参数为 SERVICE OBJECT [INTERFACE],如下指令可以查看 org.freedesktop.login1
服务下,对象名为/org/freedesktop/login1/session/auto
$ busctl introspect org.freedesktop.login1 /org/freedesktop/login1/session/auto
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.login1.Session interface - - -
.Lock method - - -
.Active property b true emits-change
.Remote property b false const
.RemoteHost property s "" const
.RemoteUser property s "" const
.Scope property s "session-2.scope" const
.Seat property (so) "seat0" "/org/freedesktop/login1/seat/s… const
.Lock signal - - -
字段说明
TYPE 类型,主要分为:
NAME 名称
SIGNATURE 参见Signature
RESULT/VALUE 不同Type定义稍有不同:
- method时,对应的是函数的输出结果签名
- Property时,对应的是属性当前的值
FLAGS 只针对property有效:
- emits-change 改变后会发送org.freedesktop.DBus.Properties.PropertiesChanged信号
- const 常量属性,在对象生命周期内不会改变
busctl monitor
monitor子命令用来监听总线上所有的消息,例如我们如果想看busctl list
命令背后发生了什么,可以通过monitor来观测,下面截取了关键部分输出:
$ busctl --user monitor
Monitoring bus message stream.
‣ Type=method_call Endian=l Flags=0 Version=1 Priority=0 Cookie=2
Sender=:1.21712 Destination=org.freedesktop.DBus Path=/org/freedesktop/DBus Interface=org.freedesktop.DBus Member=ListNames
UniqueName=:1.21712
MESSAGE "" {
};
‣ Type=method_return Endian=l Flags=1 Version=1 Priority=0 Cookie=3 ReplyCookie=2
Sender=org.freedesktop.DBus Destination=:1.21712
MESSAGE "as" {
ARRAY "s" {
STRING "org.freedesktop.DBus";
STRING ":1.10105";
STRING "org.freedesktop.Notifications";
此处省略N个结果
};
};
协议及流程
Message
Message header
Message包括头和数据,是D-Bus总线上通信的标准报文格式。Message的定义如下(摘自技术规范):
Value | Description |
---|---|
1st BYTE | 大小端标志,l - 小端 B - 大端. |
2nd BYTE | 消息类型,参见下表 |
3rd BYTE | 消息标志位(Flag),定义参见下表 |
4th BYTE | 协议主版本号 |
1st UINT32 | 数据长度,不包含头 |
2nd UINT32 | 消息序列号,用来匹配会请求和回复,从1开始 |
ARRAY of STRUCT of (BYTE,VARIANT) | 头字段(header fields),一个field code, field value数组,根据消息类型不同二不同 |
消息类型定义
Conventional name | Decimal value | Description |
---|---|---|
INVALID | 0 | 无效 |
METHOD_CALL | 1 | Method调用 |
METHOD_RETURN | 2 | Method调用返回 |
ERROR | 3 | 错误回复 |
SIGNAL | 4 | 信号发送 |
消息标志位定义
Conventional name | Hex value | Description |
---|---|---|
NO_REPLY_EXPECTED | 0x1 | Method调用时,此标志位标志忽略回复 |
NO_AUTO_START | 0x2 | 不自动启动目标服务 |
ALLOW_INTERACTIVE_AUTHORIZATION | 0x4 | 允许交互式授权 |
消息头字段
Conventional Name | Field Code | Type | Required In | Description |
---|---|---|---|---|
INVALID | 0 | N/A | not allowed | 非法 |
PATH | 1 | OBJECT_PATH | METHOD_CALL, SIGNAL | 类型为Method调用时,代表被调用者的Object Path 。类型为信号时,代表发送者的Object Path |
INTERFACE | 2 | STRING | SIGNAL | 类型为Method调用时,代表被调用者的Interface。类型为信号时,代表发送者的Interface |
MEMBER | 3 | STRING | METHOD_CALL, SIGNAL | Method或Signal的名字 |
ERROR_NAME | 4 | STRING | ERROR | error名 |
REPLY_SERIAL | 5 | UINT32 | ERROR, METHOD_RETURN | 回复序列号,通过匹配Message header中的消息序列号字段,该字段可以判断METHOD_RETURN是对应那个METHOD_CALL。 |
DESTINATION | 6 | STRING | optional | 消息目的地的Bus name |
SENDER | 7 | STRING | optional | 消息发送者的Bus name |
SIGNATURE | 8 | SIGNATURE | optional | 消息体内容签名 |
UNIX_FDS | 9 | UINT32 | optional | 消息相关的文件句柄 |
org.freedesktop.DBus
org.freedesktop.DBus是一个特殊的Bus name,它是由dbus-daemon实现的,包含了一些关键的方法,例如:
- Hello 用来获取Unique name
- RequestName 用来注册Well known name
- AddMatch 用来添加路由规则
- ListName 用来获取所有Bus name
获取Unique name
已sd-bus实例中的程序注册为例:
- 调用
sd_bus_open_user
创建连接到Session bus的socket - 鉴权以及句柄传递特性协商
C : AUTH EXTERNAL S : DATA S : OK 39cd3840b528b2ed120f4f9f625be2bf C : NEGOTIATE_UNIX_FD S : AGREE_UNIX_FD C : BEGIN
- 调用
org.freedesktop.DBus.Hello
接口,获取一个Unique name
‣ Type=method_call Endian=l Flags=0 Version=1 Priority=0 Cookie=1
Sender=:1.23493 Destination=org.freedesktop.DBus Path=/org/freedesktop/DBus Interface=org.freedesktop.DBus Member=Hello
UniqueName=:1.23493
MESSAGE "" {
};
‣ Type=method_return Endian=l Flags=1 Version=1 Priority=0 Cookie=1 ReplyCookie=1
Sender=org.freedesktop.DBus Destination=:1.23493
MESSAGE "s" {
STRING ":1.23493";
};
注册Well known name
- 执行获取Unique name中的步骤
- 调用
RequestName
方法获取Bus name
‣ Type=method_call Endian=l Flags=0 Version=1 Priority=0 Cookie=2
Sender=:1.23493 Destination=org.freedesktop.DBus Path=/org/freedesktop/DBus Interface=org.freedesktop.DBus Member=RequestName
UniqueName=:1.23493
MESSAGE "su" {
STRING "net.poettering.Calculator";
UINT32 4;
};
‣ Type=method_return Endian=l Flags=1 Version=1 Priority=0 Cookie=4 ReplyCookie=2
Sender=org.freedesktop.DBus Destination=:1.23493
MESSAGE "u" {
UINT32 1;
};
方法调用
- 执行获取Unique name中的步骤
- Client发送
METHOD CALL
类型的Message,并在消息头字段中指定Bus name/Path/Interface/Method,并附加符合方法签名的参数 - dbus-daemon根据Bus name字段,查找并转发给相应的Server
- Server根据Path/Interface/Member来查找需要执行的函数
- Server中的函数解析相应的参数,并执行业务逻辑,并将结果发送到dbus-daemon
- dbus-daemon将结果转发给Client
信号Pub-Sub
Puber端
- 执行获取Unique name中的步骤
- Puber发送SIGNAL类型的Message,并在消息头字段中指定Path/Interface/Signal,并附加符合信号签名的参数
Suber端
- 执行获取Unique name中的步骤
- 调用
org.freedesktop.DBus
的AddMatch
方法来subscribe对应的信号
‣ Type=method_call Endian=l Flags=0 Version=1 Priority=0 Cookie=2
Sender=:1.28315 Destination=org.freedesktop.DBus Path=/org/freedesktop/DBus Interface=org.freedesktop.DBus Member=AddMatch
UniqueName=:1.28315
MESSAGE "s" {
STRING "type='signal',sender='org.sdbuscpp.examplemanager',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded',path='/org/sdbuscpp/examplemanager'";
};
‣ Type=method_return Endian=l Flags=1 Version=1 Priority=0 Cookie=3 ReplyCookie=2
Sender=org.freedesktop.DBus Destination=:1.28315
MESSAGE "" {
};
AddMatch
只有一个参数rule
,类型为string,格式为逗号分割的Key-Value对,具体支持的含义如下(摘自此处):
key | possible values | Description |
---|---|---|
type | signal, method_call, method_return, error | Matches specific message type |
sender | Unique or well-known bus name | Matches messages sent by a particular sender |
interface | Interface name | Matches messages sent to or from the specified interface |
member | Any valid method or signal name | Matches method and signal names |
path | Any valid object path | Matches messages sent to or from the specified object |
path_namespace | Any valid object path | Matches messages sent to or from all objects at or below the specified path |
destination | Unique or well-known bus name | Matches messages sent to the specified destination |
arg[0,1,2…] | Any string | Matches messages based on the content of their arguments. Only arguments of string type may be matched. |
arg[0,1,2…]path | Any string | Specialized matching for path-like arguments. Ex: arg0path=/aa/bb will match /, /aa/, /aa/bb/cc but not /aa, /aa/b, or /aa/bb |
arg0namespace | Any string | Specialized matching for partial bus names. Primarily intended for monitoring NameOwnerChanged for a group of related bus names. Ex: member="NameOwnerChanged", arg0namespace="com.foo.bar" will match "com.foo.bar.baz" and "com.foo.bar.quux" |
Introspect
D-Bus技术规范要求所有的服务均实现标准接口(Starndard Interfaces),包含如下几个:
- org.freedesktop.DBus.Introspectable
- org.freedesktop.DBus.Peer
- org.freedesktop.DBus.Properties
- org.freedesktop.DBus.ObjectManager
其中busctl命令就是调用了Introspectable
方法,来获取目标服务的API描述,具体步骤如下:
- 执行获取Unique name中的步骤
- 调用目标的
Introspectable
方法
‣ Type=method_call Endian=l Flags=0 Version=1 Priority=0 Cookie=2
Sender=:1.26055 Destination=net.poettering.Calculator Path=/net/poettering/Calculator Interface=org.freedesktop.DBus.Introspectable Member=Introspect
UniqueName=:1.26055
MESSAGE "" {
};
‣ Type=method_return Endian=l Flags=1 Version=1 Priority=0 Cookie=52 ReplyCookie=2
Sender=:1.25848 Destination=:1.26055
UniqueName=:1.25848
MESSAGE "s" {
STRING "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Peer">
<method name="Ping"/>
<method name="GetMachineId">
<arg type="s" name="machine_uuid" direction="out"/>
</method>
此处省略N字...
</node>
";
};
dbus-daemon配置
在dbus-daemon manpage里面的CONFIGURATION FILE章节有详细描述dbus-daemon的配置。值得注意的是,在大多系统中,需要先定义新的<policy>
才能往System bus注册新的服务。
sdbus-cpp的文档给出了一个示例:
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.sdbuscpp.concatenator"/>
<allow send_destination="org.sdbuscpp"/>
<allow send_interface="org.sdbuscpp.concatenator"/>
</policy>
</busconfig>
sd-bus及example
sd-bus是systemd提供的一个D-Bus binding,作者在博客提供了一个Client、Server的示例代码。
Client
#include <stdio.h>
#include <stdlib.h>
#include <systemd/sd-bus.h>
int main(int argc, char *argv[]) {
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *m = NULL;
sd_bus *bus = NULL;
const char *path;
int r;
/* Connect to the system bus */
r = sd_bus_open_system(&bus);
if (r < 0) {
fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
goto finish;
}
/* Issue the method call and store the respons message in m */
r = sd_bus_call_method(bus,
"org.freedesktop.systemd1", /* service to contact */
"/org/freedesktop/systemd1", /* object path */
"org.freedesktop.systemd1.Manager", /* interface name */
"StartUnit", /* method name */
&error, /* object to return error in */
&m, /* return message on success */
"ss", /* input signature */
"cups.service", /* first argument */
"replace"); /* second argument */
if (r < 0) {
fprintf(stderr, "Failed to issue method call: %s\n", error.message);
goto finish;
}
/* Parse the response message */
r = sd_bus_message_read(m, "o", &path);
if (r < 0) {
fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r));
goto finish;
}
printf("Queued service job as %s.\n", path);
finish:
sd_bus_error_free(&error);
sd_bus_message_unref(m);
sd_bus_unref(bus);
return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
Server
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <systemd/sd-bus.h>
static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
int64_t x, y;
int r;
/* Read the parameters */
r = sd_bus_message_read(m, "xx", &x, &y);
if (r < 0) {
fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
return r;
}
/* Reply with the response */
return sd_bus_reply_method_return(m, "x", x * y);
}
static int method_divide(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
int64_t x, y;
int r;
/* Read the parameters */
r = sd_bus_message_read(m, "xx", &x, &y);
if (r < 0) {
fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
return r;
}
/* Return an error on division by zero */
if (y == 0) {
sd_bus_error_set_const(ret_error, "net.poettering.DivisionByZero", "Sorry, can't allow division by zero.");
return -EINVAL;
}
return sd_bus_reply_method_return(m, "x", x / y);
}
/* The vtable of our little object, implements the net.poettering.Calculator interface */
static const sd_bus_vtable calculator_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Multiply", "xx", "x", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD("Divide", "xx", "x", method_divide, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
int main(int argc, char *argv[]) {
sd_bus_slot *slot