Перевод статьи написанной Íñigo Huguet для сайта Fedora Magazine. Тут будет общее описание и принципы работы D-Bus.

Что такое D-Bus

D-Bus служит различным целям, правленным на облегчение взаимодействия различных процессов в системе. В этой статье будет описан D-Bus и то, как он выполняет эту функцию.

Из определения создателей D-Bus:

D-Bus - это система шины сообщений, простой способ взаимодействия приложений друг с другом. Помимо межпроцессного взаимодействия, D-Bus помогает координировать жизненный цикл процесса; это позволяет просто и надежно писать код приложения или демона с «одним экземпляром», а также запускать приложения и демоны по требованию, когда их услуги необходимы.

D-Bus - это, главным образом, протокол межпроцессного взаимодействия (IPC). Его главное преимущество заключается в том, что приложениям не нужно установить индивидуальную связь со всеми остальными процессами, с которыми им необходимо общаться. Вместо этого D-Bus предлагает виртуальную шину, к которой подключаются все приложения и отправляют сообщения.

Процессы без D-BusProcesses with D-Bus
Процессы без D-BusProcesses with D-Bus

Достижение желаемого приложения

Мы говорим о системе D-Bus, но в системе может быть более одной шины. На самом деле, обычно их как минимум 2:

  • Системная шина: приложения, управляющие общесистемными ресурсами, подключенными к ней.
  • Одна сеансовая шина на каждого вошедшего в систему пользователя: настольные приложения обычно используют эту шину.

Чтобы отправлять сообщения D-Bus в желаемый пункт назначения по шине, необходим способ идентификации каждого из приложений, подключенных к шине. По этой причине каждое приложение получает по крайней мере одно имя шины, которое другие могут указать в качестве места назначения своих сообщений. Существует два типа имен:

  • Уникальные имена соединений: Каждое приложение, подключающееся к шине, автоматически получает его. Это уникальный идентификатор, который никогда не будет повторно использоваться для другого приложения. Эти имена начинаются с двоеточия (:) и обычно выглядят примерно так: :1.94 (символы после двоеточия не имеют особого значения, но уникальны).
  • Известные имена: Хотя уникальные имена соединений назначаются динамически, приложения должны быть легко обнаружены и должны иметь фиксированное имя, которое хорошо известно. Они выглядят как перевернутые доменные имена, похожие на org.freedesktop.NetworkManager.

Вы можете увидеть, какие приложения подключены к двум основным шинам, используя команды busctl ниже. Мы обнаружим, что в настоящее время многие основные компоненты системы используют D-Bus. В следующих примерах опустите параметр –acquired, чтобы увидеть уникальные имена соединений.

1
2
3
4
$ busctl list --system --acquired
...
$ busctl list --user --acquired
...

По аналогии с IP-сетями уникальные имена соединений подобны динамическим IP-адресам, а общеизвестные имена подобны именам хостов или доменным именам.

Например, NetworkManager - это демон, который управляет сетевой конфигурацией системы. Клиенты, такие как центр управления GNOME или инструменты командной строки nmcli и nmstate, подключаются к системной шине и отправляют сообщения D-Bus на широко известное имя org.freedesktop.NetworkManager, чтобы получить информацию или запросить изменения.

Примечание: в спецификации D-Bus используется термин “имя шины”, так что это как-то официально. Это может сбивать с толку, поскольку люди склонны думать, что это относится к разным шинам, а не к приложениям, подключенным к одной шине. Во многих статьях и инструментах используется термин “сервис” вместо “имя шины”, потому что он более интуитивен, но в спецификации он имеет другое значение, поэтому не рекомендуется. В этой статье я использую термин “пункт назначения” везде, где это возможно. В любом случае помните, что “имя шины” относится к “имени В шине”, а не к “имени шины”.

Объекты D-Bus

Приложения D-Bus могут предоставлять некоторые из своих ресурсов и действий в том, что мы могли бы назвать API D-Bus.

Открытые ресурсы напоминают основные концепции объектно-ориентированных языков программирования, поэтому их называют объектами D-Bus. В частности, каждое приложение может предоставлять неопределенное количество объектов со свойствами и методами. Отдельные объекты D-Bus идентифицируются по своим путям D-Bus, которые очень похожи на пути Unix (т. е. /path/to/the/object).

