D-Bus 教程

内容

教程制作中

本教程尚未完成;其中可能包含一些有用信息,但也存在许多空白。目前,您还需要参考 D-Bus 规范、Doxygen 参考文档,并查看其他应用程序如何使用 D-Bus 的一些示例。

增强教程绝对是受鼓励的 - 将您的补丁或建议发送到邮件列表。如果您创建了一个 D-Bus 绑定,请为您的绑定添加一个教程部分,即使只是一个包含几个示例的简短部分。

D-Bus 是一种用于进程间通信(IPC)的系统。在架构上,它有几个层次:

  • 一个库,libdbus,允许两个应用程序连接到彼此并交换消息。
  • 一个建立在 libdbus 上的 消息总线守护进程 可执行文件,多个应用程序可以连接到它。该守护进程可以将来自一个应用程序的消息路由到零个或多个其他应用程序。
  • 基于特定应用程序框架的包装库或绑定。例如,libdbus-glib 和 libdbus-qt。还有针对诸如 Python 等语言的绑定。这些包装库是大多数人应该使用的 API,因为它们简化了 D-Bus 编程的细节。libdbus 旨在成为更高级绑定的低级后端。libdbus API 的许多部分仅对绑定实现有用。

libdbus仅支持一对一连接,就像原始网络套接字一样。但是,与其通过连接发送字节流,你发送_消息_。消息具有标识消息类型的标头,以及包含数据有效负载的主体。libdbus还抽象了使用的确切传输方式(套接字等),并处理诸如身份验证之类的细节。

消息总线守护程序形成了一个轮子的中心。轮子的每个辐都是与使用 libdbus 的应用程序之间的一对一连接。应用程序通过其辐将消息发送到总线守护程序,总线守护程序将消息适当地转发给其他连接的应用程序。将守护程序视为路由器。

总线守护程序在典型计算机上有多个实例。第一个实例是一个机器全局的单例,即类似于sendmail或Apache的系统守护程序。该实例对其将接受的消息有严格的安全限制,并用于系统范围的通信。其他实例是每个用户登录会话创建的。这些实例允许用户会话中的应用程序相互通信。

系统范围和每个用户的守护进程是分开的。正常的会话内进程间通信不涉及系统范围的消息总线进程,反之亦然。

世界上有许多技术都在其规定目的中具有“进程间通信”或“网络通信”:CORBA, DCE, DCOM, DCOP, XML-RPC, SOAP, MBUS, Internet Communications Engine (ICE),可能还有数百种。每种技术都针对特定类型的应用程序。D-Bus专为两种特定情况而设计:

  • 桌面应用程序在相同桌面会话中的通信;以允许整个桌面会话的集成,并解决进程生命周期的问题(桌面组件何时启动和停止运行)
  • 桌面会话与操作系统之间的通信,操作系统通常包括内核和任何系统守护程序或进程。

对于桌面会话内部使用情况,GNOME 和 KDE 桌面在不同的 IPC 解决方案(如 CORBA 和 DCOP)方面有着丰富的经验。D-Bus 基于这些经验构建,并经过精心设计以满足这些桌面项目的特定需求。D-Bus 可能适用于其他应用程序,也可能不适用;FAQ 中有一些与其他 IPC 系统的比较。

系统范围或与操作系统通信的情况解决的问题在 Linux Hotplug 项目的以下文本中得到了很好的解释:

目前 Linux 支持中存在的一个空白是,目前不支持带有任何动态“与用户交互”组件的策略。例如,这通常在首次连接网络适配器或打印机时需要,并且需要确定适当的磁盘驱动器挂载位置。似乎这样的操作可以支持任何可以识别负责人的情况:单用户工作站,或任何远程管理的系统。

这是一个经典的“远程系统管理员”问题,其中在这种情况下,热插拔需要将事件从一个安全域(操作系统内核,在这种情况下)传递到另一个(桌面用于已登录用户,或远程系统管理员)。任何有效的响应必须反向进行:远程域采取某些行动,使内核暴露所需设备功能。(该操作通常可以异步执行,例如让新硬件处于空闲状态直到会议结束。)截至目前,Linux 没有被广泛采用的解决这类问题的方案。然而,新的 D-Bus 工作可能开始解决这个问题。

