D-Bus详解

发布时间 2023-07-10 17:57:10作者: 我家有只江小白
D-Bus详解

D-Bus详解

什么是D-Bus

D-Bus(Desktop-Bus)是一个IPC(Inter-Process Communication)通信机制,已经被各主流Linux发行版采用。其技术规范(specification)最早发布于2006年。相较于传统意义上的IPC机制(例如PIPE/FIFO/Socket/共享内存/SysvIpc),D-Bus提供了更高层次的抽象:

  1. 方法调用(Method Call):用来实现跨进程的方法(函数)调用,配合代码生成工具,可以做到让进程间的函数调用和普通的函数调用几无区别
  2. 信号(Signal):发布订阅(Pub-Sub)模式的通信机制,发送进程注册并发送(广播)信号,接收进程订阅自己感兴趣的信号。
  3. 属性(Property):可类比C++类中成员变量的Getter-Setter,如果进程A提供了一个属性,那么其他进程可以通过D-Bus来读取、写入该属性。

D-Bus制定技术规范的同时,也提供了一个参考实现,主要包括2部分:

  • libdbus,进程可以通过libdbus提供的函数来接入到D-Bus,例如其会提供注册方法、调用方法、发布信号等等函数。根据官方手册友情提示:
If you use this low-level API directly, you're signing up for some pain.

所以后来又有人折腾出其他的binding,例如sd-bus、gdbus、Qt D-Bus

  • dbus-daemon,D-Bus的中心节点,实际上进程A和进程B通过D-Bus通信时,并不是通过Socket直接连接,而是进程A <-> dbus-daemon <->。dbus-daemon提供服务注册、消息路由等功能。

基本概念

Bus

一个系统上可以存在任意多总线,以Ubuntu为例:

  • System bus 系统总线 开机创建,有且只能有一条,Unix domain socket路径为/run/dbus/system_bus_socket
  • Session bus 用户总线 每个用户登录后会创建专有的D-Bus总线,Unix domain socket路径为/run/user/$UID/bus

不同Bus之间是逻辑隔离的,相互直接无法通信。

Bus Name

Bus name是服务注册到总线的名称,Bus name又分为两类:

Unique name 以:开始,Unique name是dbus-daemon分配的,类似于:1.30,技术规范明确要求其永不重复

Well Known name 为了方便人脑记忆引入的名字。为了命名不冲突,技术规范推荐使用逆转域名方式命名,例如org.freedesktop.DBus

Object Path

一个进程可以包含多个对象,D-Bus用Object path来标识对象,一般以/org/freedesktop/DBus这种形式命名。

Interface

Interface包含方法(Method)、属性(Property)和信号(Signal),以org.freedesktop.DBus这种形式命名。

Method

方法,程序可以通过D-Bus提供的机制,调用另外一个进程提供的Method,即RPC的概念

Signal

信号,D-Bus中Pub-Sub通信模型的具体实现。

Property

属性,类似于C++类中成员变量的Getter-Setter,可以定义为只读、只写、读写类型。

Signature

D-Bus技术规范规定使用String类型来编码方法、信号的签名(参数)。例如i代表int32_t类型,a代表数组等等。

概念小结

接触D-Bus时,所有概念中,最难理解的就是路由寻址相关的。其中主要有几个原因:

  1. 寻址概念多且边界不是很清晰
  2. 容易混淆,例如bus name和interface,本身是两个不相干的概念,但是实践中经常用同样的字符串

通过和Web来做一个类比,可以让这些概念更加具象一点,下面已网址https://github.com/freedesktop/dbus/commits/master?after=13e7b14e195e60b6a068166eb8872fa56c6328de+34&branch=master&qualified_name=refs%2Fheads%2Fmaster举例

D-BusWeb类比内容
Bus 因特网、专网 因特网
Bus name (Unique) IP地址 20.205.243.166
Bus name (Well known) 域名
Object path URL路径 /freedesktop/dbus/commits/
Interface URL master
Method URL master
Signature 参数 master?after=13e7b...34&branch=master

