Linux D-Bus 详解

前言

在构建复杂应用或服务时,经常会遇到进程间通信的问题。DBus 提供了一个高效、可靠的解决方案,使得不同程序之间可以轻松地进行消息交换和远程调用。本篇文章将带你详细了解 DBus 的工作机制及如何在你的项目中有效利用这一技术。

什么是 D-Bus?

D-Bus 是 Linux 及其他类 Unix 系统下的一种进程间通信机制(IPC)。那什么是进程间通信机制(IPC)呢?IPC(Inter-process communication)是操作系统提供给进程用于管理共享数据的一套机制,简单来说,如果进程 A 和进程 B 要进行通信(发送数据),就要通过 IPC,如下图所示:

image.png

除了 D-Bus,Linux 常见的进程间通信(IPC)还有如下几种:

  • 共享内存(Shared Memory):多个进程直接访问同一块内存区域,适用于需要高速数据交换的场景。
  • 管道(Pipes)
    • 无名管道:父子进程之间的通信。
    • 命名管道:不相关的进程通过一个具有名称的特殊文件进行通信。
  • 套接字(Sockets):支持不同机器间或同一机器内进程间的通信,常见于网络编程。
  • 信号(Signals):向进程发送异步通知,常用于中断、控制进程或通知事件发生。
  • 文件系统(File-based Communication):通过普通的文件读写操作,多个进程共享文件来交换数据。

既然有这么多 IPC,那为什么还需要 D-Bus 呢?这里就要说到 D-Bus 的出生背景,D-Bus 最初由 GNOME(桌面环境) 开发者 Havoc Pennington 发起,GNOME 和 KDE 作为两个主流的桌面环境,各自都有自己独立的进程间通信方案(如 GNOME 使用 Bonobo,KDE 使用 DCOP)。这两者的兼容性问题显著影响了 Linux 桌面应用程序的整合和一致性。因此,D-Bus 的设计目标之一就是消除这些碎片化的通信机制,提供一个通用的 IPC 解决方案。

D-Bus 最初用于桌面环境,后来使用范围逐渐扩展,包含的系统服务越来越多。例如 NetworkManager 网络守护进程、BlueZ 蓝牙堆栈都使用 D-Bus 来提供其部分或全部的服务,systemd 也正促使传统的系统守护进程(如 logind)转换到 D-Bus 服务。所以 D-Bus 还有一个非常大的用途是用来进行硬件控制(比如通过 NetworkManager 服务来操作 wifi 或通过 BlueZ 服务来操作蓝牙)。

D-Bus 总线

D-Bus 提供了一种软件总线抽象,它将一组进程间的所有通信集中在一个共享的虚拟通道上。连接到总线的进程并不知道其内部实现细节,但 D-Bus 规范保证了所有连接到总线的进程都能通过它相互通信。

image.png

D-Bus 提供了两种总线:

  • 系统总线(system bus):系统总线是整个系统范围的,所有用户和系统服务都可以访问它。系统总线通常用于系统级别的进程和服务之间的通信,比如与硬件、系统守护进程(如 systemdNetworkManager)等的交互。普通用户想要访问系统总线,则需要以 root 用户身份修改 D-Bus 的配置文件让其生效。
  • 会话总线(session bus):会话总线是为每个用户会话创建的,它仅对当前登录的用户的进程可见,其他用户的进程无法访问同一个会话总线。每个用户在他们的会话中都有一个独立的会话总线。

image.png

D-Bus 的配置文件位于 /usr/share/dbus-1 目录中。系统总线和会话总线的配置文件 system.confsession.conf 分别在 /etc/dbus-1 目录下。

D-Bus 通信模型

D-Bus 采用了 客户端-服务器(CS)模型的通信方式,但它中间多了一层消息总线,消息总线起到中央枢纽的角色,类似 MQTT 的 broker。进程既可以提供服务也可以作为客户端请求服务。