D-Bus可能会被用于设计之外的其他用途。与其他形式的IPC区分开来的一般特性是:

  • 二进制协议旨在异步使用(类似于X Window系统协议的精神)。
  • 随时间保持的有状态、可靠的连接。
  • 消息总线是一个守护程序,而不是“swarm”或分布式架构。
  • 许多实施和部署问题都是具体指定的,而不是模棱两可的/可配置的/可插拔的。
  • 语义与现有的 DCOP 系统类似,使得 KDE 更容易采用它。
  • 支持消息总线系统范围模式的安全功能。

无论您使用哪种应用程序框架来编写 D-Bus 应用程序,一些基本概念都是适用的。但是,您编写的确切代码在 GLib、Qt 和 Python 应用程序中会有所不同。

这里是一个图表(png svg),可能有助于您理解接下来的概念。

本地对象和对象路径

你的编程框架可能会定义"对象"的样子;通常会有一个基类。例如:java.lang.Object,GObject,QObject,Python的基础Object,或其他。我们称之为 native object

低级别的 D-Bus 协议和相应的 libdbus API 不关心本地对象。但是,它提供了一个称为 object path 的概念。对象路径的概念是,更高级别的绑定可以为本地对象实例命名,并允许远程应用程序引用它们。

对象路径看起来像文件系统路径,例如一个对象可以被命名为/org/kde/kspread/sheets/3/cells/4/5。可读性强的路径很好,但如果对您的应用程序有意义,您可以自由创建一个名为/com/mycompany/c5yo817y0c1y1c5b的对象。

通过以您拥有的域名的组件开头(例如 /org/kde),对对象路径进行命名空间处理是明智的。这样可以确保同一进程中的不同代码模块不会相互干扰。

每个对象都有 成员;成员有两种类型,即 方法信号。方法是可以在对象上调用的操作,可以有可选的输入(也称为参数或“输入参数”)和输出(也称为返回值或“输出参数”)。信号是对象向任何对对象感兴趣的观察者广播的信息;信号可能包含数据负载。

方法和信号都通过名称引用,例如“Frobate”或“OnClicked”。

每个对象支持一个或多个 接口。将接口视为一组命名的方法和信号,就像在GLib、Qt或Java中一样。接口定义对象实例的 类型

DBus 用一个简单的命名空间字符串来标识接口,类似 org.freedesktop.Introspectable。大多数绑定将直接将这些接口名称映射到适当的编程语言结构,例如 Java 接口或 C++ 纯虚类。

代理对象 是一个方便的本地对象,用于表示另一个进程中的远程对象。低级别的 DBus API 需要手动创建方法调用消息,发送消息,然后手动接收和处理方法回复消息。更高级别的绑定提供了代理作为一种替代方案。代理看起来像一个普通的本地对象;但当您在代理对象上调用方法时,绑定会将其转换为一个 DBus 方法调用消息,等待回复消息,解包返回值,并从本地方法返回它。

在伪代码中,没有代理的编程可能看起来像这样:

      Message message = new Message("/remote/object/path", "MethodName", arg1, arg2);
      Connection connection = getBusConnection();
      connection.send(message);
      Message reply = connection.waitForReply(message);
      if (reply.isError()) {
      } else { Object returnValue = reply.getReturnValue(); }

使用代理进行编程可能如下所示:

      Proxy proxy = new Proxy(getBusConnection(), "/remote/object/path");
      Object returnValue = proxy.MethodName(arg1, arg2);

当每个应用程序连接到总线守护程序时,守护程序立即为其分配一个名字,称为 唯一连接名。唯一名以 ':'(冒号)字符开头。这些名字在总线守护程序的生命周期内永不重复使用 - 换句话说,您知道给定的名字将始终指向同一个应用程序。唯一名的示例可能是 :34-907。冒号后面的数字除了它们的唯一性外没有其他含义。

当一个名称映射到特定应用程序的连接时,该应用程序被认为 拥有 该名称。

应用程序可能会请求拥有额外的_众所周知的名称_。例如,您可以编写一个规范来定义一个名为com.mycompany.TextEditor的名称。您的定义可以指定,要拥有此名称,应用程序应在路径/com/mycompany/TextFileManager上拥有一个支持接口org.freedesktop.FileHandler的对象。

应用程序可以向该总线名称、对象和接口发送消息以执行方法调用。

你可以将唯一名称看作 IP 地址,将知名名称看作域名。因此 com.mycompany.TextEditor 可能映射到类似 :34-907 的内容,就像 mycompany.com 映射到类似 192.168.0.5 的内容一样。