综上,路由寻址层级大体如下:

Bus -> Bus Name -> Object path -> Interface -> Method

busctl使用

busctl是systemd提供的一个D-Bus总线调试工具。通过busctl命令的输出,可对D-Bus中各种抽象的概念有个感性的认识。

busctl list

busctl list会打印出总线中已注册的服务(注:manpage这里写的是peer,暂且称为服务),命令执行结果截取一段如下:

$ busctl list --no-pager --system
NAME                                    PID PROCESS         USER             CONNECTION    UNIT                        SESSION DESCRIPTION
:1.10                                  1067 udisksd         root             :1.10         udisks2.service             -       -
:1.14                                  1122 ModemManager    root             :1.14         ModemManager.service        -       -
:1.16                                  1155 gdm3            root             :1.16         gdm.service                 -       -
:1.22                                  1064 systemd-logind  root             :1.22         systemd-logind.service      -       -
:1.25                                  1198 unattended-upgr root             :1.25         unattended-upgrades.service -       -
:1.2633                             1382069 networkd-dispat root             :1.2633       networkd-dispatcher.service -       -
org.bluez                           1283814 bluetoothd      root             :1.7917       bluetooth.service           -       -
org.debian.apt                            - -               -                (activatable) -                           -       -
org.freedesktop.Accounts               1023 accounts-daemon root             :1.7          accounts-daemon.service     -       -
org.freedesktop.Avahi                  1027 avahi-daemon    avahi            :1.3          avahi-daemon.service        -       -
org.freedesktop.DBus                      1 systemd         root             -             init.scope                  -       -
org.freedesktop.NetworkManager         1034 NetworkManager  root             :1.11         NetworkManager.service      -       -
org.freedesktop.UDisks2                1067 udisksd         root             :1.10         udisks2.service             -       -
org.freedesktop.thermald                  - -               -                (activatable) -                           -       -

表格字段说明:

NAME Bus name

PID 服务对应的进程ID

PROCESS 服务对应的进程名

USER 对应启动服务的用户

CONNECTION 服务与dbus-daemon的连接信息,及Unique name。服务和连接是多对1的关系,一个CONNECTION可以注册多个Bus name,但是Bus name只能对应一个CONNECTION

$ busctl --user |grep 1.43
:1.43                                           5089 gnome-shell     yk   :1.43         user@1000.service -       -
org.gnome.Panel                                 5089 gnome-shell     yk   :1.43         user@1000.service -       -
org.gnome.ScreenSaver                           5089 gnome-shell     yk   :1.43         user@1000.service -       -
org.gnome.Shell                                 5089 gnome-shell     yk   :1.43         user@1000.service -       -
...省略N行...

值得注意的是,org.freedesktop.thermald服务显示为activatable,意思是现在服务可以被激活,这个可以被激活的信息是怎么来的呢,这里就又牵涉到了systemd。在thermald.service中,有这么一行配置BusName=org.freedesktop.thermald,也就是通过启动thermald.service,可以就会注册名为org.freedesktop.thermaldD-Bus服务。

$ cat /usr/lib/systemd/system/thermald.service
[Unit]
Description=Thermal Daemon Service
ConditionVirtualization=no

[Service]
Type=dbus
SuccessExitStatus=1
BusName=org.freedesktop.thermald
ExecStart=/usr/sbin/thermald --systemd --dbus-enable --adaptive

UNIT 对应服务启动的systemd unit

SESSION 对应systemd的中的login session

busctl introspect

introspect(内省反射)可用来查看服务里的接口定义,对应的参数为 SERVICE OBJECT [INTERFACE],如下指令可以查看 org.freedesktop.login1服务下,对象名为/org/freedesktop/login1/session/auto