要让某个进程提供一个服务或作为客户端请求一个服务,需要提供如下参数:

  • 服务名称(Service Name):注册在 D-Bus 总线上服务的唯一名称
  • 对象路径(Object Path):类似文件系统中的路径,每个 D-Bus 服务可以注册一个或多个对象,路径用来指定具体的对象,例如 /org/freedesktop/DBus/com/example/MyService/MyObject
  • 接口(Interface):定义了对象可以响应的具体方法(方法集)。接口名称通常也是域名反转形式,例如 org.freedesktop.DBuscom.example.MyService.Interface。每个对象可以实现多个接口,通过接口来定义具体能调用的方法。
  • 通信模式:接口提供了 3 种通信模式,分别是:
    • 方法调用(Method Call):方法类似于编程语言的函数,可以传入不同参数实现不同的效果或返回不同的接口。
    • 属性(Property):属性相当于编程语言的变量,可以读取或设置变量。
    • 信号(Signal):类似于回调函数,当某个事件触发(比如通知事件)时,向 D-Bus 总线广播信号,通知其他进程某些事件的发生。

image.png

D-Bus 消息类型

D-Bus 支持 4 种类型的消息,消息类型决定了消息的路由方式、目标以及如何处理:

  • 方法调用(Method Call):调用方法(Method)和获取、设置属性(Property)都属于此类消息,发送者会等待目标对象返回结果或错误消息。
  • 方法返回(Method Return):当发送者调用方法后,会等待方法调用的响应,它属于此类消息,此类型消息包含了执行结果,返回一个或多个结果值。
  • 错误消息(Error Message):方法调用发生错误时会返回此消息,包含错误的原因。
  • 信号消息(Signal Message):信号消息用于广播信息,即某些事件的发生,通常用于通知其他进程系统状态的变化或某个操作的结果。信号是 无请求 的,即不需要响应。

image.png

D-Bus 命令调试

在编写程序来使用 D-Bus 之前,一般会先使用命令来测试我们想要的结果。D-Bus 提供了如下的命令以供我们通过命令来进行调试:

  • busctl:一个用于与 D-Bus 总线进行交互的命令行工具,提供了更多管理和控制 D-Bus 总线的功能。
  • dbus-send:一个较旧的 D-Bus 工具,用于向 D-Bus 总线发送消息。
  • dbus-monitor:一个用于监视 D-Bus 消息流的命令行工具。它可以捕捉并显示通过 D-Bus 总线发送的消息和信号。
  • d-feet:一个易于使用的 D-Bus GUI 调试器。

比如我想查看 org.freedesktop.NetworkManager 服务(一个网络管理工具)的 /org/freedesktop/NetworkManager 对象路径提供了哪些接口以及方法属性信号,可以使用如下命令:busctl introspect org.freedesktop.NetworkManager /org/freedesktop/NetworkManager。它会打印如下信息:

NAME                                TYPE      SIGNATURE        RESULT/VALUE                             FLAGS
org.freedesktop.DBus.Introspectable interface -                -                                        -
.Introspect                         method    -                s                                        -
org.freedesktop.DBus.Peer           interface -                -                                        -
.GetMachineId                       method    -                s                                        -
.Ping                               method    -                -                                        -
org.freedesktop.DBus.Properties     interface -                -                                        -
.Get                                method    ss               v                                        -
.GetAll                             method    s                a{sv}                                    -
.Set                                method    ssv              -                                        -
.PropertiesChanged                  signal    sa{sv}as         -                                        -
org.freedesktop.NetworkManager      interface -                -                                        -
.ActivateConnection                 method    ooo              o                                        -
.AddAndActivateConnection           method    a{sa{sv}}oo      oo                                       -
.AddAndActivateConnection2          method    a{sa{sv}}ooa{sv} ooa{sv}                                  -
.CheckConnectivity                  method    -                u                                        -
.CheckpointAdjustRollbackTimeout    method    ou               -                                        -
.CheckpointCreate                   method    aouu             o                                        -
.CheckpointDestroy                  method    o                -                                        -
.CheckpointRollback                 method    o                a{su}                                    -
.DeactivateConnection               method    o                -                                        -
.Enable                             method    b                -                                        -
.GetAllDevices                      method    -                ao                                       -
.GetDeviceByIpIface                 method    s                o                                        -
.GetDevices                         method    -                ao                                       -
.GetLogging                         method    -                ss                                       -
.GetPermissions                     method    -                a{ss}                                    -
.Reload                             method    u                -                                        -
.SetLogging                         method    ss               -                                        -
.Sleep                              method    b                -                                        -
.state                              method    -                u                                        -
.ActivatingConnection               property  o                "/"                                      emits-change
.ActiveConnections                  property  ao               0                                        emits-change
......

