综述
D-Bus是一种IPC(进程间通信)机制的实现,相比其它的IPC,D-Bus更加的轻量和高速,这些优势得益于它的如下特点:基于连接(Connection-based),以二进制格式通信。
D-Bus有各种不同语言的实现(也并不一定是语言,可以是其它,比如某个特定的框架),最基础的是C,有官方提供的参考代码,也有Java、Python等,每种语言的实现被称为D-Bus Binding。
D-Bus主要包含两个组件,进程间通信需要调用的D-Bus库(可以由不同的语言来实现)和D-Bus守护进程(Daemon),进程间的通信主要通过D-Bus守护进程来完成,因此对于一个程序来说,它要做的只是通过D-Bus库与守护进程交互,而不需要关注底层的细节,这对于多语言的实现也很有帮助。D-Bus守护进程就像一条总线(Bus,后面提到的D-Bus总线就是D-Bus守护进程),各类需要通信的进程都连接其上,并交互信息(Message)。
一个系统中可以有多条D-Bus总线,即多个D-Bus守护进程。D-Bus总线有两种类型,一种是系统总线(System Bus),另一种是会话总线(Session Bus)。前者用于系统级别的通信,比如当有新硬件接入时的通知;后者由单个用户使用。下面是Ubuntu18.04(下同)的一个例子,红框部分指定了类型(这里有两个D-Bus总线没有明确指定是系统还是会话总线,但是通过配置文件还是有指定的):
每条总线有一个地址(Address),名称格式类似“/tmp/.hiddensocket”(从名称上也可以看出来,D-Bus的底层实现其实是Socket,这个到以后如果讲D-Bus实现时会说明),通过该地址其它进程就可以连接到该总线,这需要通过D-Bus库调用接口,并使用该总线地址来打开(Open)与D-Bus守护进程的连接(Connection)。
D-Bus守护进程的启动依赖于命令dbus-launch,它会调用另外的一个命令dbus-daemon(这个说法来自https://www.freedesktop.org/wiki/IntroductionToDBus/,但似乎可以直接调用了dbus-daemon),后者带一个参数--config-file,可以指定配置文件,通常的配置文件位于/etc/dbus-1/目录下,也分为用于系统总线和会话总线的两种配置。如下所示:
配置文件基于XML,具体以后再介绍。
D-Bus实现中的一些概念(后面几节中将陆续介绍):
A... | is identified by a(n)... | which looks like... | and is chosen by... |
Bus | address | unix:path=/var/run/dbus/system_bus_socket | system configuration |
Connection | bus name | :34-907 (unique) com.mycompany.TextEditor (well-known) | D-Bus (unique) or the owning program (well-known) |
Object | path | /com/mycompany/TextFileManager | the owning program |
Interface | interface name | org.freedesktop.Hal.Manager | the owning program |
Member | member name | ListNames | the owning program |
连接
进程与D-Bus总线的连接可以通过一个或多个名字找到,这些名字被称为连接的总线名(Connection's Bus Name,注意它是连接的名字,而不是总线的名字,前面已经讲过,总线通过地址找到),该名字的一般格式是com.acme.Foo,名字由点(.)分隔的字符串组成,字符串可以包含大小写字符、数字、连接线(-)、下划线(_)。这里我们可以说连接拥有一个总线名com.acme.Foo。
当一个连接建立,D-Bus总线会给该连接分配一个不可变且唯一的总线名称,它被称为唯一连接名(Unique Connection Name),只要该D-Bus总线还在,该名字就会被保留着,即使连接已经消失而新连接已经建立。这类总线名以冒号(:)开头(只有唯一连接名会以冒号开头),格式如下:“:34-987”,这里需要关注的只是冒号,而不是后面的数字,它们本身没有意义,只是为了保证唯一性。
连接还可以有其它的名称,比如说用可读的名称来提供服务(Service),这种名字需要包含两个及以上的点,比如“com.acm.PortableHole”。同一时间,D-Bus总线上只有一个连接可以拥有指定名称。
关于不同的名称,这里做补充说明。连接的总线名可以认为是进程用的名称,是可读的名称,唯一连接名是D-Bus总线用来标记连接的名称,重点在于唯一性,这两个名称其实都表示的是同一个连接。至于最后一个跟服务有关的名称,目前还不确定具体的作用。
对象模型
D-Bus实现中包含了一些面向对象的概念。 D-Bus总线上通信的是两个进程,但是我们不直接讨论进程,而是将连接的两端称为对象(Object),对象存在与连接中,只有在连接的上下文中对象才有意义。一个进程可以包含若干个对象,对象由进程代码创建并存在于进程的上下文。进程通过对象进行交互,交互的内容有以下的部分:
-
从一个进程(该进程称为客户进程(Client))发送给另一个进程,并被其对象接收的请求(Request);
-
另一个进程通过对象返回的对请求的回应;
-
某进程中对象发出的请求,属于广播,而不是针对特定进程,任何对该请求有兴趣的进程都可以响应;
因此D-Bus总线上存在着一对一和一对多的请求信息(Message)。D-Bus总线本身必须要存在至少一个对象。客户进程可以通过向该对象发送请求来获取D-Bus总线的状态。
D-Bus对象可以通过称为代理(Proxy)的实体来访问,之所以称为代理,是因为它是对象在客户进程中的代表,进程通过这个代表来访问真正的D-Bus对象。关于代理和对象本体的处理,不同的语言实现会有不同,这里暂时不管。
对象有一个名称,也称为路径(Path),它的格式类似“/org/kde/kspread/sheets/3/cell”,在一个连接的上下文中,路径必须是唯一的。由于对象存在与连接当中,因此可以通过连接的总线名加对象路径访问到此对象。在代码中,一旦对象被找到,通常会保存为变量供后续使用,避免每次使用都要重新定位。
当客户进程向一个对象发送请求时,其目的是调用对象的方法(Method),即对象执行某个特定的动作。如果请求的对象不存在,则通常会报错。调用的方法可以存在入参(Input Parameter),它也包含在请求中。对于每一个请求,对象都会回复,可以是一个结果(Output Parameter),也可以是异常,异常的格式必须包含异常的名称和错误信息。关于参数的组织形式,由实现的语言决定,对于这些数据的所有处理,被称为Marshalling。
D-Bus实现中,对方法的调用跟函数调用最大的不同在于,函数调用之后通常是等待函数返回然后再执行后续的操作,而向D-Bus对象发送请求之后不需要等待,而是继续处理其它的事务。这种处理方式一般被称为异步(Asynchronous)调用。
客户进程不止会向一个对象发送请求,也可以广播请求,这被称为发信号(Signal)。客户进程可以通过特定的名称向D-Bus总线注册感兴趣的信号,当有对象发送信号时,所有感兴趣的进程都会收到该信号的一份拷贝。信号可以被接收、也可以不被接收,可以被多个进程接收,也可以只被一个进程接收。进程处理信号之后并不会返回结果,所以发送信号的对象并不知道是否有人接收和处理。信号中也可以带有参数。信号通常是一些进程可能会关注的事件,比如某个进程的连接断开了,这个由D-Bus总线本身的对象发出。
所有的对象都支持方法,并能够发送信号,这些方法和信号被称为对象的成员(Member)。所有的这些成员通过接口(Interface)指定(Specified)。在面向对象的编程语言中,比如Java,接口是定义的集合,而成员是对接口的实现,这些实现与接口保持一致参数。
当客户进程调用方法或者监听信号时,必须指定对象和成员,还可以指定成员所对应的接口,因为在一个对象中,可能包含两个接口,而这两个接口都实现了同一个名称的成员。这种情况不常见,因此通常接口可以被忽略。
激活服务
前面提到的对象都是进程(非D-Bus守护进程)主动连接D-Bus总线并创建对象,还有一种方式来让D-Bus守护进程来唤醒一般进程并提供服务。当有进程想要使用某个特定的代理,但是它还为处于非活跃状态(即没有连接)时,D-Bus守护进程会尝试激活该进程,这依赖于特定的文件.service,该文件指定了需要启动的进程以及它提供的总线名,下面是一个例子:
# (Lines starting with hash marks are comments)
# Fixed section header (do not change):
[D-BUS Service]
Names=com.bigmoneybank.Deposits;com.bigmoneybank.Withdrawals
Exec=/usr/local/bin/bankcounter
这里的/usr/local/bin/bankcounter是需要激活的客户进程,它可以提供两个连接的总线名com.bigmoneybank.Deposits和com.bigmoneybank.Withdrawals。.service文件所在位置写在D-Bus配置文件的<servicedir>模块下,通常是/usr/share/dbus-1/services/:
代码实现
dbus是D-Bus的官方参考实现,可以在如下网页下载到:
https://gitlab.freedesktop.org/dbus/dbus/-/tree/master
它有不同的对应版本,目前比较常用的是dbus-1.10和dbus-1.12。至于其它Binding,可以在https://www.freedesktop.org/wiki/Software/DBusBindings/找到,下图是1.10的版本:
其中最重要的是:bus目录中的代码用来生成dbus-daemon,即D-Bus守护进程;dbus目录中的代码相当于D-Bus库,提供了API的实现;tools包含了一些常用的命令,比如dbus-launch、dbus-monitor等。
在dbus目录源代码中,函数分为两类,一类是以“_”开头,或者不是以“DBus”,“dbus_”,“DBUS_”开头的函数,它们都是内部函数;另一类就是外部可调用的函数。dbus提供给外部调用的接口,可以参考https://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html和https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.html。DBusConnection表示的是D-Bus连接,对应的代码在dbus\dbus-connection.h,而DBusMessage就是DBusConnection之间传递的信息,对应的代码在dbus\dbus-message.h。
参考资料
DBUS的基础介绍IntroductionToDBus。
更具体的内容可以参考D-Bus Specification。