分类: LINUX
2015-05-26 15:24:45
在LINUX系统下写开源代码的程序员,对DBUS应该是不会再陌生了。它的低时延和低消耗等优点吸引了很多人的目光,因为我们现在开发的平台,很多设计到DBUS这个通讯机制,所以我花了比较多的时间去熟悉它。通过这段时间的学习,把笔记也做一下,今天先介绍什么DBUS。
什么是DBUS?
DBUS是实质上一个适用于桌面应用的进程间的通讯机制,即所谓的IPC机制。适合在同一台机器,不适合于INTERNET的IPC机制。DBUS不是一个为所有可能的应用的通用的IPC机制,不支持其他IPC机制的很多特性。DBUS提供了一个低时延、低消耗的IPC通讯,因为它采用了二进制的数据交换协议,不需要转换成文本化的数据进行交换,DBUS提供了面向多重对象系统的包装,可以在原有的面向对象的应用框架下使用DBUS,不需要学习新的概念和规范等。
DBUS是支持一对一和多对多的对等通讯,在一对一的直接通讯时,两个应用程序连接在一起,这是最简单的工作方式。在多对多的通讯时,这就需要一个叫DBUS后台的角色去分转,一个应用程序发消息给另外一个应用程序,先到达后台,再让后台将信息发送到目的应用程序。在这里DBUS后台就充当着一个路由器的角色。
DBUS包含了系统更新通知,如插入新设备通知、新软件安装通知等,和桌面应用的交互协作能力,可以作为文件系统监控器和配置服务器。
Dbus由对象、消息、连接、Dbus后台几部分组成。
对象是一个独立的处理消息的实体。对象有一个或多个接口,在每个接口有一个或多个的方法,每个方法实现了具体的消息处理。在一对一的通讯中,对象通过一个连接直接和另一个客户端应用程序连接起来。在多对多的通讯中,对象通过一个连接和Dbus后台进程连接起来。对象有一个路径用于指明该对象的存放位置,消息传递时通过该路径找到该对象。
客户端应用是一个桌面应用程序,是请求消息的发起者。客户端应用通过和自身的相连的一个连接将请求消息发送出去,也通过该连接接收回应的消息、错误消息、系统更新消息等。在一对一的通讯中,请求消息直接到达对象。在多对多的通讯中,请求消息先到达Dbus后台,Dbus后台将消息转发到目的对象。
连接是一个双向的消息传递通道。一个连接将对象和Dbus后台或客户端应用连接起来,连接支持非阻塞式的异步消息发送和阻塞式的同步消息发送。消息通过连接到达目的端后,连接会将挂起在该连接上的进程唤醒,由该进程将消息取走。每个连接都有一个唯一的名字和可选的其他多个名字,用于在多对多通讯时指明消息的发送者和接收者。
连接基于操作系统提供的通讯端口实现消息的交换,现在基于的通讯端口有三种,分别是UNIX的socket、TCP/IP、管道(调试时用)。通讯端口拥有一个地址,服务器在这个地址上监听,客户端则连接到这个地址上。
消息是Dbus的IPC机制中的一个信息传递媒介。调用者将调用的方法、方法的参数打包进一个消息,接收者将方法和参数从消息中解包出来,执行这个方法调用。执行完后,将结果打包进返回消息中,返回给调用者。消息有四种类型,分别是方法调用消息、结果返回消息、错误消息、信号消息。这里的信号消息是主动发送的事件,如新设备插入、文件更改的事件等。
Dbus后台是在多对多通讯时用来转发消息,管理连接的一个后台进程。每个Dbus后台都和多个连接关联,其内部维护了连接名和连接实体的映射关系。Dbus后台就象一个路由器,将从发送者连接得到的消息转发到由消息中的接收者连接名指定的接收者连接中。
Dbus后台有多个。有一个用于和系统通讯,监控系统更新事件,其类型为DBUS_BUS_SYSTEM的Dbus后台。每个桌面会话(session)有一个用于多个桌面应用之间相互通讯,其类型为DBUS_BUS_SESSION的Dbus后台。一般至少有两个Dbus后台,一个系统用,一个桌面会话用。系统用的Dbus后台只能处理系统的消息,桌面会话用的Dbus后台只能处理桌面会话应用的消息。
1、客户端。
在客户端使用DBUS比较简单,首先,从DBUS_BUS_SESSION类型的DBUS后台获得一个连接,再从这个连接创建得到一个对象的代理,以后对对象的所有操作都将通过这个代理来完成。
得到服务代理后,可以在应用程序的各个地方通过对象代理的方法使用函数想对象发出一个方法调用的消息。请求对象的服务,可以发送异步的方法(异步服务),也可以发送同步方法(同步服务),方法是同步还是异步有对象定义。
2、服务端。
在服务器进程启动后,调用函数dbus_g_object_type_install_info将对象的安装信息结构告诉DBUS,随后,从DBUS_BUS_SESSION类型的DBUS获得一个连接,再从这个连接得到一个DBUS对象的代理。通过这个DBUS代理调用方法RequestName为这个连接得到一个命名,客户端应用可以使用这个名字将请求消息发送到连接。接着,服务器进程创建一个指定类型的对象(glib对象)。
其中安装信息由XML文件,通过dbus-binding-tool转换成对象的头文件。
3、消息。
消息由消息头和消息体组成。消息头由消息的固有字段信息组成。消息体由一串字符串值组成。消息体的每个字符串值的意义由消息头中的描述指定,消息头的长度必须是8的倍数,相应的,消息体由8的倍数处开始。
============================================================
D-Bus体系
有很多种IPC或者网络通信系统,如:CORBA, DCE, DCOM, DCOP, XML-RPC, SOAP, MBUS, Internet Communications Engine (ICE)等等,可能会有数百种,dbus的目的主要是下面两点:
1.在同一个桌面会话中,进行桌面应用程序之间的通讯
2.桌面程序与内核或者守护进程的通信。
Dbus是一套进程通信体系,它有以下几层:
1.libdbus库,提供给各个应用程序调用,使应用程序具有通信和数据交换的能力,两个应用程序可以直接进行通信,就像是一条socket通道,两个程序之间建立通道之后,就可以通讯了。
2.消息守护进程,在libdbus的基础上创建,可以管理多个应用程序之间的通信。每个应用程序都和消息守护进程建立dbus的链接,然后由消息守护进程进行消息的分派。
3.各种包装库,有libdbus-glib,libdbus-qt等等,目的是将dbus的底层api进行一下封装。
下面有一张图可以很方便说明dbus的体系结构。
dbus中的消息由一个消息头(标识是哪一种消息)和消息数据组成,比socket的流式数据更方便一些。bus daemon 就像是一个路由器,与各个应用程序进行连接,分派这些消息。bus daemon 在一台机器上有多个实例,第一个实例是全局的实例,类似于sendmail和或者apache,这个实例有很严格的安全限制,只接受一些特定的系统消息,用于系统通信。其他bus daemon是一些会话,用于用户登录之后,在当前会话(session)中进行的通讯。系统的bus daemon 和会话的bus daemon 是分开的,彼此不会互相影响,会话bus daemon 不会去调用系统的bus daemon 。
Native Objects and Object Paths
在不同的编程语言中,都定义了一些“对象”,如java中的java.lang.Object,GLIB中的GObject,QT中的QObject等等。D-BUS的底层接口,和libdbus API相关,是没有这些对象的概念的,它提供的是一种叫对象路径(object path),用于让高层接口绑定到各个对象中去,允许远端应用程序指向它们。object path就像是一个文件路径,可以叫做/org/kde/kspread/sheets/3/cells/4/5等。
Methods and Signals
每个对象都有一些成员,两种成员:方法(methods)和信号(signals),在对象中,方法可以被调用。信号会被广播,感兴趣的对象可以处理这个信号,同时信号中也可以带有相关的数据。每一个方法或者信号都可以用一个名字来命名,如”Frobate” 或者 “OnClicked”。
Interfaces
每个对象都有一个或者多个接口,一个接口就是多个方法和信号的集合。dbus使用简单的命名空间字符串来表示接口,如org.freedesktop.Introspectable。可以说dbus接口相当于C++中的纯虚类。
Proxies
代理对象用于模拟在另外的进程中的远端对象,代理对象像是一个正常的普通对象。d-bus的底层接口必须手动创建方法调用的消息,然后发送,同时必须手动接受和处理返回的消息。高层接口可以使用代理来替换这些,当调用代理对象的方法时,代理内部会转换成dbus的方法调用,等待消息返回,对返回结果解包,返回给相应的方法。可以看看下面的例子,使用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);
客户端代码减少很多。
Bus Names
当一个应用程序连接上bus daemon时,daemon会分配一个唯一的名字给它。以冒号(:)开始,这些名字在daemon的生命周期中是不会改变的,可以认为这些名字就是一个IP地址。当这个名字映射到应用程序的连接上时,应用程序可以说拥有这个名字。同时应用可以声明额外的容易理解的名字,比如可以取一个名字com.mycompany.TextEditor,可以认为这些名字就是一个域名。其他应用程序可以往这个名字发送消息,执行各种方法。
名字还有第二个重要的用途,可以用于跟踪应用程序的生命周期。当应用退出(或者崩溃)时,与bus的连接将被OS内核关掉,bus将会发送通知,告诉剩余的应用程序,该程序已经丢失了它的名字。名字还可以检测应用是否已经启动,这往往用于只能启动一个实例的应用。
Addresses
使用d-bus的应用程序既可以是server也可以是client,server监听到来的连接,client连接到server,一旦连接建立,消息就可以流转。如果使用dbus daemon,所有的应用程序都是client,daemon监听所有的连接,应用程序初始化连接到daemon。
dbus地址指明server将要监听的地方,client将要连接的地方,例如,地址:unix:path=/tmp/abcdef表明server将在/tmp/abcdef路径下监听unix域的socket,client也将连接到这个socket。一个地址也可以指明是TCP/IP的socket,或者是其他的。
当使用bus daemon时,libdbus会从环境变量中(DBUS_SESSION_BUS_ADDRESS)自动认识“会话daemon”的地址。如果是系统daemon,它会检查指定的socket路径获得地址,也可以使用环境变量(DBUS_SESSION_BUS_ADDRESS)进行设定。
当dbus中不使用daemon时,需要定义哪一个应用是server,哪一个应用是client,同时要指明server的地址,这不是很通常的做法。
Big Conceptual Picture
要在指定的对象中调用指定的方法,需要知道的参数如下:
Address -> [Bus Name] -> Path -> Interface -> Method
bus name是可选的,除非是希望把消息送到特定的应用中才需要。interface也是可选的,有一些历史原因,DCOP不需要指定接口,因为DCOP在同一个对象中禁止同名的方法。
Messages - Behind the Scenes
如果使用dbus的高层接口,就可以不用直接操作这些消息。DBUS有四种类型的消息:
1.方法调用(method call) 在对象上执行一个方法
2.方法返回(method return)返回方法执行的结果
3.错误(error)调用方法产生的异常
4.信号(signal)通知指定的信号发生了,可以想象成“事件”。
要执行 D-BUS 对象的方法,需要向对象发送一个方法调用消息。它将完成一些处理并返回一个方法返回消息或者错误消息。信号的不同之处在于它们不返回任何内容:既没有“信号返回”消息,也没有任何类型的错误消息。
每个消息都有一个消息头,包含多个字段,有一个消息体,包含多个参数。可以认为消息头是消息的路由信息,消息体作为一个载体。消息头里面的字段包含发送的bus name,目标bus name,方法或者信号名字等,同时消息头里面定义的字段类型规定了消息体里面的数据格式。例如:字符“i”代表了”32-bit integer”,“ii”就代表了消息体里面有两个”32-bit integer”。
Calling a Method - Behind the Scenes
在dbus中调用一个方法包含了两条消息,进程A向进程B发送方法调用消息,进程B向进程A发送应答消息。所有的消息都由daemon进行分派,每个调用的消息都有一个不同的序列号,返回消息包含这个序列号,以方便调用者匹配调用消息与应答消息。调用消息包含一些参数,应答消息可能包含错误标识,或者包含方法的返回数据。
方法调用的一般流程:
1.使用不同语言绑定的dbus高层接口,都提供了一些代理对象,调用其他进程里面的远端对象就像是在本地进程中的调用一样。应用调用代理上的方法,代理将构造一个方法调用消息给远端的进程。
2.在DBUS的底层接口中,应用需要自己构造方法调用消息(method call message),而不能使用代理。
3.方法调用消息里面的内容有:目的进程的bus name,方法的名字,方法的参数,目的进程的对象路径,以及可选的接口名称。
4.方法调用消息是发送到bus daemon中的。
5.bus daemon查找目标的bus name,如果找到,就把这个方法发送到该进程中,否则,daemon会产生错误消息,作为应答消息给发送进程。
6.目标进程解开消息,在dbus底层接口中,会立即调用方法,然后发送方法的应答消息给daemon。在dbus高层接口中,会先检测对象路径,接口,方法名称,然后把它转换成对应的对象(如GObject,QT中的QObject等)的方法,然后再将应答结果转换成应答消息发给daemon。
7.bus daemon接受到应答消息,将把应答消息直接发给发出调用消息的进程。
8.应答消息中可以包容很多返回值,也可以标识一个错误发生,当使用绑定时,应答消息将转换为代理对象的返回值,或者进入异常。
bus daemon不对消息重新排序,如果发送了两条消息到同一个进程,他们将按照发送顺序接受到。接受进程并需要按照顺序发出应答消息,例如在多线程中处理这些消息,应答消息的发出是没有顺序的。消息都有一个序列号可以与应答消息进行配对。
Emitting a Signal - Behind the Scenes
在dbus中一个信号包含一条信号消息,一个进程发给多个进程。也就是说,信号是单向的广播。信号可以包含一些参数,但是作为广播,它是没有返回值的。
信号触发者是不了解信号接受者的,接受者向daemon注册感兴趣的信号,注册规则是”match rules”,记录触发者名字和信号名字。daemon只向注册了这个信号的进程发送信号。
信号的一般流程如下:
1.当使用dbus底层接口时,信号需要应用自己创建和发送到daemon,使用dbus高层接口时,可以使用相关对象进行发送,如Glib里面提供的信号触发机制。
2.信号包含的内容有:信号的接口名称,信号名称,发送进程的bus name,以及其他参数。
3.任何进程都可以依据”match rules”注册相关的信号,daemon有一张注册的列表。
4.daemon检测信号,决定哪些进程对这个信号感兴趣,然后把信号发送给这些进程。
5.每个进程收到信号后,如果是使用了dbus高层接口,可以选择触发代理对象上的信号。如果是dbus底层接口,需要检查发送者名称和信号名称,然后决定怎么做。
Glib绑定接口在"dbus/dbus-glib.h"头文件中定义。
dbus和glib的数据类型映射如下:
D-Bus basic type GType Free function Notes BYTE G_TYPE_UCHAR BOOLEAN G_TYPE_BOOLEAN INT16 G_TYPE_INT Will be changed to a G_TYPE_INT16 once
GLib has itUINT16 G_TYPE_UINT Will be changed to a G_TYPE_UINT16 once
GLib has itINT32 G_TYPE_INT Will be changed to a G_TYPE_INT32 once
GLib has itUINT32 G_TYPE_UINT Will be changed to a G_TYPE_UINT32 once
GLib has itINT64 G_TYPE_GINT64 UINT64 G_TYPE_GUINT64 DOUBLE G_TYPE_DOUBLE STRING G_TYPE_STRING g_free OBJECT_PATH DBUS_TYPE_G_PROXY g_object_unref The returned proxy does not have an interface set; use
dbus_g_proxy_set_interface to invoke methods
Container type mappings
dbus数据也有包容器类型,像DBUS_TYPE_ARRAY 和 DBUS_TYPE_STRUCT,dbus的数据类型可以是嵌套的,如有一个数组,内容是字符串的数组集合。
但是,并不是所有的类型都有普通的使用,DBUS_TYPE_STRUCT应该可以包容非基本类型的数据类型。glib绑定尝试使用比较明显的方式进行声明。
D-Bus type signature Description GType C typedef Free function Notes as Array of strings G_TYPE_STRV char ** g_strfreev v Generic value container G_TYPE_VALUE GValue * g_value_unset The calling conventions for values expect that method callers have
allocated return values; see below.
同时定义了新的数组类型集合。
D-Bus type signature Description GType C typedef Free function Notes ay Array of bytes DBUS_TYPE_G_BYTE_ARRAY GArray * g_array_free au Array of uint DBUS_TYPE_G_UINT_ARRAY GArray * g_array_free ai Array of int DBUS_TYPE_G_INT_ARRAY GArray * g_array_free ax Array of int64 DBUS_TYPE_G_INT64_ARRAY GArray * g_array_free at Array of uint64 DBUS_TYPE_G_UINT64_ARRAY GArray * g_array_free ad Array of double DBUS_TYPE_G_DOUBLE_ARRAY GArray * g_array_free ab Array of boolean DBUS_TYPE_G_BOOLEAN_ARRAY GArray * g_array_free
定义了字典类型
D-Bus type signature Description GType C typedef Free function Notes a{ss} Dictionary mapping strings to strings DBUS_TYPE_G_STRING_STRING_HASHTABLE GHashTable * g_hash_table_destroy client端编写
我们的程序在使用dbus的时候,首先需要连接上dbus,使用dbus_g_bus_get获得dbus连接。然后可以创建代理对象。
需要调用方法的时候,可以有两种方式:1.同步调用,使用dbus_g_proxy_call发送方法请求到远端对象,dbus会阻塞等待远端对象的回应,输出参数里将会带有相应的回应数据,以G_TYPE_INVALID作为终止符。2.异步调用,使用dbus_g_proxy_begin_call,它将返回一个DBusGPendingCall对象,可以使用dbus_g_pending_call_set_notify连接到自己的处理函授中。
可以使用dbus_g_proxy_add_signal 和 dbus_g_proxy_connect_signal来连接信号,dbus_g_proxy_add_signal用来声明信号处理函数,属于必须被调用的接口,dbus_g_proxy_connect_signal可以调用多次。
Generated Bindings
使用内置的xml文件,可以很方便地自动创建出易于使用的dbus代理对象。如下的一个xml文件描述了了一个方法:
“in”标识输入参数,“out”标识输出参数。
使用dbus-binding-tool工具来生成头文件,如dbus-binding-tool –mode=glib-client my-object.xml > my-object-bindings.h,会产生如下的内联函数原型:
gboolean
com_example_MyObject_many_args (DBusGProxy *proxy, const guint IN_x,
const char * IN_str, const gdouble IN_trouble,
gdouble* OUT_d_ret, char ** OUT_str_ret,
GError **error);
DBusGProxyCall*
com_example_MyObject_many_args_async (DBusGProxy *proxy, const guint IN_x,
const char * IN_str, const gdouble IN_trouble,
com_example_MyObject_many_args_reply callback,
gpointer userdata);
typedef void
(*com_example_MyObject_many_args_reply)
(DBusGProxy *proxy, gdouble OUT_d_ret, char * OUT_str_ret,
GError *error, gpointer userdata);所有函数的第一个参数都是DBusGProxy对象,一般是使用dbus_g_proxy_new_*函数创建出来的。客户端发送方法请求可以增加标记,目前只有org.freedesktop.DBus.GLib.NoReply标记,dbus可以不要回应消息,没有“out”参数,这样运算速度会快一点。
server端的编写
在GLib中,通过dbus表现出GObject,必须写XML文件描述这个对象的方法等属性。像上一篇文章中提到的例子:
一旦写完XML,运行dbus-binding-tool工具,如 dbus-binding-tool –mode=glib-server my-object.xml > my-object-glue.h.
然后在本地代码中include产生的头文件,调用dbus_g_object_class_install_info进行类的初始化,传递对象和对象信息进去,如 dbus_g_object_type_install_info (COM_FOO_TYPE_MY_OBJECT, &com_foo_my_object_info);每个对象类都需要这样做。
为了执行方法,需要定义一个C函数,如my_object_many_args,需要遵守的规则如下:
1.函数返回gboolean,true表示成功,false标识失败。
2.第一个参数必须是对象实例的指针。
3.跟在实例指针后面的参数是方法的输入参数。
4.输入参数后面是输出参数。
5.最后一个参数必须是GError **,如果函数返回失败,必须使用g_set_error填充该错误参数。
如下的xml文件
对应的函数定义为:
gboolean
my_object_increment (MyObject *obj, gint32 x, gint32 *ret, GError **error);最后可以使用dbus_g_connection_register_g_object输出一个对象,如
dbus_g_connection_register_g_object (connection,”/com/foo/MyObject”, obj);server端的声明(Annotations):
org.freedesktop.DBus.GLib.CSymbol
org.freedesktop.DBus.GLib.Async
org.freedesktop.DBus.GLib.Const
org.freedesktop.DBus.GLib.ReturnVal首先需要启动守护进程
dbus-daemon –system –print-pid –print-address
结果提示 Failed to start message bus: Could not get UID and GID for username “messagebus”
dbus需要有一个messagebus用户,创建该用户即可,useradd messagebus,问题解决。执行一个dbus测试程序,提示:D-Bus library appears to be incorrectly set up; failed to read machine uuid: Failed to open “/usr/var/lib/dbus/machine-id”: No such file or directory
没有machine-id文件,查了一下,需要给它定义一个id,使用dbus-uuidgen >/usr/var/lib/dbus/machine-id
产生这个文件,该问题解决。再次执行测试程序,又有问题:Couldn’t connect to session bus: Failed to execute dbus-launch to autolaunch D-Bus session,看了帮助
AUTOMATIC LAUNCHING一节,需要设置DBUS_SESSION_BUS_ADDRESS环境变量的值,先执行dbus-launch,获得了DBUS_SESSION_BUS_ADDRESS值,再export一下,最后执行测试程序,OK了在dbus帮助中有一篇关于,可以在脚本中启动dbus-launch,同时自动设置DBUS_SESSION_BUS_ADDRESS环境变量,脚本文件rundbus如下:
if test -z "$DBUS_SESSION_BUS_ADDRESS" ; then
## if not found, launch a new one
eval `dbus-launch --sh-syntax --exit-with-session`
echo "D-Bus per-session daemon address is: $DBUS_SESSION_BUS_ADDRESS"
fi执行. rundbus即可。