具体的用法可以通过 命令 --help 查看。

D-Bus 数据类型

在D-Bus 中,也有类似编程语言中变量类型的概念,比如字符串类型、布尔类型等等,当我们调用一个方法,需要传入指定的参数类型,如果传的类型不对,则会导致调用方法失败。

类型分别如下两种:

  • 基础类型(Basic types):类似于 C/C++、Python 等语言的基本数据类型。
  • 容器类型(Container types):允许组合或嵌套其他类型,形成复杂的结构。

基本类型有如下:

  • y (Byte): 无符号 8 位整数 (uint8_t)
  • b (Boolean): 布尔类型,truefalse
  • n (Int16): 有符号 16 位整数 (int16_t)
  • q (UInt16): 无符号 16 位整数 (uint16_t)
  • i (Int32): 有符号 32 位整数 (int32_t)
  • u (UInt32): 无符号 32 位整数 (uint32_t)
  • x (Int64): 有符号 64 位整数 (int64_t)
  • t (UInt64): 无符号 64 位整数 (uint64_t)
  • d (Double): 双精度浮点数 (double)
  • s (String): UTF-8 编码的字符串
  • o (Object Path): 表示 D-Bus 对象路径的字符串
  • g (Signature): 表示 D-Bus 类型签名的字符串
  • h (Unix File Descriptor): 文件描述符,用于传递文件句柄

容器类型有如下:

  • a (Array): 数组类型。数组中所有元素必须是相同的数据类型。表示为 a<type>,例如 ai 表示 int32_t 的数组。
  • v (Variant): 可变类型,类似于 C++ 的 std::variant,可以存储任意 D-Bus 类型。
  • ( ) (Struct): 结构类型,表示为多个不同类型的组合。类似于 C/C++ 中的结构体。例如,(is) 表示一个包含 int32_tstring 的结构体。
  • { } (Dictionary/Dict Entry): 字典条目,表示键值对。通常表示为 a{key_type value_type},例如 a{su} 表示键为 string,值为 uint32_t 的字典。

D-Bus 用户空间库

不同的编程语言都提供了相关的库供应用程序进行调用,一般常用的如下:

  • libdbuslibdbus 是 D-Bus 最基础的用户空间库,它提供了与 D-Bus 总线进行通信的功能。libdbus 是 C 语言编写的库,提供了低层次的接口来发送、接收消息以及管理 D-Bus 连接。它是 D-Bus 系统的核心库,许多 D-Bus 相关的工具和库都依赖它。
  • GDBus (GLib-based)GDBus 是 GLib 提供的一个 D-Bus 实现,它是通过 libdbus 进行封装,并提供了更加高层次、面向对象的接口,通常用于 GNOME 和其他基于 GLib 的应用程序。
  • dbus-c++dbus-c++ 是一个 C++ 封装库,提供了封装的类和函数。它是一个较老的库,维护和更新的频率较低,不推荐使用。
  • dbus-cxx:采用现代化 C++ 进行开发,提供了更强大的 D-Bus Api 实现,推荐使用。
  • Qt D-BusQt D-Bus 是 Qt 库的一个模块,提供了对 D-Bus 的支持。它使得开发者能够在 Qt 应用程序中通过 D-Bus 进行进程间通信。
  • PyDBus (Python)PyDBus 是一个 Python 库,提供了高层次的 Python 接口。
  • D-Bus for Java:这是一个为 Java 语言提供的 D-Bus 客户端库。

