概述

D-Bus 是 GNU/Linux 系统中的一种跨进程通讯(IPC)机制。本文介绍了 D-Bus 的组成、基本概念,以及提供了一个使用 Qt 套件来实现 D-Bus 跨进程通讯的例子。

D-BUS is a message bus system, a simple way for applications to talk to one another.

The low-level API for DBUS is written in C but most of the documentation and code is written for a higher level binding, such as Python or GLib. [Ref]

D-Bus is an Inter-Process Communication (IPC) and Remote Procedure Calling (RPC) mechanism originally developed for Linux to replace existing and competing IPC solutions with one unified protocol. It has also been designed to allow communication between system-level processes (such as printer and hardware driver services) and normal user processes. [Qt 定义]

概览

D-Bus 是一个 Linux 进程间通讯的统一接口套件。常用来进行 系统进程 – 用户进程 交互。

使用 D-Bus 进行 IPC,通常是通过一个总线(Bus)进行的,但是进程 – 进程单点通讯也可以实现。

结构

基于 libdbus 的 D-BUS 套件系统包括:

  • 函数库 libdbus ,用于两个应用程序互相联系和交互消息。

  • 一个基于 libdbus 构造的消息总线守护进程 (dbus-daemon),可同时与多个应用程序相连,并能把来自一个应用程序的消息路由到0或者多个其他程序。

libdbus 实现了一套进程间通讯方案,是一套 D-Bus 机制的实现库。在内核态,libdbus 通过系统提供的底层 IPC 方案(例如 Unix Domain Socket 甚至 TCP)来充当底层的进程间通讯的信道。在类 Unix 系统中,libdbus 默认利用 Unix Domain Socket 来通讯;在 Windows 系统中,libdbus 默认利用 Nonce-TCP 通讯。通讯方式也可以指定,例如使用 systemd 或 macOS 上的 launchd 来充当信道也是可以的。[Ref]

freedesktop D-Bus 套件中的 dbus-daemon 是基于 libdbus 实现的抽象总线。应用程序可以通过 dbus-daemon 来实现点对点和广播等通讯。

基于 libdbus 的高层封装包括 QtDBus、Java 中的 org.freedesktop.dbus 包等。现代应用程序在基于 D-Bus 的开发实践中,通常使用更高层的 D-Bus 封装,而不直接使用 libdbus 这样偏底层的 D-Bus 实现(官方也不推荐这么做)。

D-Bus stackD-Bus stack

libdbus(以及它的高层封装 QtDBus 等)可以使应用程序连接到另一个程序【上图中的红线】,或者使应用程序连接到 dbus-daemon 并通过它连接到另一个程序【上图中的白线】,而后者使用的更为广泛。

dbus-daemon

通常情况下,单个用户的 Linux 会至少启动了两个 dbus-daemon 进程,一个属于 system,一个属于 session。后者在用户登录的时候由 dbus-launch 启动。

大多数普通程序都是使用 session 的 dbus-daemon。

dbus-daemon 是有地址的,环境变量 DBUS_SESSION_BUS_ADDRESS 用于表示当前登录用户的 session 的 dbus-daemon 进程的地址。

事实上,使用系统自启动的 dbus-daemon 是可以的。但是系统自启动的 dbus-daemon 的输出被重定向其他地方去了,很难调试;某些时候,单独启动一个 dbus-daemon 比较方便。

底层概念

Bus

在 D-Bus 中,总线 (bus) 是核心的概念:不同的程序可以 通过总线 进行某些操作,比如 方法调用发送信号监听特定的信号

总线的职责是:

  • 跟踪、维护连接在其上的应用程序;

  • 负责消息从其起点到目的地的路由

总线通常有两种,系统总线 (system bus) 和会话总线 (session bus),系统总线通常只有一条,用户总线在用户登录时创建。

系统总线是一个持久的总线,在系统启动时就创建,供系统内核和后台进程使用,具有较高的安全性系统总线最常用是发送系统消息,比如:插入一个新的存储设备、有新的网络连接等。