$ busctl introspect org.freedesktop.login1 /org/freedesktop/login1/session/auto
NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGS
org.freedesktop.login1.Session      interface -         -                                        -
.Lock                               method    -         -                                        -
.Active                             property  b         true                                     emits-change
.Remote                             property  b         false                                    const
.RemoteHost                         property  s         ""                                       const
.RemoteUser                         property  s         ""                                       const
.Scope                              property  s         "session-2.scope"                        const
.Seat                               property  (so)      "seat0" "/org/freedesktop/login1/seat/s… const
.Lock                               signal    -         -                                        -

字段说明

TYPE 类型,主要分为:

NAME 名称

SIGNATURE 参见Signature

RESULT/VALUE 不同Type定义稍有不同:

  • method时,对应的是函数的输出结果签名
  • Property时,对应的是属性当前的值

FLAGS 只针对property有效:

  • emits-change 改变后会发送org.freedesktop.DBus.Properties.PropertiesChanged信号
  • const 常量属性,在对象生命周期内不会改变

busctl monitor

monitor子命令用来监听总线上所有的消息,例如我们如果想看busctl list命令背后发生了什么,可以通过monitor来观测,下面截取了关键部分输出:

$ busctl --user monitor
Monitoring bus message stream.
‣ Type=method_call  Endian=l  Flags=0  Version=1  Priority=0 Cookie=2
  Sender=:1.21712  Destination=org.freedesktop.DBus  Path=/org/freedesktop/DBus  Interface=org.freedesktop.DBus  Member=ListNames
  UniqueName=:1.21712
  MESSAGE "" {
  };

‣ Type=method_return  Endian=l  Flags=1  Version=1  Priority=0 Cookie=3  ReplyCookie=2
  Sender=org.freedesktop.DBus  Destination=:1.21712
  MESSAGE "as" {
          ARRAY "s" {
                  STRING "org.freedesktop.DBus";
                  STRING ":1.10105";
                  STRING "org.freedesktop.Notifications";
                  此处省略N个结果
          };
  };

协议及流程

Message

Message header

Message包括头和数据,是D-Bus总线上通信的标准报文格式。Message的定义如下(摘自技术规范):

ValueDescription
1st BYTE 大小端标志,l - 小端 B - 大端.
2nd BYTE 消息类型,参见下表
3rd BYTE 消息标志位(Flag),定义参见下表
4th BYTE 协议主版本号
1st UINT32 数据长度,不包含头
2nd UINT32 消息序列号,用来匹配会请求和回复,从1开始
ARRAY of STRUCT of (BYTE,VARIANT) 头字段(header fields),一个field code, field value数组,根据消息类型不同二不同

消息类型定义

Conventional nameDecimal valueDescription
INVALID 0 无效
METHOD_CALL 1 Method调用
METHOD_RETURN 2 Method调用返回
ERROR 3 错误回复
SIGNAL 4 信号发送

消息标志位定义

Conventional nameHex valueDescription
NO_REPLY_EXPECTED 0x1 Method调用时,此标志位标志忽略回复
NO_AUTO_START 0x2 不自动启动目标服务
ALLOW_INTERACTIVE_AUTHORIZATION 0x4 允许交互式授权

消息头字段

Conventional NameField CodeTypeRequired InDescription
INVALID 0 N/A not allowed 非法
PATH 1 OBJECT_PATH METHOD_CALL, SIGNAL 类型为Method调用时,代表被调用者的Object Path 。类型为信号时,代表发送者的Object Path
INTERFACE 2 STRING SIGNAL 类型为Method调用时,代表被调用者的Interface。类型为信号时,代表发送者的Interface
MEMBER 3 STRING METHOD_CALL, SIGNAL Method或Signal的名字
ERROR_NAME 4 STRING ERROR error名
REPLY_SERIAL 5 UINT32 ERROR, METHOD_RETURN 回复序列号,通过匹配Message header中的消息序列号字段,该字段可以判断METHOD_RETURN是对应那个METHOD_CALL。
DESTINATION 6 STRING optional 消息目的地的Bus name
SENDER 7 STRING optional 消息发送者的Bus name
SIGNATURE 8 SIGNATURE optional 消息体内容签名
UNIX_FDS 9 UINT32 optional 消息相关的文件句柄