C++ 版代码示例

下面以 dbus-cxx 库为例,给出相关的 D-Bus 代码示例。

作为服务:

#include <dbus-cxx.h>
#include <thread>
#include <chrono>

double add(double param1, double param2) {
    return param1 + param2;
}

int main() {
    std::shared_ptr<DBus::Dispatcher> dispatcher = DBus::StandaloneDispatcher::create();

    // 创建一个连接对象 `conn`,表示一个到 D-Bus 总线的连接。此处连接到用户会话总线。
    std::shared_ptr<DBus::Connection> conn = dispatcher->create_connection(DBus::BusType::SESSION);

    // 请求在 D-Bus 上注册服务名称 "dbuscxx.quickstart_0.server"。
    // `DBUSCXX_NAME_FLAG_REPLACE_EXISTING` 标志表示如果该名称已存在,则替换原服务。
    if (conn->request_name("dbuscxx.quickstart_0.server", DBUSCXX_NAME_FLAG_REPLACE_EXISTING) != DBus::RequestNameResponse::PrimaryOwner)
        return 1;  // 如果无法获得该名称的所有权,程序退出。

    // 创建一个对象,允许其他进程通过 D-Bus 调用该对象的方法。
    // "/dbuscxx/quickstart_0" 是该对象在 D-Bus 上的路径。
    // `DBus::ThreadForCalling::DispatcherThread` 表示该对象方法将在 D-Bus 分派线程中被调用。
    std::shared_ptr<DBus::Object> object = conn->create_object("/dbuscxx/quickstart_0", DBus::ThreadForCalling::DispatcherThread);

    // 创建一个方法 "add",该方法可以通过 D-Bus 调用。
    // `sigc::ptr_fun(add)` 将 C++ 函数 `add` 转换为可以在 D-Bus 中使用的函数对象。
    // 该方法接收两个 `double` 类型的参数并返回一个 `double` 类型的值。
    object->create_method<double(double, double)>("dbuscxx.Quickstart", "add", sigc::ptr_fun(add));

    // 让程序运行 10 秒钟,保持 D-Bus 服务的活跃。
    // 在这段时间内,其他进程可以调用该服务的 `add` 方法。
    std::this_thread::sleep_for(std::chrono::seconds(10));

    return 0;
}

作为客户端:

#include <dbus-cxx.h>
#include <iostream>

int main()
{
  std::shared_ptr<DBus::Dispatcher> dispatcher = DBus::StandaloneDispatcher::create();

  std::shared_ptr<DBus::Connection> connection = dispatcher->create_connection( DBus::BusType::SESSION );

  // 创建一个对象代理 `object`,它代表了 D-Bus 上的远程对象。
  // 这里的代理对象代表了一个名为 "dbuscxx.quickstart_0.server" 的服务
  // 和该服务上的对象路径 "/dbuscxx/quickstart_0"。
  std::shared_ptr<DBus::ObjectProxy> object = connection->create_object_proxy("dbuscxx.quickstart_0.server", "/dbuscxx/quickstart_0");

  // 创建方法代理 `add_proxy`,该代理方法将通过 D-Bus 调用远程服务的方法。
  // 这里的代理方法代表服务对象上的 "add" 方法,它接受两个 `double` 类型的参数。
  DBus::MethodProxy<double(double,double)>& add_proxy
    = *(object->create_method<double(double,double)>("dbuscxx.Quickstart","add"));

  double answer;
  // 使用方法代理调用远程 D-Bus 服务的 `add` 方法。此方法是阻塞调用,直到远程方法执行完毕。
  answer = add_proxy( 1.1, 2.2 );

  std::cout << "1.1 + 2.2 = " << answer << std::endl;

  return 0;
}

参考文献

  • https://dbus.freedesktop.org/doc/dbus-tutorial.html
  • https://en.wikipedia.org/wiki/D-Bus
  • https://www.softprayog.in/programming/d-bus-tutorial
  • https://dbus.freedesktop.org/doc/dbus-daemon.1.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值