会话总线是在某个用户登录后启动,属于某个用户私有,是某用户的应用程序用来通话的通道。在很多嵌入式系统中,只有一个用户 ID 登录运行,因此只有一个会话总线。

dbus-daemon 就是 Bus。

虽然我们可以通过 D-Bus 实现点对点的进程间通讯,但是在业务中我们更倾向于使用中心化的 D-Bus,即利用 Bus (dbus-daemon)进行消息中转。

Address(属于比较底层的概念)

D-Bus 系统符合 C/S 结构。使用 D-Bus 的应用程序既可以是 C 端也可以是 S 端,S 端监听到来的连接,C 端连接到 S 端,一旦连接建立,消息就可以流转。

如果使用 dbus-daemon,所有的应用程序都是 C 端,dbus-daemon 监听所有的连接,应用程序初始化时连接到 dbus-daemon。每一条 dbus-daemon 定义的总线都有一个地址,进程通过总线的地址连接到总线上。

一个 D-Bus 的地址是指 S 端用于监听、C 端将要连接的地方,例如:

1
unix:path=/tmp/some-socket.socket

上述 D-Bus 地址表示的是 S 端将在位于路径 some-socket.socket 的 UNIX Domain Socket 监听,C 端将要连接到这个地址。

除了 Unix Domain Socket,地址也可以是指定的 TCP/IP Socket 或者其他在或者将在 D-Bus 协议中定义的传输方式。

如果使用 dbus-daemon,libdbus 将通过读取环境变量 DBUS_SESSION_BUS_ADDRESS 自动获取session bus 的地址,或通过检查一个指定的 UNIX domain socket 路径获取 system bus 的地址。

如果使用 D-Bus,但不使用 dbus-daemon,需要定义哪个应用是 S 端,哪个是 C 端,并定义一套机制用于认可 S 端的地址。

Connection Name

总线上的每个连接都有一个或多个名字。当连接建立以后,D-Bus 服务会分配一个不可改变的连接名,称为 唯一连接名 (unique connection name),唯一连接名即使在进程结束后也不会再被其他进程所使用。

唯一连接名以冒号开头,如「:34-907」。但唯一连接名总是临时分配,无法确定,也难以记忆,因此应用可以要求有另外一个 容易记住的名字 (well-known name) 来对应唯一连接名。通常使用反域名来标记,例如可以用 「com.mycompany」来映射「:34-907」。

应用程序可能会要求拥有额外的 容易记住的名字 (well-known name)。例如,可以写一个规范来定义一个名字叫做 com.mycompany.TextEditor。协议可以指定自己拥有名字为 com.mycompany.TextEditor 的连接,一个路径为/com/mycompany/TextFileManager的对象,对象拥有接口org.freedesktop.FileHandler。

应用程序就可以发送消息到总线上的连接名字、对象和接口以执行方法调用。

连接名可以用于跟踪应用程序的生命周期。当应用退出(或者崩溃)时,与总线的连接将被 OS 内核关掉,总线将会发送通知,告诉剩余的应用程序。

Object

D-Bus 的对象由客户进程创建。当经由一个 D-Bus 连接收到一条消息时,消息是被发往一个对象而不是整个应用程序。

接口

每一个对象支持一个或者多个接口,接口是一组方法和信号的集合,接口定义一个对象实体的类型。

D-Bus 对接口的命名方式,类似 org.freedesktop.Introspectable (反域名)

概念与 C++ 抽象类、Java Interface 类似。D-Bus 接口——

  • 定义了接口中的方法名、信号、属性

  • 定义了当连接建立后双方的行为约定

Message

D-Bus 通信机制是通过进程间发送 消息 实现的。一般,消息用来传递远程过程的调用(calls),以及调用引发的返回值及错误(如有)。

最基本的 D-Bus 协议是一对一的通信协议。

与 Socket 通信不同,D-Bus 是面向消息的协议