org.freedesktop.DBus

org.freedesktop.DBus是一个特殊的Bus name,它是由dbus-daemon实现的,包含了一些关键的方法,例如:

  1. Hello 用来获取Unique name
  2. RequestName 用来注册Well known name
  3. AddMatch 用来添加路由规则
  4. ListName 用来获取所有Bus name

获取Unique name

sd-bus实例中的程序注册为例:

  1. 调用sd_bus_open_user创建连接到Session bus的socket
  2. 鉴权以及句柄传递特性协商

C : AUTH EXTERNAL S : DATA S : OK 39cd3840b528b2ed120f4f9f625be2bf C : NEGOTIATE_UNIX_FD S : AGREE_UNIX_FD C : BEGIN

  1. 调用org.freedesktop.DBus.Hello接口,获取一个Unique name
   ‣ Type=method_call  Endian=l  Flags=0  Version=1  Priority=0 Cookie=1
     Sender=:1.23493  Destination=org.freedesktop.DBus  Path=/org/freedesktop/DBus  Interface=org.freedesktop.DBus  Member=Hello
     UniqueName=:1.23493
     MESSAGE "" {
     };
     
     ‣ Type=method_return  Endian=l  Flags=1  Version=1  Priority=0 Cookie=1  ReplyCookie=1
     Sender=org.freedesktop.DBus  Destination=:1.23493
     MESSAGE "s" {
             STRING ":1.23493";
     };

注册Well known name

  1. 执行获取Unique name中的步骤
  2. 调用RequestName方法获取Bus name
   ‣ Type=method_call  Endian=l  Flags=0  Version=1  Priority=0 Cookie=2
     Sender=:1.23493  Destination=org.freedesktop.DBus  Path=/org/freedesktop/DBus  Interface=org.freedesktop.DBus  Member=RequestName
     UniqueName=:1.23493
     MESSAGE "su" {
             STRING "net.poettering.Calculator";
             UINT32 4;
     };
   
   ‣ Type=method_return  Endian=l  Flags=1  Version=1  Priority=0 Cookie=4  ReplyCookie=2
     Sender=org.freedesktop.DBus  Destination=:1.23493
     MESSAGE "u" {
             UINT32 1;
     };

方法调用

  1. 执行获取Unique name中的步骤
  2. Client发送METHOD CALL类型的Message,并在消息头字段中指定Bus name/Path/Interface/Method,并附加符合方法签名的参数
  3. dbus-daemon根据Bus name字段,查找并转发给相应的Server
  4. Server根据Path/Interface/Member来查找需要执行的函数
  5. Server中的函数解析相应的参数,并执行业务逻辑,并将结果发送到dbus-daemon
  6. dbus-daemon将结果转发给Client

信号Pub-Sub

Puber端

  1. 执行获取Unique name中的步骤
  2. Puber发送SIGNAL类型的Message,并在消息头字段中指定Path/Interface/Signal,并附加符合信号签名的参数

Suber端

  1. 执行获取Unique name中的步骤
  2. 调用org.freedesktop.DBusAddMatch方法来subscribe对应的信号
   ‣ Type=method_call  Endian=l  Flags=0  Version=1  Priority=0 Cookie=2
     Sender=:1.28315  Destination=org.freedesktop.DBus  Path=/org/freedesktop/DBus  Interface=org.freedesktop.DBus  Member=AddMatch
     UniqueName=:1.28315
     MESSAGE "s" {
             STRING "type='signal',sender='org.sdbuscpp.examplemanager',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded',path='/org/sdbuscpp/examplemanager'";
     };
   
   ‣ Type=method_return  Endian=l  Flags=1  Version=1  Priority=0 Cookie=3  ReplyCookie=2
     Sender=org.freedesktop.DBus  Destination=:1.28315
     MESSAGE "" {
     };