Вы можете проверить, какие объекты предоставляет определенное приложение, с помощью команды busctl. Например:

1
$ busctl tree org.freedesktop.NetworkManager

Приложения могут интуитивно выражать иерархию между объектами благодаря этому идентификатору, напоминающему путь. Легко понять, что /Background/Color - это объект, который иерархически принадлежит объекту /Background. Спецификация не требует соблюдения этой иерархической структуры, но почти все приложения это делают.

Примечание. Во многих приложениях пути к объектам обычно начинаются с обратного домена автора (т. е. /org/freedesktop/NetworkManager/*). Это позволяет избежать конфликтов имен с другими объектами из разных библиотек или источников. Однако это не является обязательным и не все приложения это делают.

Интерфейсы, методы и свойства D-Bus

Каждый объект D-Bus реализует один или несколько интерфейсов D-Bus, и каждый интерфейс определяет некоторые свойства и методы. Свойства подобны переменным, а методы - функциям. Имена интерфейсов D-Bus выглядят так: org.example.AppName.InterfaceName.

Если вы уже знаете какой-либо язык программирования, использующий концепцию интерфейса, например Java, он будет вам знаком. Если нет, просто помните, что интерфейсы подобны типам объектов, и каждый объект может иметь более одного типа одновременно. Но имейте в виду, что, в отличие от того, что происходит во многих из этих языков, объекты D-Bus никогда не определяют прямых членов, а только интерфейсы.

Пример:

  • Приложение сервера печати определяет следующие интерфейсы:
    • Принтер: определяет метод PrintDocument и свойство InkLevel.
    • Сканер: определяет метод Сканировать документ.
  • Объект /Devices/1 представляет собой обычный принтер, поэтому он реализует только интерфейс Принтер.
  • Объект /Devices/2 представляет принтер со сканером, поэтому реализует оба интерфейса.

Интроспекция

Интроспекция позволяет вам получить список интерфейсов с их методами и свойствами, которые реализует объект D-Bus. Например, команда busctl используется следующим образом:

1
$ busctl introspect org.freedesktop.NetworkManager /org/freedesktop/NetworkManager

Методы и свойства

Теперь давайте посмотрим, как вызывать методы D-Bus и читать свойства из терминала на двух примерах:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Method call
busctl call                                              \
    --system                                             \
    org.freedesktop.NetworkManager           # app name  \
    /org/freedesktop/NetworkManager/Settings # object    \
    org.freedesktop.NetworkManager.Settings  # interface \
    ListConnections                          # method

# Property read
busctl get-property                                         \
    --system                                                \
    org.freedesktop.NetworkManager              # app name  \
    /org/freedesktop/NetworkManager/Devices/2   # object    \
    org.freedesktop.NetworkManager.Device.Wired # interface \
    PermHwAddress                               # property

Чтобы читать или записывать свойства, нам фактически нужно вызывать методы стандартного интерфейса org.freedesktop.DBus.Properties, определенного спецификациями D-Bus. Busctl, однако, делает это скрытно с помощью команды get/set-property, чтобы немного абстрагироваться от этого. Совет по упражнению: прочитайте свойство с помощью вызова busctl (подсказка: вам нужно указать сигнатуру аргументов, то есть ss).

Примечание: при вызове методов D-Bus указывать интерфейс не обязательно, но если два интерфейса определяют одно и то же имя метода, результат не определен, поэтому его следует всегда указывать. По этой причине инструмент busctl делает это обязательным.

Система типов D-Bus

D-Bus использует строгую систему типов, поэтому все свойства, аргументы и возвращаемые значения должны иметь четко определенный тип.

Существует несколько основных типов, таких как BYTE, BOOLEAN, INT32, UINT32, DOUBLE, STRING или OBJECT_PATH. Эти базовые типы можно сгруппировать в три различных типа контейнеров: STRUCT, ARRAY и DICT_ENTRY (словари). Контейнеры могут быть вложены в другие контейнеры того же или другого типа.

Существует также тип VARIANT, который допускает динамическую типизацию.

Каждый тип можно идентифицировать по сигнатуре. Сигнатуры базовых типов представляют собой одиночные символы, такие как i, u, s и т. д. Сигнатуры составных типов представляют собой строки типа (iibs), ai или a{s(ii)}. Полное описание всех типов и сигнатур выходит за рамки этой статьи, но в зависимости от используемого вами языка и/или библиотеки D-Bus вам потребуются по крайней мере некоторые знания о том, как указать тип значений, передаваемых или получаемых. Дополнительную информацию см. в спецификации D-Bus.

Собираем все вместе (пример на Python)

Теперь, когда мы знаем основные понятия D-Bus, мы готовы отправлять сообщения D-Bus. Давайте посмотрим полный пример Python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env python3

# Import the 'dbus' module from dbus-python package.
# The package can be installed with `pip install dbus-python`.
# Documenation: https://dbus.freedesktop.org/doc/dbus-python/
import dbus

# We need the NetworkManager's D-Bus API documentation to
# know what objects and methods we are interested in:
# https://networkmanager.dev/docs/api/latest/spec.html

# We'll connect to system bus
bus = dbus.SystemBus()

# We'll send our messages to NetworkManager
NM_WELL_KNOWN_NAME = "org.freedesktop.NetworkManager"

# Call to the following method:
#  - object: /org/freedesktop/NetworkManager
#  - interface: org.freedesktop.NetworkManager
#  - method: GetDeviceByIpIface
#  - input argument: "eth0" (type: STRING)
#  - return value: the device's path (type: OBJECT_PATH)
#
# Get the path to the object that represents the device with
# the interface name "eth0".
nm_dbus_obj = bus.get_object(
    NM_WELL_KNOWN_NAME, "/org/freedesktop/NetworkManager"
)
nm_dbus_iface = dbus.Interface(
    nm_dbus_obj, "org.freedesktop.NetworkManager"
)
try:
    device_dbus_path = nm_dbus_iface.GetDeviceByIpIface("eth0")
except dbus.exceptions.DBusException as e:
    print("D-Bus error: " + str(e))
    quit()

print("D-Bus path to eth0 device: " + str(device_dbus_path))

# Call to the following method:
#  - object: the device that we obtained in the previous step
#  - interface: org.freedesktop.NetworkManager.Device
#  - method: Disconnect
#
# Request to the NM daemon to disconnect the device
# Note: NM will return an error if it was already disconnected
device_dbus_obj = bus.get_object(
    NM_WELL_KNOWN_NAME, device_dbus_path
)
device_dbus_iface = dbus.Interface(
    device_dbus_obj, "org.freedesktop.NetworkManager.Device"
)
try:
    device_dbus_iface.Disconnect()
except dbus.exceptions.DBusException as e:
    print("D-Bus error: " + str(e))
    quit()

print("Device disconnected")

Обратите внимание, что нам не нужно было указывать тип аргумента метода. Это связано с тем, что dbus-python делает все возможное, чтобы преобразовать значения Python в эквивалентные значения D-Bus (т. е. str в STRING, list в ARRAY, dict в DICT_ENTRY и т. д.). Однако, поскольку D-Bus имеет строгую систему типов, вам нужно будет указать тип в случае неоднозначности. Например, для целочисленных типов вам часто потребуется использовать dbus.UInt16(значение), dbus.Int64(значение) и т. д.

Дополнительные возможности D-Bus

Еще одной важной и широко используемой функцией D-Bus являются сигнальные сообщения. Приложения могут подписываться на сигналы других приложений, указывая, какие из них им интересны. Приложение-продюсер отправляет сигнальные сообщения при возникновении определенных событий, а подписчик получает их асинхронно. Это позволяет избежать необходимости постоянного опроса.

В D-Bus есть много других полезных функций, таких как активация служб, аутентификация, самоанализ и т. д., которые выходят далеко за рамки этой статьи.

Полезные инструменты

Для командной строки busctl - наиболее интуитивно понятный и полный инструмент с такими замечательными функциями, как мониторинг или захват трафика D-Bus в виде файла pcap. Однако типы значений должны быть указаны как сигнатуры D-Bus, что сложно, если вы их не очень хорошо знаете. В этом случае проще использовать dbus-send из пакета dbus-tools или qdbus из Qt.

Когда вы начинаете экспериментировать с D-Bus, изучение иерархии объектов различных приложений становится намного проще с помощью инструмента с графическим интерфейсом, такого как Qt DBUS Viewer (QDbusViewer).

Screenshot of QDbusViewer

Рекомендованные статьи