名称除了用于路由消息之外,还有第二个重要用途,用于跟踪生命周期。当应用程序退出(或崩溃)时,其与消息总线的连接将被操作系统内核关闭。然后,消息总线发送通知消息,告知其余应用程序该应用程序的名称已失去所有者。通过跟踪这些通知,您的应用程序可以可靠地监视其他应用程序的生命周期。

总线名称也可以用于协调单实例应用程序。例如,如果您希望确保只有一个 com.mycompany.TextEditor 应用程序在运行,则可以让文本编辑应用程序在总线名称已经有所有者时退出。

使用 D-Bus 的应用程序可以是服务器或客户端。服务器监听传入连接;客户端连接到服务器。一旦建立连接,消息的流动是对称的;客户端和服务器的区别只在建立连接时才重要。

如果您正在使用总线守护程序,那么您的应用程序很可能是总线守护程序的客户端。也就是说,总线守护程序会监听连接,而您的应用程序会发起与总线守护程序的连接。

D-Bus 地址 指定服务器将监听的位置,以及客户端将连接的位置。例如,地址 unix:path=/tmp/abcdef 指定服务器将在路径 /tmp/abcdef 处的 UNIX 域套接字上监听,并且客户端将连接到该套接字。地址还可以指定 TCP/IP 套接字,或者在未来的 D-Bus 规范迭代中定义的任何其他传输方式。

当使用 D-Bus 与消息总线守护程序时,libdbus 通过读取环境变量自动发现每个会话总线守护程序的地址。它通过检查一个众所周知的 UNIX 域套接字路径来发现系统范围的总线守护程序(尽管您可以通过环境变量覆盖此地址)。

如果您在没有总线守护程序的情况下使用 D-Bus,则由您定义哪个应用程序将是服务器,哪个将是客户端,并指定一个机制让它们就服务器的地址达成一致。这是一个不寻常的情况。

将所有这些概念汇总起来,为了指定在特定对象实例上调用特定方法,必须命名一系列嵌套组件:

      地址 -> \\[公交名称\\] -> 路径 -> 接口 -> 方法

巴士名称用括号括起来表示是可选的 -- 只有在使用巴士守护程序时才需要提供名称,以便将方法调用路由到正确的应用程序。如果直接连接到另一个应用程序,则不使用巴士名称;没有巴士守护程序。

接口也是可选的,主要是出于历史原因;DCOP 不需要指定接口,而是简单地禁止在同一对象实例上使用重复的方法名称。因此,D-Bus 允许您省略接口,但如果您的方法名称不明确,则无法确定将调用哪个方法。

消息 - 幕后

D-Bus通过在进程之间发送消息来工作。如果您使用的是足够高级的绑定,您可能永远不会直接处理消息。

有 4 种消息类型:

  • 调用方法消息要求在对象上调用方法。
  • 方法返回消息返回调用方法的结果。
  • 错误消息返回由调用方法引起的异常。
  • 信号消息是指发出的信号已被发出的通知(事件已发生)。您也可以将其视为“事件”消息。

方法调用与消息非常简单地对应:您发送一个方法调用消息,然后收到一个方法返回消息或错误消息作为回复。

每条消息都有一个 头部,包括 字段,和一个 主体,包括 参数。您可以将头部视为消息的路由信息,将主体视为有效载荷。头部字段可能包括发送者总线名称、目的地总线名称、方法或信号名称等。头部字段之一是描述主体中找到的值的 类型签名。例如,字母“i”表示“32位整数”,因此签名“ii”表示有效载荷包含两个32位整数。

调用方法 - 幕后

DBus中的方法调用由两条消息组成:从进程A发送到进程B的方法调用消息,以及从进程B发送到进程A的匹配方法回复消息。调用和回复消息都通过总线守护程序路由。调用者在每个调用消息中包含不同的序列号,回复消息包含此序列号,以允许调用者将回复与调用匹配。

调用消息将包含方法的任何参数。回复消息可能指示错误,也可能包含方法返回的数据。