AddMatch只有一个参数rule,类型为string,格式为逗号分割的Key-Value对,具体支持的含义如下(摘自此处):

keypossible valuesDescription
type signal, method_call, method_return, error Matches specific message type
sender Unique or well-known bus name Matches messages sent by a particular sender
interface Interface name Matches messages sent to or from the specified interface
member Any valid method or signal name Matches method and signal names
path Any valid object path Matches messages sent to or from the specified object
path_namespace Any valid object path Matches messages sent to or from all objects at or below the specified path
destination Unique or well-known bus name Matches messages sent to the specified destination
arg[0,1,2…] Any string Matches messages based on the content of their arguments. Only arguments of string type may be matched.
arg[0,1,2…]path Any string Specialized matching for path-like arguments. Ex: arg0path=/aa/bb will match /, /aa/, /aa/bb/cc but not /aa, /aa/b, or /aa/bb
arg0namespace Any string Specialized matching for partial bus names. Primarily intended for monitoring NameOwnerChanged for a group of related bus names. Ex: member="NameOwnerChanged", arg0namespace="com.foo.bar" will match "com.foo.bar.baz" and "com.foo.bar.quux"

Introspect

D-Bus技术规范要求所有的服务均实现标准接口(Starndard Interfaces),包含如下几个:

  • org.freedesktop.DBus.Introspectable
  • org.freedesktop.DBus.Peer
  • org.freedesktop.DBus.Properties
  • org.freedesktop.DBus.ObjectManager

其中busctl命令就是调用了Introspectable方法,来获取目标服务的API描述,具体步骤如下:

  1. 执行获取Unique name中的步骤
  2. 调用目标的Introspectable方法
   ‣ Type=method_call  Endian=l  Flags=0  Version=1  Priority=0 Cookie=2
     Sender=:1.26055  Destination=net.poettering.Calculator  Path=/net/poettering/Calculator  Interface=org.freedesktop.DBus.Introspectable  Member=Introspect
     UniqueName=:1.26055
     MESSAGE "" {
     };
   
   ‣ Type=method_return  Endian=l  Flags=1  Version=1  Priority=0 Cookie=52  ReplyCookie=2
     Sender=:1.25848  Destination=:1.26055
     UniqueName=:1.25848
     MESSAGE "s" {
             STRING "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
   "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
   <node>
    <interface name="org.freedesktop.DBus.Peer">
     <method name="Ping"/>
     <method name="GetMachineId">
      <arg type="s" name="machine_uuid" direction="out"/>
     </method>
    此处省略N字...
   </node>
   ";
     };

dbus-daemon配置

dbus-daemon manpage里面的CONFIGURATION FILE章节有详细描述dbus-daemon的配置。值得注意的是,在大多系统中,需要先定义新的<policy>才能往System bus注册新的服务。

sdbus-cpp的文档给出了一个示例:

<!DOCTYPE busconfig PUBLIC
 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <policy user="root">
    <allow own="org.sdbuscpp.concatenator"/>
    <allow send_destination="org.sdbuscpp"/>
    <allow send_interface="org.sdbuscpp.concatenator"/>
  </policy>
</busconfig>

sd-bus及example

sd-bus是systemd提供的一个D-Bus binding,作者在博客提供了一个Client、Server的示例代码。

Client

#include <stdio.h>
#include <stdlib.h>
#include <systemd/sd-bus.h>