D-Bus 有 4 种消息类型:

  • method_call

  • method_return

  • signal

  • error

Service,Service Name

当在总线上通讯时,应用程序用 服务名 来向其他程序标识自己。

就像一台电脑在网络上可以有多个 Hostname 一样,一个应用程序(对象)在总线上也可以暴露多个 服务(每个服务用一个服务名标识)。

只有用总线来通讯时,才有必要使用服务名。假如使用点对点的 D-Bus 通讯,使用服务名是没有必要且不支持的。

服务名一般用反域名来组织。

Qt 调用 D-Bus 接口的例子

例如如果我们想调用 com.example.SomeService 服务在 /com/example/SomeService 路径下,由 com.example.SomeInterface 提供的 DoSomething() 方法,用 Qt D-Bus 需要这样写:

1
2
3
4
5
6
7
8
9
10
#define SERVICE_NAME "com.example.SomeService"
#define OBJECT_PATH "/com/example/SomeService"
#define INTERFACE "com.example.SomeInterface"

/// 1. 创建 Interface 的本地代理
auto interface = QDBusInterface(SERVICE_NAME, OBJECT_PATH, INTERFACE,
QDBusConnection::sessionBus());

/// 2. 调用远端方法 (同步)
QDBusMessage result = interface.call("DoSomething");

QtDBus 类型系统

D-Bus 标准定义了一系列原生类型。QDBusArgument 支援这些原生类型,并将它们同 C++ 的类型进行对应。

Primitive (原始类型)

Qt 类型 D-Bus 底层类型
uchar BYTE
bool BOOLEAN
short INT16
ushort UINT16
int INT32
uint UINT32
qlonglong INT64
qulonglong UINT64
double DOUBLE
QString STRING
QDBusVariant VARIANT
QDBusObjectPath OBJECT_PATH
QDBusSignature SIGNATURE

上述类型是受 QDBusArgument 支持的原生类型。除了上述类型,QDBusArgument 还支持 QStringListQByteArray 两种类型。

Compound (构造/复杂类型)

D-Bus 允许对上述基本类型进行 3 种组合:

  • ARRAY – 在 Qt 中使用 QList<?> 构造

  • MAP / DICTIONARY – 在 Qt 中使用 QMap<K, V> 构造

  • STRUCT – 在 Qt 中使用下面方式构造 – 需要定义序列化、反序列化的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 第一步:定义 Structure 并声明它到 QVariant 中
struct MyStructure
{
int count;
QString name;
};
Q_DECLARE_METATYPE(MyStructure)

// 第二步:定义序列化方式
// Marshall the MyStructure data into a D-Bus argument
QDBusArgument &operator<<(QDBusArgument &argument, const MyStructure &mystruct)
{
argument.beginStructure();
argument << mystruct.count << mystruct.name;
argument.endStructure();
return argument;
}

// 第三步:定义反序列化方式
// DMarshall the MyStructure data from the D-Bus argument
const QDBusArgument &operator>>(const QDBusArgument &argument, MyStructure &mystruct)
{
argument.beginStructure();
argument >> mystruct.count >> mystruct.name;
argument.endStructure();
return argument;
}
  • 第一步:定义 Structure 并声明它到 QVariant 中

    • 需要使用 Q_DECLARE_METATYPE 将新定义的结构体声明到全局。
  • 第二步:定义序列化方式

    • 即重写 << 操作符号,定义将 Structure 注入 argument 中的方式

    • 序列化的过程需要用 argument.beginStructure()、argument.endStructure() 包裹

    • 在 argument.beginStructure()、argument.endStructure(),之间将 Structure 中的内容按顺序放入 argument 中。

  • 第三步:定义反序列化方式

    • 流程同上,只是方向相反。

作为参数,ARRAY、MAP 两种类型中的 值类型都必须确定,不能用 QVariant。

例如将 QList<QVariant> 类型的 List 设为某个 Argument 的参数是不被允许的。

ARRAY、MAP 如果使用 QList、QMap 数据结构,则不必定义序列化、反序列化过程。