DBus 中的方法调用过程如下:

  • 语言绑定可能提供代理,这样在进程内对象上调用方法会在另一个进程中的远程对象上调用方法。如果是这样,应用程序会在代理上调用一个方法,代理会构造一个方法调用消息发送到远程进程。
  • 对于更低级别的 API,应用程序可以自行构造方法调用消息,而不使用代理。
  • 在任何一种情况下,方法调用消息包含:属于远程进程的总线名称;方法的名称;方法的参数;远程进程内的对象路径;以及可选的指定方法的接口的名称。
  • 方法调用消息被发送到总线守护程序。
  • 总线守护程序查看目标总线名称。如果某个进程拥有该名称,则总线守护程序将方法调用转发给该进程。否则,总线守护程序创建一个错误消息,并将其作为方法调用消息的回复发送回去。
  • 接收过程解包方法调用消息。在简单的低级API情况下,可能会立即运行该方法并向总线守护程序发送方法回复消息。当使用高级绑定API时,绑定可能会检查对象路径、接口和方法名称,并将方法调用消息转换为对本地对象(GObject、java.lang.Object、QObject等)方法的调用,然后将本地方法的返回值转换为方法回复消息。
  • 总线守护程序接收方法回复消息并将其发送给发起方法调用的进程。
  • 调用方法的过程会查看方法的回复,并利用回复中包含的任何返回值。回复还可能指示发生了错误。在使用绑定时,方法回复消息可能会转换为代理方法的返回值,或转换为异常。

总线守护程序永远不会重新排序消息。也就是说,如果您向同一接收者发送两条方法调用消息,它们将按照发送顺序接收。但是,接收者不需要按顺序回复调用;例如,它可以在单独的线程中处理每个方法调用,并根据线程完成的时间以未定义的顺序返回回复消息。方法调用具有唯一的序列号,方法调用者使用该序列号将回复消息与调用消息匹配。

发出信号 - 幕后

DBus中的信号由一个进程发送给任意数量的其他进程的单个消息组成。也就是说,信号是单向广播。信号可能包含参数(数据有效载荷),但由于它是广播,因此永远不会有“返回值”。与方法调用相比(请参阅“调用方法 - 幕后”一节),在方法调用消息中有一个匹配的方法回复消息。

信号的发射器(又称发送器)对信号的接收者一无所知。接收者通过总线守护程序注册,根据“匹配规则”接收信号 - 这些规则通常包括发送者和信号名称。总线守护程序仅将每个信号发送给对该信号表示兴趣的接收者。

DBus 中的信号发生如下:

  • 创建信号消息并发送给总线守护程序。在使用低级 API 时,这可能需要手动完成;而在某些绑定中,当本机对象发出本机信号或事件时,绑定可能会为您完成此操作。
  • 信号消息包含指定信号的接口名称;信号的名称;发送信号的进程的总线名称;以及任何参数
  • 消息总线上的任何进程都可以注册“匹配规则”,指示它感兴趣的信号。总线上有一个已注册匹配规则的列表。
  • 总线守护程序检查信号并确定哪些进程对其感兴趣。它将信号消息发送给这些进程。
  • 每个接收到信号的进程都会决定如何处理它;如果使用绑定,绑定可能选择在代理对象上发出本机信号。如果使用低级API,进程可能只是查看信号发送者和名称,并根据那个来决定如何处理。

D-Bus对象可能支持接口org.freedesktop.DBus.Introspectable。该接口有一个名为Introspect的方法,不带参数并返回一个XML字符串。XML字符串描述了对象的接口、方法和信号。有关此内省格式的更多详细信息,请参阅D-Bus规范。

GLib 中推荐的 D-Bus API 是 GDBus,自 GLib 2.26 版本起已经随 GLib 一起发布。这里不提供文档。有关如何使用 GDBus 的详细信息,请参阅GLib文档

一个更旧的API,dbus-glib,也存在。它已被弃用,不应在新代码中使用。尽可能将现有代码从dbus-glib迁移到GDBus也是推荐的。

Python API,dbus-python,现在在dbus-python教程中单独记录(如果使用python-docutils构建,则还可在doc/tutorial.txt和doc/tutorial.html中找到,这些在dbus-python源代码分发中)。

Qt绑定libdbus的QtDBus自Qt 4.2版本起已随Qt一起分发。这里不提供文档。有关如何使用QtDBus的详细信息,请参阅Qt文档

总结
这篇文章介绍了D-Bus系统的基本概念和架构,包括libdbus库、消息总线守护进程、包装库或绑定、对象路径、接口、代理对象等内容。文章强调了D-Bus的设计目的是用于桌面应用程序之间的通信以及与操作系统之间的通信。此外,还介绍了D-Bus与其他IPC系统的比较,以及其异步二进制协议、可靠连接、消息总线守护进程等特性。最后,文章提到了在不同应用框架下编写D-Bus应用程序的基本概念。