int main(int argc, char *argv[]) {
        sd_bus_error error = SD_BUS_ERROR_NULL;
        sd_bus_message *m = NULL;
        sd_bus *bus = NULL;
        const char *path;
        int r;

        /* Connect to the system bus */
        r = sd_bus_open_system(&bus);
        if (r < 0) {
                fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
                goto finish;
        }

        /* Issue the method call and store the respons message in m */
        r = sd_bus_call_method(bus,
                               "org.freedesktop.systemd1",           /* service to contact */
                               "/org/freedesktop/systemd1",          /* object path */
                               "org.freedesktop.systemd1.Manager",   /* interface name */
                               "StartUnit",                          /* method name */
                               &error,                               /* object to return error in */
                               &m,                                   /* return message on success */
                               "ss",                                 /* input signature */
                               "cups.service",                       /* first argument */
                               "replace");                           /* second argument */
        if (r < 0) {
                fprintf(stderr, "Failed to issue method call: %s\n", error.message);
                goto finish;
        }

        /* Parse the response message */
        r = sd_bus_message_read(m, "o", &path);
        if (r < 0) {
                fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r));
                goto finish;
        }

        printf("Queued service job as %s.\n", path);

finish:
        sd_bus_error_free(&error);
        sd_bus_message_unref(m);
        sd_bus_unref(bus);

        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}

Server

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <systemd/sd-bus.h>

static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
        int64_t x, y;
        int r;

        /* Read the parameters */
        r = sd_bus_message_read(m, "xx", &x, &y);
        if (r < 0) {
                fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
                return r;
        }

        /* Reply with the response */
        return sd_bus_reply_method_return(m, "x", x * y);
}

static int method_divide(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
        int64_t x, y;
        int r;

        /* Read the parameters */
        r = sd_bus_message_read(m, "xx", &x, &y);
        if (r < 0) {
                fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
                return r;
        }

        /* Return an error on division by zero */
        if (y == 0) {
                sd_bus_error_set_const(ret_error, "net.poettering.DivisionByZero", "Sorry, can't allow division by zero.");
                return -EINVAL;
        }

        return sd_bus_reply_method_return(m, "x", x / y);
}

/* The vtable of our little object, implements the net.poettering.Calculator interface */
static const sd_bus_vtable calculator_vtable[] = {
        SD_BUS_VTABLE_START(0),
        SD_BUS_METHOD("Multiply", "xx", "x", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),
        SD_BUS_METHOD("Divide",   "xx", "x", method_divide,   SD_BUS_VTABLE_UNPRIVILEGED),
        SD_BUS_VTABLE_END
};

int main(int argc, char *argv[]) {
        sd_bus_slot *slot = NULL;
        sd_bus *bus = NULL;
        int r;

        /* Connect to the user bus this time */
        r = sd_bus_open_user(&bus);
        if (r < 0) {
                fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
                goto finish;
        }

        /* Install the object */
        r = sd_bus_add_object_vtable(bus,
                                     &slot,
                                     "/net/poettering/Calculator",  /* object path */
                                     "net.poettering.Calculator",   /* interface name */
                                     calculator_vtable,
                                     NULL);
        if (r < 0) {
                fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));
                goto finish;
        }

        /* Take a well-known service name so that clients can find us */
        r = sd_bus_request_name(bus, "net.poettering.Calculator", 0);
        if (r < 0) {
                fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));
                goto finish;
        }

        for (;;) {
                /* Process requests */
                r = sd_bus_process(bus, NULL);
                if (r < 0) {
                        fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));
                        goto finish;
                }
                if (r > 0) /* we processed a request, try to process another one, right-away */
                        continue;

                /* Wait for the next request to process */
                r = sd_bus_wait(bus, (uint64_t) -1);
                if (r < 0) {
                        fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));
                        goto finish;
                }
        }

finish:
        sd_bus_slot_unref(slot);
        sd_bus_unref(bus);

        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}

参考

官方主页

D-Bus overview

D-Bus specification

sdbus-cpp@Github

D-Bus@Github

The new sd-bus API of systemd

Introspect dtd

Qt D-Bus官方主页

dbus-daemon manpage