QtDBus 示例

示例 1:查看已注册的服务

以下示例使用 QtDBus 连接系统的 Session Bus,并且获得该总线上已注册的 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void print_session_bus_registered_service_names()
{
// 1. 打开和 session bus 的连接
QDBusConnection sessionBusConn = QDBusConnection::sessionBus();
// 2. session bus 的属性接口 —— org.freedesktop.DBus 接口
QDBusConnectionInterface *sessionBusConnInt = sessionBusConn.interface();
// 3. 调用上述接口中的一个方法获取 session bus 的已注册的服务
QDBusReply<QStringList> reply = sessionBusConnInt->registeredServiceNames();
// 4. Returns true if no error occurred; otherwise, returns false.
if (!reply.isValid()) {
qDebug() << "Error:" << reply.error().message();
exit(1);
}
// 5. Returns the remote function's calls return value.
const QStringList values = reply.value();
for (const QString &name : values)
qDebug() << name;
}

细节:

  • QDBusConnection::sessionBus():是 QDBusConnection 类的静态方法,获得通向系统 Session Bus 的连接对象。该对象在当前程序结束运行前都有效。

QDBusConnection::sessionBus() 使用环境变量 DBUS_SESSION_BUS_ADDRESS 中指定的地址来连接到 Session Bus 的 dbus-daemon。

  • QDBusConnection::interface():用该方法获得某个连接的 QDBusConnectionInterface 接口。

    • 通过 QDBusConnectionInterface 接口访问到的是 D-Bus 套件定义的特殊接口 —— org.freedesktop.DBus 接口。

    • org.freedesktop.DBus 接口可以访问总线的一些属性,例如 查询当前在总线上已注册的服务、查询某个服务是否注册、查询服务 Owner 等。同时,也可以通过这一接口来注册新的服务、取消注册服务等。上面这些操作都可以在 Qt 中通过 QDBusConnectionInterface 接口实现。

  • QDBusConnectionInterface::registeredServiceNames():查询当前在总线上已注册的服务。该方法返回 QDBusReply<T> 类型的对象。

  • QDBusReply<T>:用来包裹 D-Bus 的返回值。除了返回值,还包含一些可能存在的错误信息等。

    • 该对象的 isValid() 如果返回 false,表示该返回值不可信,需要通过 error() 来获取错误资料。

    • 如果返回值是可信的,可以用 value() 获取返回值。

示例 2:查看已注册的服务 – 手动

1
2
3
4
5
6
7
8
9
10
void manually_get_session_bus_registered_service_names()
{
// 1. 打开和 session bus 的连接
QDBusConnection sessionBusConn = QDBusConnection::sessionBus();
// 2. 获取远端对象的接口
QDBusInterface dbus_iface("org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", bus);
// 3. 调用接口方法及获取返回值
qDebug() << dbus_iface.call("ListNames").arguments().at(0);
}

