Qt高级——D-Bus快速入门
一、D-Bus简介
1、D-Bus简介
D-Bus是Desktop Bus的缩写,是针对桌面环境优化的IPC(InterProcess Communication)机制,用于进程间的通信或进程与内核的通信。 D-Bus是为Linux系统开发的进程间通信(IPC)和远程过程调用(RPC)机制,使用统一的通信协议来代替现有的各种IPC解决方案。D-Bus允许系统级进程(如:打印机和硬件驱动服务)和普通用户进程进行通信。 D-Bus使用一个快速的二进制消息传递协议,D-Bus协议的低延迟和低消耗特点适用于同一台机器的通信。D-Bus的规范目前由freedesktop.org项目定义,可供所有团体使用。 D-Bus不和低层的IPC直接竞争,比如sockets,shared memory或message queues。低层IPC有自己的特点,和D-Bus并不冲突。 与其他重量级的进程间通信技术不同,D-Bus是非事务的。D-Bus使用了状态以及连接的概念,比UDP等底层消息传输协议更“聪明”。但另一方面,D-Bus传送的是离散消息,与TCP协议将数据看做“流”有所不同。D-Bus支持点对点的消息传递以及广播/订阅式的通信。
2、不同IPC通信方式比较
不同IPC通信机制的特点如下: A、CORBA是用于面向对象编程中复杂IPC的一个强大的解决方案。 B、Bonobo是一个只用于GNOME的解决方案,基于CORBA并依赖于GObject。 C、DCOP是一个较轻量级的IPC框架,功能较少,但可以很好地集成到KDE桌面环境中。 D、SOAP和XML-RPC设计用于Web服务,因而使用HTTP作为其传输协议。 E、D-BUS设计用于桌面应用程序和OS通信。
3、D-Bus特性
A、D-BUS的协议是低延迟而且低开销的,设计小巧且高效,以便最小化传送时间。从设计上避免往返交互并允许异步操作。 B、协议是二进制的,而不是文本,排除序列化过程。 C、考虑了字节序问题。 D、易用性:按照消息而不是字节流来工作,并且自动地处理了许多困难的IPC问题,并且D-Bus库以可以封装的方式来设计,开发者可以使用框架里存在的对象/类型系统,而不用学习一种新的专用于IPC的对象/类型系统。 E、请求时启动服务以及安全策略。 F、支持多语言(C/C++/Java/C#/Python/Ruby),多平台(Linux/windows/maemo)。 G、采用C语言,而不是C++。 H、由于基本上不用于internet上的IPC,因此对本地IPC进行了特别优化。 I、提供服务注册,理论上可以进行无限扩展。
二、D-Bus架构
1、D-Bus架构简介
D-Bus是按一定的层次结构实现的,总体上D-Bus分为三层:
A、接口层——接口层由libdbus库提供,进程通过libdbus库使用D-Bus的能力。通过底层库的接口可以实现两个进程之间进行连接并发送消息。
B、总线层——由消息总线守护进程(message bus daemon )提供,消息总线守护进程是基于libdbus底层库的,可以路由消息。消息总线守护进程负责进程间的消息路由和传递,其中包括Linux内核和Linux桌面环境的消息传递。
C、封装层——封装层是一系列基于特定应用程序框架的Wrapper库,将D-Bus底层接口封装成方便用户使用的通用API。
2、D-Bus接口层
libdbus只支持点对点的通信,即只支持一进程与另外的一个进程进行通信。通信是基于消息的,消息包含头部和消息体。 libdbus提供C语言的底层API,API是为了将D-Bus绑定到特定的对象或是语言而设计的,官方文档中建议不要在应用上直接使用D-Bus的底层接口,推荐使用D-Bus的绑定,如QtDBus、GDBus、dbus-c++等实现。
3、D-Bus总线层
D-Bus总线层由消息总线守护进程(message bus daemon )提供。消息总线守护进程是一个后台进程,是/usr/bin/dbus-daemon的一个运行实例, 负责消息的转发,dbus-daemon运行时会调用libdus的库。应用程序调用特定的应用程序框架的Wrapper库与dbus-daemon进行通信。应用程序通过D-Bus与其它进程通信必须先建立到消息总线守护进程实例的连接。
最常见的基于dbus的程序也符合C/S结构。比如开发两个程序A和B,其中A是客户端,B是服务端。假设A要调用B的一个函数func,那么实际的消息流动方向是:A告诉dbus-daemon请求要调用B的func函数,然后dbus-daemon去调用B的func函数,如果func有返回值的话,B会把返回值告诉dbus-daemon,然后dbus- daemon再把返回值告诉A。如果B进程还没有启动,则dbus-daemon会自动的先把B进程启动起来。
通常情况下,Linux会有两个dbus-daemon进程,一个属于system,一个属于session,在用户登录的时候由dbus-launch启动。
大多数普通程序,都是使用session的dbus-daemon,默认情况下,A就是将消息发给属于session的dbus-daemon。
dbus-daemon是有地址的,环境变量DBUS_SESSION_BUS_ADDRESS用于表示当前登录用户的session的dbus-daemon进程的地址,可以使用echo $DBUS_SESSION_BUS_ADDRESS查看。
当用户登录进桌面环境的时候,系统启动脚本会调用到dbus-launch来启动一个dbus-daemon进程,同时会把启动的dbus-daemon地址赋予环境变量DBUS_SESSION_BUS_ADDRESS。
一般情况下,不需要考虑DBUS_SESSION_BUS_ADDRESS,但某些时候,单独启动一个dbus-daemon有助于程序的调试。
利用dbus-daemon自启动机制运行的服务进程,都是后台进程,标准输出设备已经被重定向,如果B进程有一些调试用的打印信息输出,则很难直接查看。此时,可以单独启动一个dbus-daemon,让A和B都使用自己启动的dbus-daemon,此时,dbus-daemon能把B的打印信息显示出来。
先在终端下启动一个dbus-daemon,命令如下形式如下:
DBUS_VERBOSE=1 dbus-daemon --session --print-address
如此启动的dbus-daemon会前台执行,并且打印出地址。
然后,在执行A程序的时候,设置环境变量DBUS_SESSION_BUS_ADDRESS为刚才得到的地址值。
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-7MlJMxxGnW,guid=437c0e6060516670cfccacc15afc43c6 ./A
此时运行程序A和B,使用自己启动的dbus-daemon来转发消息,并且会把B的打印信息显示出来。
消息总线守护进程是一个特殊的进程,用于管理系统内的总线,可以将一个进程的消息路由给另外一个进程。如果有很多应用程序连接到消息总线守护进程的总线上,总线能把消息路由到对应的一个或多个进程中去。因此在总线层上,实现了点对点通信的支持,也实现了广播/订阅通信方式。
在最底层,D-Bus只支持点对点的通信,一般使用本地套接字(AF_UNIX)在应用和消息总线守护进程之间通信。D-Bus的点对点是经过bus daemon抽象过的,由bus daemon来完成寻址和发送消息,因此每个应用不必关心要把消息发给哪个进程。D-Bus发送消息通常包含如下步骤:
A、应用程序创建和发送消息给消息总线守护进程。
B、消息总线守护进程对收到的消息进行分发处理。
C、目标程序接收到消息,然后根据消息的种类,做不同的响应:确认、应答、忽略。
总线是D-Bus的进程间通信机制,一个系统中通常存在多条总线,总线由D-Bus总线守护进程管理。
最重要的总线为系统总线(System Bus),Linux内核引导时,系统总线就已被装入内存。只有Linux内核、Linux桌面环境和权限较高的程序才能向系统总线写入消息,以此保障系统安全性,防止有恶意进程假冒Linux发送消息。
会话总线(Session Buses)由普通进程创建,可同时存在多条。会话总线属于某个进程私有,用于进程间传递消息。
4、D-Bus封装层
D-Bus封装层是将libdbus底层API绑定到特定的对象系统或是语言中,將不便使用的libdbus底层API封裝成可以在应用层使用的高級API,如libdbus-glib、libdbus-qt等。 D-Bus在很多不同的编程语言上都有其接口实现。不同语言的接口封装了D-Bus低级API,提供了更符合编程语言的语法结构。 实现D-Bus接口的语言正在逐渐增加。在C语言中,有最底层的API,但其实现及使用上非常复杂。C语言中另一个实用化的实现基于GLib。在Java、Perl、Python等都有D-Bus接口实现。
三、D-Bus术语
1、D-Bus术语简介
总线是消息总线守护进程(message bus daemon)的运行实例,每个总线都有一个地址,应用进程就是通过总线地址和相应的总线连接的。总线上的每一个连接都有一个连接名,连接名也称bus name。每个连接上有至少一个对象,通常有多个对象,对象使用对象路径唯一标识。对象要实现一个或多个接口,每个接口包含一组方法和信号。
2、总线(Bus)
在D-Bus中,总线(bus)是核心的概念:不同的程序可以通过总线进行某些操作,比如方法调用、发送信号和监听特定的信号。总线通常有两种,系统总线(system bus)和用户总线(session bus),系统总线通常只有一条,用户总线在用户登录时创建。 系统总线是一个持久的总线,在系统启动时就创建,供系统内核和后台进程使用,具有较高的安全性。系统总线最常用是发送系统消息,比如:插入一个新的存储设备、有新的网络连接等。 会话总线是在某个用户登录后启动,属于某个用户私有,是某用户的应用程序用来通话的通道。在很多嵌入式系统中,只有一个用户ID登录运行,因此只有一个会话总线。 一条消息总线就是一个消息路由器,是消息总线守护进程(message bus daemon)的一个实例。
3、地址(Address)
使用d-bus的应用程序既可以是server端也可以是client端,server端监听到来的连接,client端连接到server端,一旦连接建立,消息就可以流转。如果使用dbus daemon,所有的应用程序都是client端,dbus daemon监听所有的连接,应用程序初始化连接到dbus daemon。 每一条总线都有一个地址,进程通过总线的地址连接到总线上。一个D-Bus的地址是指server端用于监听,client端将要连接的地方,例如unix:path=/tmp/abcedf标识server端将在路径/tmp/abcedf的UNIX domain socket监听,client端将要连接到这个地址。地址可以是指定的TCP/IP socket或者其他在或者将在D-Bus协议中定义的传输方式。 如果使用bus daemon,libdbus将通过读取环境变量DBUS_SESSION_BUS_ADDRESS自动获取session bus damon的地址,通过检查一个指定的UNIX domain socket路径获取system bus的地址。 如果使用D-bus,但不是daemon,需要定义哪个应用是server端,哪个是client端,并定义一套机制用于认可server端的地址。
4、连接名(Bus 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内核关掉,总线将会发送通知,告诉剩余的应用程序。
5、对象和对象路径(Object and Object Path)
D-Bus的对象和面向对象语言中的对象含义是不同的,D-Bus的对象表示的是D-Bus通道中信息流向的端点。对象由客户进程创建,并在连接进程中保持不变。 所有使用D-BUS的应用程序都包含一些对象, 当经由一个D-BUS连接收到一条消息时,消息是被发往一个对象而不是整个应用程序。应用程序框架中定义了这样的对象,如GObject,QObject等,在D-Bus中称为原生对象(native object)。 对于底层的D-Bus协议,即libdbus API,并不理会原生对象,使用对象路径(object path)的概念。通过对象路径,高层API接口可以绑定到对象,允许远程应用指向对象。对象路径如同文件系统路径,例如一个对象可能叫做“/org/kde/kspread/sheets/3/cells/4/5”。 对象路径在全局(session或者system)是唯一的,用于消息的路由。
6、接口(Interface)
每一个对象支持一个或者多个接口,接口是一组方法和信号的集和,接口定义一个对象实体的类型。D-Bus对接口的命名方式,类似org.freedesktop.Introspectable。开发人员通常使用编程语言名字作为接口名字。
7、方法(Methods)
每一个对象有两类成员:方法和信号。方法是一段函数代码,带有输入和输出;信号是广播给所有兴趣的其他实体,信号可以带有数据payload。 客户向某对象发送一个请求,即对象被请求执行一个明确的、有名称的动作。如果客户请求执行一个目标对象未提供的方法,将会产生一个错误。方法的定义可以支持输入参数。对于每个请求,都有一个包含请求结果以及结果数据(输出参数)的响应返回给请求者。当请求无法完成时,响应中将包含异常信息,其中至少有异常名称以及错误信息。 大多数语言都将这些封装在自身的语言机制中,比如将参数包装进消息包,将异常信息转换成语言自身的异常等等。在这些实现中,向远程对象传递一个字符串参数就好像是在本地执行一个字符串参数的函数一样简单。此时不再需要数据类型转换、数据复制等繁琐工作,语言本身封装了一切底层实现。
8、信号(Signals)
信号依然遵从面向对象概念,信号是从对象发出但没有特定目的地址的单向数据广播。客户进程可以预先注册其感兴趣的信号,如特定名称的信号或从某个对象发出的信号等。当对象发出信号后,所有订阅了该信号的客户进程将收到此信号的复本。接收端可能有多种情况出现,或者有一个客户进程,或者有多个客户进程,或者根本没有客户进程对这个信号感兴趣。对于信号来说没有响应消息,发出信号的对象不会知道是不是有客户进程在接收,有多少客户进程接收,以及从客户进程收到任何反馈。 信号可以有参数。但信号是单向通信,因此不可能像方法一样具有输入输出参数。D-Bus允许客户进程通过参数比对过滤其需要的信号。 信号一般用来广播一些客户可能会感兴趣的事件,比如某个其它的客户进程与总线的连接断开等。这些信号来自总线对象,因此从信号中客户进程可以分辨断线是由于正常退出、被杀掉或者程序崩溃。
9、代理(Proxies)
代理对象用来表示其他的remote object。当触发了proxy对象的method时,将会在D-Bus上发送一个method_call的消息,并等待答复,根据答复返回。 总线上的对象一般通过代理来访问。总线上的对象位于客户进程以外,而客户可以调用本地接口与对象通信,此时,本地接口充当了代理的角色。当触发了代理对象的方法时,将会在D-Bus上发送一个method_call的消息,并等待答复返回,就象使用一个本地对象一样。 一些语言的代理支持“断线重连”。比如所连接的对象在某段时间里暂时断开了与总线的连接,代理会自动重连到相同的连接名并重新找到对象,程序甚至不会知道目标对象有段时间不可用。并不是所有的语言都支持这一特性,在GLib中的两种代理中的一种支持。 比如不用代理时的代码如下:
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);
10、服务
服务是 D-BUS 的最高层次抽象,服务的实现当前还在不断发展变化。应用程序可以通过一个总线来注册一个服务,如果成功,则应用程序就已经获得了服务。其他应用程序可以检查在总线上是否已经存在一个特定的服务,如果没有可以要求总线启动它。 服务是 D-BUS 的最高层次抽象,服务的实现当前还在不断发展变化。应用程序可以通过一个总线来注册一个服务,如果成功,则应用程序就已经获得了服务。其他应用程序可以检查在总线上是否已经存在一个特定的服务,如果没有可以要求总线启动它。 当通过总线进行通信时,应用程序会获取服务名称。服务名称是应用程序如何选择同一总线上的其他应用程序的依据。服务名称由总线守护进程进行代理,用于将消息从一个应用程序路由到另一个应用程序。服务名称的类似概念是IP地址和主机名:计算机通常具有一个IP地址,并且根据其向网络提供的服务,可以具有与之相关联的一个或多个主机名。 另一方面,如果不使用总线,服务名称也不会使用。如果再将它与计算机网络进行比较,这将等同于点到点网络:因为对等方是已知的,所以不需要使用主机名来查找它或它的IP地址。 总线服务名称的格式实际上与主机名非常相似:它是字母和数字的点分隔序列。常见的做法是根据服务定义的组织的域名来命名自己的服务名称。例如,D-Bus服务由freedesktop.org定义,可以使用org.freedesktop.DBus服务名称在总线上找到它。
四、消息和消息总线
1、消息简介
D-Bus通信机制是通过进程间发送消息实现的,最基本的D-Bus协议是一对一的通信协议。与socket通信不同,D-Bus是面向消息的协议。但如果使用高层的绑定接口,不会直接接触到D-Bus的消息。 D-Bus 有四种类型的消息: A、method_call方法调用 B、method_return方法返回 C、error错误 D、signal信号 代理中的远程对象调用涉及到了消息总线以及method_call和method_return两类消息。 消息有消息头(header)和消息体(body)。消息头包含消息体的路由信息,消息体是净荷,通常包含的是参数。消息头通常包含发送进程的连接名(Bus Name)、方法或者信号名等等,其中有一字段是用于描述消息体中的参数的类型的,例如“i”标识32位整数,“ii”表示2个32位整数。
2、调用method的流程
进程A要调用进程B的一个method,进程A发送method_call消息到进程B,进程B回复method_return消息。在发送消息时,发送方会在消息中添加不同的序列号,同样,回复消息中也会含有序列号,以便对应。 调用method的流程如下: A、在发送method_call消息时,如果使用了代理,进程A要调用进程B的某方法,不用构造method_call消息,只需调用代理的本地方法,代理会自动生成method_call消息发送到消息总线上。 B、如果使用底层API,进程A需要构造一个method_call消息。method_call消息包含对应进程B的连接名、方法名、方法所需参数、进程B中的对象路径和进程B中声明此方法的接口。 C、将method_call消息发送到消息总线上。 D、信息总线检查消息头中的目的连接名,当找到一个进程与此连接名对应时发送消息到该进程。当找不到一个进程与此连接名对应时,返回给进程A一个error消息。 E、进程B解析消息,如果是采用底层API方式,则直接调用方法,然后发宋应答消息到消息总线。如果是D-BUs高级API接口,会先检测对象路径、接口、方法名称,然后把消息转换成对应的本地对象(如GObject,QT中的QObject等)的方法,调用本地对象方法后再将应答结果转换成应答消息发给消息总线。 F、消息总线接收到method_return消息,将把method_return消息直接发给发出调用消息的进程。 消息总线不会对总线上的消息进行重排序。如果发送了两条消息到同一个进程,将按照发送顺序接收到。接收进程不需要按照顺序发出应答消息,例如在多线程中处理这些消息,应答消息的发出是没有顺序的。消息都有一个序列号可以与应答消息进行配对。
3、发送signal的流程
发送信号是单向广播的,信号的发送者不知道对信号作响应的有哪些进程,所以信号发送是没有返回值的。信号接收者通过向消息总线注册匹配规则来表示对某信号感兴趣,而匹配规则通常包含信号的发出者和信号名。 信号发送的流程如下: A、当使用dbus底层接口时,信号需要应用进程自己创建和发送到消息总线;使用dbus高层API接口时,可以使用相关对象进行发送。信号消息包含有声明信号的接口名、信号名、所在进程对应的连接名和相关参数。 B、连接到消息总线上的进程如果对某个信号感兴趣,则注册相应的匹配规则。消息总线保持有匹配规则表。 C、消息总线根据匹配规则表,将信号发送到对信号感兴趣的进程。 D、每个进程收到信号后,如果使用dbus高级API接口,可以选择触发代理对象上的信号。如果使用dbus底层接口,需要检查发送者名称和信号名称,然后决定怎么做。
4、DBus工具
D-Bus提供了两个小工具:dbus-send 和dbus-monitor。可以用dbus-send发送消息,用dbus-monitor监视通道上流动的消息。 dbus-send 用于发送一个消息到消息通道上,使用格式如下:
dbus-send [--system | --session] --type=TYPE --print-reply --dest=连接名对象路径接口名.方法名参数类型:参数值参数类型:参数值
dbus-send --session --type=method_call --print-reply --dest=连接名对象路径接口名.方法名 参数类型:参数值 参数类型:参数值
dbus-send支持的参数类型包括:string, int32, uint32, double, byte, boolean。 dbus-monitor 用于打印消息通道上的消息,使用格式如下:
dbus-monitor [--system | --session | --address ADDRESS] [--profile | --monitor] [watch expressions]
dbus-monitor "type='signal', sender='org.gnome.TypingMonitor', interface='org.gnome.TypingMonitor'"
5、消息总线上的方法和信号
消息总线是一个特殊的应用,主要关于消息总线上的方法和信号。
A、Introspection
消息总线上有一个接口org.freedesktop.DBus.Introspectable,接口中声明了一个方法Introspect,不带参数,将返回一个XML string,XML字符串描述接口、方法、信号。
B、消息总线上的方法和信号
可以通过向名称为“org.freedesktop.DBus”的连接上的对象“/”发送消息来调用消息总线提供的方法。消息总线对象支持标准接口"org.freedesktop.DBus.Introspectable",可以调用org.freedesktop.DBus.Introspectable.Introspect方法查看消息总线对象支持的接口。
dbus-send --session --type=method_call --print-reply --dest=org.freedesktop.DBus / org.freedesktop.DBus.Introspectable.Introspect
用户总线对象支持标准接口“org.freedesktop.DBus.Introspectable”和接口 “org.freedesktop.DBus”。接口“org.freedesktop.DBus”有18个方法和3个信号。
<interface name="org.freedesktop.DBus">
<method name="Hello">
<arg direction="out" type="s"/>
</method>
<method name="RequestName">
<arg direction="in" type="s"/>
<arg direction="in" type="u"/>
<arg direction="out" type="u"/>
</method>
<method name="ReleaseName">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="StartServiceByName">
<arg direction="in" type="s"/>
<arg direction="in" type="u"/>
<arg direction="out" type="u"/>
</method>
<method name="UpdateActivationEnvironment">
<arg direction="in" type="a{ss}"/>
</method>
<method name="NameHasOwner">
<arg direction="in" type="s"/>
<arg direction="out" type="b"/>
</method>
<method name="ListNames">
<arg direction="out" type="as"/>
</method>
<method name="ListActivatableNames">
<arg direction="out" type="as"/>
</method>
<method name="AddMatch">
<arg direction="in" type="s"/>
</method>
<method name="RemoveMatch">
<arg direction="in" type="s"/>
</method>
<method name="GetNameOwner">
<arg direction="in" type="s"/>
<arg direction="out" type="s"/>
</method>
<method name="ListQueuedOwners">
<arg direction="in" type="s"/>
<arg direction="out" type="as"/>
</method>
<method name="GetConnectionUnixUser">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="GetConnectionUnixProcessID">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="GetAdtAuditSessionData">
<arg direction="in" type="s"/>
<arg direction="out" type="ay"/>
</method>
<method name="GetConnectionSELinuxSecurityContext">
<arg direction="in" type="s"/>
<arg direction="out" type="ay"/>
</method>
<method name="ReloadConfig">
</method>
<method name="GetId">
<arg direction="out" type="s"/>
</method>
<signal name="NameOwnerChanged">
<arg type="s"/>
<arg type="s"/>
<arg type="s"/>
</signal>
<signal name="NameLost">
<arg type="s"/>
</signal>
<signal name="NameAcquired">
<arg type="s"/>
</signal>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg direction="out" type="s"/>
</method>
</interface>