细节:

  • QDBusInterface(serviceName, objectPath, interfaceName, busConn, parent)

    • 是某个远端对象上的 接口 在 Qt 程序中的代理对象

    • 没有本地接口副本 的情况下,该对象会比较有用

      • 调用远端对象的方法(使用 QDBusAbstractInterface::call

      • get/set 远端对象的属性(使用 QObject::propertyQObject::setProperty

      • 将本地 SLOT 连接到远端 SIGNAL 处(使用 QObject::connect

    • 是 QObject 的子类,因此被 Qt 对象系统管理

    • 如果未有指定连接,则默认采用 Session Bus。

  • QDBusAbstractInterface::call(methodName, args...) -> QDBusMessage

    • 调用接口方法

    • 所有 args 都必须能够转换成 QVariant。

  • QDBusMessage::arguments -> QList<QVariant>:获取调用参数(当消息是调用或者 SIGNAL 类型)或返回值(当消息承载的是返回信息)。

示例 1 和示例 2 的区别是:示例 1 是 直接调用本地已有的接口代理 中定义的方法,来调用到远端接口;示例 2 是 通过 QDBusInterface 构造动态接口代理,然后通过代理来调用到远端接口。

示例 3:注册 Service 及 Object

通过 DBus 发布服务,标准做法是通过 QDBusAbstractAdapter 方案完成服务及接口的包装及注册。本示例中展现了这一过程。

现有一个非常基础的业务类:HAdder,它能够且只能完成对两个加数的相加操作。现在希望通过 DBus 搭建一款能够利用 HAdder 实现加减法的计算器服务,供其他程序调用。

在 Qt 中若要完成上述包装接口及发布需求,需要完成以下步骤:

  1. 首先,业务类 HAdder 必须继承 QObject。

  2. 新建一个接口类 IHDBusCalculator,继承于 QDBusAbstractAdapter。这一行为将使的后续发布服务对象时,IHDBusCalculator 能够被当作对象接口发布。

  3. 在 IHDBusCalculator 的 Q_OBJECT 宏之后,使用 Q_CLASSINFO(“D-Bus Interface, “cn.hanli.calculator.Calculator”) 添加表示该接口名字的元数据。

    1. 注意,元数据的 Key 必须等于 D-Bus Interface,否则不会被理会;

    2. 同时 Value 是您为该接口起的名字,通常用反域名组织;

    3. 正确地添加了该元数据的接口才会被用于接口发布。

  4. QDBusAbstractAdapter 在构造时需要传入业务类的指针,以将其正确添加到对象树中,方便其在后续发布对象的时候被 Qt 的 MOC 机制找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class IHDBusCalculator: public QDBusAbstractAdaptor {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "cn.hanli.calculator.Calculator")
public:
explicit IHDBusCalculator(HAdder *parent)
: QDBusAbstractAdaptor(parent)
, m_adder(parent)
{ }

public slots:
double performAdd(double a, double b);
double performMinus(double a, double b);

signals:
void resultOk(double res);

private:
HAdder *m_adder;
}
  1. IHDBusCalculator 将用于包装加法、减法功能并暴露给 Qt 的 D-Bus 系统,其功能实现直接调用了 HAdder 中的加法业务逻辑。

    1. Qt 需要能通过反射机制找到暴露在接口中的方法,因此接口中的方法(如上面的 performAdd、performMinus)的声明应被放在 Slot Section 中。
1
2
3
4
5
6
7
8
9
10
11
double IHDBusCalculator::performAdd(double d, double d1) {
double result = this->m_adder->performAdd(d, d1);
emit resultOk(result);
return result;
}

double IHDBusCalculator::performMinus(double d, double d1) {
double result = this->m_adder->performAdd(d, -d1);
emit resultOk(result);
return result;
}
  1. 最后,在主函数中注册业务类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
/// 1. 打开 Session Bus 的连接,然后注册服务。
QDBusConnection connection = QDBusConnection::sessionBus();
if (!connection.registerService("cn.hanli.calculator")) {
qDebug() << connection.lastError().message();
exit(1);
}
/// 2. 新建业务类以及基于该业务类的 DBus 接口类
/// 的实例,并将业务类的指针喂给接口的构造函数。
HAdder calculator;
auto *calculatorInterface = new IHDBusCalculator(&calculator);

/// 3. 往 DBus 上注册【业务类实例】。请注意,必须注册
/// 业务类 (即 HAdder) 而非接口类 (即 IHAdder) 的实例。
connection.registerObject("/", &calculator);

return QCoreApplication::exec();
}

整个程序的类图安排:

Class MapClass Map

几个类分别负责的事情:

  • QDBusConnection:负责注册服务、注册对象;

  • HAdder:完成底层业务(加法);

  • IHDBusCalculator,IHDBusCalculator:包装底层业务,定义加法、减法功能接口

  • QDBusAbstractAdapter:标记 IHDBusCalculator,标记它们是接口类

运行程序后,使用 D-Feet 可以查看到已发布的服务。