В новом выпуске systemd v221 мы представляем API sd-bus, поставляемый со стабильной версией systemd. sd-bus - это наша минимальная библиотека D-Bus IPC на языке программирования Си, поддерживающая в качестве бэкэндов как классическую D-Bus на основе сокетов, так и kdbus. Библиотека была частью systemd в течение некоторого времени, но использовалась только внутри проекта, поскольку мы хотели свободно вносить изменения в API, не затрагивая внешних пользователей. Однако теперь, начиная с v221, мы уверены, что сделали стабильный API.

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

Что такое D-Bus?

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

D-Bus является основным компонентом систем Linux более 10 лет. Это, безусловно, наиболее широко распространенная локальная система IPC высокого уровня в Linux. С момента создания systemd - это была система IPC, в которой она предоставляла свои интерфейсы. И даже до systemd это была система IPC, которую Upstart использовал для своих интерфейсов. Она используется GNOME, KDE и множеством системных компонентов.

К D-Bus относится как спецификация, так и эталонная реализация. Эталонная реализация предоставляет как компонент сервера шины, так и клиентскую библиотеку. Хотя существует множество других популярных последующих реализаций клиентской библиотеки, как для Си, так и для других языков программирования, единственная широко используемая серверная сторона - это та, которая указана в эталонной реализации. (Однако проект kdbus работает над предоставлением альтернативы этой реализации сервера в качестве компонента ядра.)

D-Bus в основном используется как локальный IPC поверх сокетов AF_UNIX. Однако протокол также можно использовать поверх TCP/IP. Он изначально не поддерживает шифрование, поэтому использование D-Bus напрямую через TCP обычно не является хорошей идеей. Можно объединить D-Bus с транспортом, таким как ssh, чтобы защитить его. systemd использует это, чтобы сделать многие из своих API доступными удаленно.

Часто задаваемый вопрос о D-Bus: почему он вообще существует, учитывая, что сокеты AF_UNIX и FIFO уже есть в UNIX и долгое время успешно используются. Чтобы ответить на этот вопрос, давайте сравним D-Bus с популярными сегодня веб-технологиями: AF_UNIX/FIFO для D-Bus тоже самое, что TCP для HTTP/REST. В то время, как сокеты AF_UNIX/FIFO только перекладывают необработанные байты между процессами, D-Bus определяет фактическую кодировку сообщений и добавляет такие концепции, как транзакция вызова методов, система объектов, механизмы безопасности, многоадресная передача сообщений и многое другое.

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

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

Для чего подходит sd-bus?

Давайте обсудим, для чего написана библиотека sd-bus, как она соотносится с другими библиотеками D-Bus и почему она может стать библиотекой для вашего проекта.

Для языка программирования Си существуют две популярные библиотеки D-Bus: libdbus, поставляемая в эталонной реализации D-Bus, а также GDBus, компонент GLib, низкоуровневой инструментальной библиотеки GNOME.

Из этих двух библиотек libdbus намного старше, так как она была написана во время составления спецификации. Она была написана с упором на то, чтобы быть переносимой и полезной в качестве серверной части для привязок языков более высокого уровня. Обе эти цели требовали, чтобы API был очень универсальным, в результате чего получился относительно сложный в использовании API в котором отсутствуют элементы, которые делают его легким и интересным для использования из Си. Он предоставляет строительные блоки, но в нём мало инструментов, чтобы упростить строительство дома из них. С другой стороны, библиотека подходит для большинства случаев использования (например, она OOM безопасна, что делает ее подходящей для написания системного программного обеспечения самого низкого уровня) и переносима в операционные системы, такие как Windows или более экзотические UNIX.

GDBus - это гораздо более новая реализация. Она была написана после значительного опыта использования оболочки GLib/GObject вокруг libdbus. GDBus реализована с нуля, не имеет общего кода с libdbus. Её дизайн существенно отличается от libdbus, он содержит генераторы кода, чтобы упростить размещение объектов GObject на шине или взаимодействие с объектами D-Bus как с объектами GObject. Она переводит типы данных D-Bus в GVariant, который является мощным форматом сериализации данных GLib. Если вы привыкли к программированию в стиле GLib, тогда вы почувствуете себя как дома, использовать сервисы и клиенты D-Bus с её помощью намного проще, чем с libdbus.

С sd-bus мы теперь предоставляем третью реализацию, не разделяющую кода ни с libdbus, ни с GDBus. Для нас основное внимание было уделено обеспечению своего рода промежуточного звена между libdbus и GDBus: низкоуровневой библиотекой Си, с которой действительно интересно работать, которая имеет достаточно синтаксического сахара, чтобы упростить создание клиентов и сервисов, но, с другой стороны, более низкоуровневой, чем GDBus/GLib/GObject/GVariant. Чтобы использовать её в различных компонентах системного уровня systemd, она должен быть компактной и безопасной для OOM. Еще одним важным моментом, на котором мы хотели сосредоточиться, была поддержка бэкэнда kdbus с самого начала в дополнение к транспорту сокетов исходной спецификации D-Bus («dbus1»). Фактически, мы хотели спроектировать библиотеку ближе к семантике kdbus, чем к dbus1, где-то были бы отличия, но при этом чтобы хорошо охватывались оба транспорта. В отличие от libdbus или GDBus, переносимость не является приоритетом для sd-bus, вместо этого мы стараемся максимально использовать платформу Linux и раскрывать конкретные концепции Linux везде, где это выгодно. Наконец, производительность также была проблемой (хотя и второстепенной): ни libdbus, ни GDBus не побьют рекорды скорости. Мы хотели улучшить производительность (пропускную способность и задержку), но для нас важнее простота и правильность. Мы считаем, что результат нашей работы вполне соответствует нашим целям: библиотеку приятно использовать, она поддерживает kdbus и сокеты в качестве серверной части, относительно минимальна, а производительность существенно выше, чем у libdbus и GDBus.

Вот краткие рекомендации, чтобы решить, какой из трех API использовать для вашего проекта на Си:

  • Если вы разрабатываете проект GLib/GObject, GDBus определенно будет вашим лучшим выбором.

  • Если для вас важна переносимость на ядра, отличные от Linux, включая Windows, Mac OS и другие UNIX, используйте либо GDBus (что более или менее означает использование GLib/GObject), либо libdbus (что требует большого количества ручной работы).

  • В противном случае я бы рекомендовал использовать sd-bus.

(Я не рассматриваю здесь C++, речь идет только о простом Си. Но обратите внимание: если вы используете Qt, то QtDBus является предпочтительным API D-Bus, являясь оболочкой для libdbus.)

Введение в концепции D-Bus

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

  • Шина - это то место, где вы ищете услуги IPC. Обычно существует два типа шин: системная шина, одна на систему, на которой располагаются системные службы; и пользовательская шина, одна на каждого пользователя, на которой располагаются пользовательские службы, такие как адресная книга или почтовый клиент. (Первоначально пользовательская шина была на самом деле сеансовой это значит, что вы получаете несколько шин, если входите в систему много раз как один и тот же пользователь, и в большинстве настроек так и остается, но мы работаем над истинной пользовательской шиной, которая существует в единственном экземпляре для каждого пользователя в системе, независимо от того, сколько раз этот пользователь входит в систему.)

  • Сервис - это программа, которая предлагает некоторый IPC API на шине. Служба идентифицируется именем в обратной нотации доменного имени. Таким образом, служба org.freedesktop.NetworkManager на системной шине - это то место, где доступны API-интерфейсы NetworkManager, а org.freedesktop.login1 на системной шине - это место, где доступны API-интерфейсы systemd-logind.

  • Клиент - это программа, которая использует некоторый IPC API на шине. Он общается с сервисом, контролирует его и, как правило, не предоставляет никаких услуг самостоятельно. При этом границы размыты, и многие службы также являются клиентами других служб. Часто термин узел используется как обобщение для обозначения сервиса или клиента.

  • Путь к объекту - это идентификатор объекта в определенной службе. В некотором смысле это сравнимо с указателем Си, поскольку именно так вы обычно ссылаетесь на объект Си, если пишите объектно-ориентированные программы на Си. Однако указатели Си - это просто адреса памяти, и передача адресов памяти другим процессам не имеет смысла, поскольку они относятся к адресному пространству сервиса и клиент не может получить достап к ним. Таким образом, разработчики D-Bus придумали концепцию пути к объекту, который представляет собой просто строку, которая выглядит как путь в файловой системе. Пример: /org/freedesktop/login1 - это путь к объекту «менеджер» сервиса org.freedesktop.login1 (который, как мы помним из вышеизложенного, является сервисом systemd-logind). Поскольку пути к объектам структурированы как пути файловой системы, их можно аккуратно упорядочить в виде дерева и получить полное дерево объектов. Например, вы найдете все пользовательские сеансы, которыми управляет systemd-logind в ветке /org/freedesktop/login1/session, к примеру: /org/freedesktop/login1/session/_7, /org/freedesktop/login1/session./_55 и так далее. Как сервисы именуют свои объекты и размещают их в дереве, полностью зависит от их разработчиков.

  • Каждый объект, который определяется путем, имеет один или несколько интерфейсов. Интерфейс - это набор сигналов, методов и свойств (вместе называемых членами), которые связаны друг с другом. Концепция интерфейсов D-Bus на самом деле в значительной степени идентична тому, что вы знаете из языков программирования, таких как Java, которые её поддерживают. Какие интерфейсы реализует объект, определяют разработчики сервиса. Имена интерфейсов имеют обратную нотацию доменных имен, как и имена сервисов. (Да, это, по общему признанию, сбивает с толку, поскольку для простых сервисов довольно часто встречается использование строки имени сервиса также в качестве имени интерфейса.) Тем не менее, несколько интерфейсов стандартизированы, и вы найдете их доступными для многих объектов, реализуемых различными сервисами. В частности, это org.freedesktop.DBus.Introspectable, org.freedesktop.DBus.Peer и org.freedesktop.DBus.Properties.

  • Интерфейс может содержать методы. Слово «метод» более или менее просто причудливое определение для «функции», и этот термин используется почти так же в объектно-ориентированных языках, таких как Java. Наиболее распространенное взаимодействие между узлами D-Bus заключается в том, что один узел вызывает метод на другом узле и получает ответ. Метод D-Bus принимает и возвращает несколько параметров. Параметры передаются безопасным для типов способом, а информация о типе включается в данные интроспекции, которые вы можете запросить у каждого объекта. Обычно имена методов (и других типов членов) следуют синтаксису CamelCase. Например, systemd-logind предоставляет метод ActivateSession в интерфейсе org.freedesktop.login1.Manager, который доступен в объекте /org/freedesktop/login1 сервиса org.freedesktop.login1.

  • Сигнатура описывает набор параметров, которые принимает или возвращает функция (или сигнал, свойство, см. ниже). Это последовательность символов, каждый из которых определяет тип соответствующего параметра. Набор доступных типов довольно мощный. Например, есть более простые типы, такие как s для строки или u для 32-битного целого числа, но также и сложные типы, такие как as для массива строк или a(sb) для массива структур, состоящих из одной строки и одного логического значения. См. Спецификацию D-Bus, где приводится полное описание системы типов. Упомянутый выше метод ActivateSession принимает одну строку в качестве параметра (сигнатура параметра, следовательно, равна s) и ничего не возвращает (сигнатура возврата, следовательно, является пустой строкой). Конечно, сигнатура может быть намного сложнее, другие примеры см. ниже.

  • Сигнал - это еще один тип элемента, определяемый в объектной системе D-Bus. Как и у метода, у него есть сигнатура. Однако они служат разным целям. В то время как в вызове метода один клиент отправляет запрос к одному сервису, и этот сервис возвращает ответ клиенту, сигналы предназначены для общего уведомления узлов. Сервисы отправляют их, когда хотят сообщить одному или нескольким узлам на шине, что что-то произошло или изменилось. В отличие от вызовов методов и их ответов, они обычно транслируются по всей шине. В то время как вызовы/ответы методов используются для дуплексной связи один-к-одному, сигналы обычно используются для симплексной связи один-ко-многим (обратите внимание, что это не является обязательным требованием, их также можно использовать один-к-одному). Пример: systemd-logind передает сигнал SessionNew от своего объекта-менеджера каждый раз, когда пользователь входит в систему, и сигнал SessionRemoved каждый раз, когда пользователь выходит из системы.

  • Свойство - это третий тип элементов, определяемый в объектной системе D-Bus. Это похоже на концепцию свойств, известную в таких языках, как C#. Свойства также имеют сигнатуру. Они представляют собой переменные, предоставляемые объектом, которые могут быть прочитаны или изменены клиентами. Пример: systemd-logind предоставляет свойство Docked с сигнатурой b (логическое значение). Оно отражает, считает ли systemd-logind, что система в настоящее время находится в док-станции (применимо только к ноутбукам).

D-Bus определяет много различных концепций. Конечно, все эти новые концепции могут быть непонятными. Давайте посмотрим на них с другой точки зрения. Я предполагаю, что многие из читателей имеют представление о сегодняшних веб-технологиях, в частности о HTTP и REST. Попробуем сравнить концепцию HTTP-запроса с концепцией вызова метода D-Bus:

  • HTTP-запрос, который вы отправляете в определенной сети. Это может быть Интернет, ваша локальная сеть или корпоративный VPN. В зависимости от того, в какой сети вы отправляете запрос, вы сможете общаться с определённым набором серверов. Это мало чем отличается от «шинной» концепции D-Bus.

  • Затем в сети вы выбираете конкретный HTTP-сервер для общения. Это примерно сопоставимо с выбором сервиса на конкретной шине.

  • Затем на HTTP-сервере вы запрашиваете конкретный URL-адрес. Часть URL-адреса, определяющая путь (под которой я подразумеваю все после имени хоста сервера, вплоть до последнего «/») очень похожа на путь к объекту D-Bus.

  • «Файловая» часть URL-адреса (под которой я подразумеваю все, что находится после последней косой черты, следующее за путём, который описан выше), определяет фактический вызов, который нужно сделать. В D-Bus это может быть сопоставлено с именем интерфейса и метода.

  • Наконец, параметры HTTP-вызова следуют в пути после знака «?», Они отображаются на сигнатуру вызова D-Bus.

Конечно, сравнение HTTP-запроса с вызовом метода D-Bus похоже на сравнение яблок и апельсинов. Тем не менее, я думаю, что полезно получить представление о том, что чему соответствует.

Из оболочки

Так много о концепциях и стоящей за ними серой теории. Давайте сделаем это увлекательным, давайте посмотрим, как это ощущается в реальной системе.

Некоторое время назад в systemd был включен инструмент busctl, который полезен для изучения и взаимодействия с объектной системой D-Bus. При вызове без параметров он покажет вам список всех узлов, подключенных к системной шине. (Вместо этого используйте --user, чтобы увидеть узлы вашей пользовательской шины):

$ busctl
NAME                                       PID PROCESS         USER             CONNECTION    UNIT                      SESSION    DESCRIPTION
:1.1                                         1 systemd         root             :1.1          -                         -          -
:1.11                                      705 NetworkManager  root             :1.11         NetworkManager.service    -          -
:1.14                                      744 gdm             root             :1.14         gdm.service               -          -
:1.4                                       708 systemd-logind  root             :1.4          systemd-logind.service    -          -
:1.7200                                  17563 busctl          lennart          :1.7200       session-1.scope           1          -
[…]
org.freedesktop.NetworkManager             705 NetworkManager  root             :1.11         NetworkManager.service    -          -
org.freedesktop.login1                     708 systemd-logind  root             :1.4          systemd-logind.service    -          -
org.freedesktop.systemd1                     1 systemd         root             :1.1          -                         -          -
org.gnome.DisplayManager                   744 gdm             root             :1.14         gdm.service               -          -
[…]

(Я немного сократил вывод, чтобы быть кратким).

Список начинается с узлов, подключенных в данный момент к шине. Они идентифицируются по именам, например ":1.11". В номенклатуре D-Bus они называются уникальными именами. По сути, каждый узел имеет уникальное имя, и они назначаются автоматически, когда узел подключается к шине. Если хотите, они очень похожи на IP-адрес. Вы заметите, что несколько узлов уже подключены, включая сам наш небольшой инструмент busctl, а также ряд системных сервисов. Затем в списке отображаются все текущие сервисы на шине, идентифицируемые по именам сервисов (как обсуждалось выше; чтобы отличить их от уникальных имен, они также называются хорошо известными именами). Во многих отношениях хорошо известные имена похожи на имена хостов DNS, то есть они являются более удобным способом ссылки на узел, но на нижнем уровне они просто сопоставляются с IP-адресом или, в этом сравнении, с уникальным именем. Подобно тому, как вы можете подключиться к хосту в Интернете либо по его имени, либо по его IP-адресу, вы также можете подключиться к узлу шины либо по его уникальному, либо по его общеизвестному имени. (Обратите внимание, что каждый узел может иметь сколько угодно хорошо известных имен, подобно тому, как IP-адрес может иметь несколько имен хостов, ссылающихся на него).

Ладно, это уже круто. Попробуйте сами, на своем локальном компьютере (все, что вам нужно, это современный дистрибутив на основе systemd).

Теперь перейдем к следующему шагу. Посмотрим, какие объекты на самом деле предлагает сервис org.freedesktop.login1:

$ busctl tree org.freedesktop.login1
L-/org/freedesktop/login1
  +-/org/freedesktop/login1/seat
  ¦ +-/org/freedesktop/login1/seat/seat0
  ¦ L-/org/freedesktop/login1/seat/self
  +-/org/freedesktop/login1/session
  ¦ +-/org/freedesktop/login1/session/_31
  ¦ L-/org/freedesktop/login1/session/self
  L-/org/freedesktop/login1/user
    +-/org/freedesktop/login1/user/_1000
    L-/org/freedesktop/login1/user/self

Красиво, не правда ли? Что на самом деле еще приятнее и чего не видно в выводе, так это то, что доступно полное автозавершение слов из командной строки: когда вы нажимаете TAB, оболочка автоматически заполняет имена служб за вас. Это замечательный инструмент для исследования объектов D-Bus!

В выводе показаны некоторые объекты, которые вы можете узнать из объяснений выше. А теперь пойдем дальше. Давайте посмотрим, какие интерфейсы, методы, сигналы и свойства фактически предоставляет один из этих объектов:

$ busctl introspect org.freedesktop.login1 /org/freedesktop/login1/session/_31
NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGS
org.freedesktop.DBus.Introspectable interface -         -                                        -
.Introspect                         method    -         s                                        -
org.freedesktop.DBus.Peer           interface -         -                                        -
.GetMachineId                       method    -         s                                        -
.Ping                               method    -         -                                        -
org.freedesktop.DBus.Properties     interface -         -                                        -
.Get                                method    ss        v                                        -
.GetAll                             method    s         a{sv}                                    -
.Set                                method    ssv       -                                        -
.PropertiesChanged                  signal    sa{sv}as  -                                        -
org.freedesktop.login1.Session      interface -         -                                        -
.Activate                           method    -         -                                        -
.Kill                               method    si        -                                        -
.Lock                               method    -         -                                        -
.PauseDeviceComplete                method    uu        -                                        -
.ReleaseControl                     method    -         -                                        -
.ReleaseDevice                      method    uu        -                                        -
.SetIdleHint                        method    b         -                                        -
.TakeControl                        method    b         -                                        -
.TakeDevice                         method    uu        hb                                       -
.Terminate                          method    -         -                                        -
.Unlock                             method    -         -                                        -
.Active                             property  b         true                                     emits-change
.Audit                              property  u         1                                        const
.Class                              property  s         "user"                                   const
.Desktop                            property  s         ""                                       const
.Display                            property  s         ""                                       const
.Id                                 property  s         "1"                                      const
.IdleHint                           property  b         true                                     emits-change
.IdleSinceHint                      property  t         1434494624206001                         emits-change
.IdleSinceHintMonotonic             property  t         0                                        emits-change
.Leader                             property  u         762                                      const
.Name                               property  s         "lennart"                                const
.Remote                             property  b         false                                    const
.RemoteHost                         property  s         ""                                       const
.RemoteUser                         property  s         ""                                       const
.Scope                              property  s         "session-1.scope"                        const
.Seat                               property  (so)      "seat0" "/org/freedesktop/login1/seat... const
.Service                            property  s         "gdm-autologin"                          const
.State                              property  s         "active"                                 -
.TTY                                property  s         "/dev/tty1"                              const
.Timestamp                          property  t         1434494630344367                         const
.TimestampMonotonic                 property  t         34814579                                 const
.Type                               property  s         "x11"                                    const
.User                               property  (uo)      1000 "/org/freedesktop/login1/user/_1... const
.VTNr                               property  u         1                                        const
.Lock                               signal    -         -                                        -
.PauseDevice                        signal    uus       -                                        -
.ResumeDevice                       signal    uuh       -                                        -
.Unlock                             signal    -         -                                        -

Как и раньше, команда busctl поддерживает автозавершение командной строки, поэтому и имя службы, и путь к объекту легко объединяются в оболочке простым нажатием TAB. Вывод показывает методы, свойства, сигналы одного из объектов сеанса, который в настоящее время доступен через systemd-logind. Есть раздел для каждого интерфейса, который известен объекту. Во втором столбце указывается тип члена. В третьем столбце отображается сигнатура члена. В случае методов, она описывает входные параметры. Четвертый столбец показывает возвращаемые параметры. Для свойств четвертый столбец кодирует их текущее значение.

Пока что мы только исследовали. Теперь сделаем следующий шаг: станем активными - вызовем метод:

# busctl call org.freedesktop.login1 /org/freedesktop/login1/session/_31 org.freedesktop.login1.Session Lock

Я не думаю, что мне нужно больше об этом упоминать, но в любом случае: снова доступно полное автозавершение командной строки. Третий аргумент - это имя интерфейса, четвертый - имя метода, оба могут быть легко заполнены нажатием TAB. В этом случае мы выбрали метод Lock, который активирует блокировку экрана для определенного сеанса. И оп, в тот момент, когда я нажал Enter в этой строке, у меня включилась блокировка экрана (это работает только на оконных менеджерах, которые правильно подключаются к systemd-logind. GNOME работает нормально, и KDE тоже должен работать).

Выбранный нами вызов метода Lock очень прост, поскольку он не принимает никаких параметров и не возвращает их. Конечно, для некоторых вызовов всё может быть сложнее. Вот еще один пример, на этот раз с использованием одного из вызовов на собственной шине systemd для запуска произвольного системного модуля:

# busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager StartUnit ss "cups.service" "replace"
o "/org/freedesktop/systemd1/job/42684"

Этот вызов принимает две строки в качестве входных параметров, как описано в сигнатуре, которая следует за именем метода (как обычно, автозавершение командной строки помогает вам понять, как ввести параметры правильно). Следующие за сигнатурой два параметра - это просто две передаваемые строки. Таким образом, сигнатура указывает, какие параметры будут дальше. Вызов метода StartUnit systemd принимает имя модуля для запуска в качестве первого параметра и режим, в котором он запускается, в качестве второго. Вызов возвращает значение пути к объекту. Он кодируется так же, как входной параметр: сигнатура (только o для пути к объекту), за которой следует фактическое значение.

Конечно, некоторые параметры вызова методов могут быть намного сложнее, но с busctl их все относительно легко закодировать. См. подробности на странице руководства.

busctl поддерживает ряд других операций. Например, вы можете использовать его для мониторинга трафика D-Bus по мере его возникновения (включая создание файла .cap для использования с Wireshark!) Или вы можете установить или получить определенные свойства. Тем не менее, этот пост должен быть о sd-bus, а не busctl, поэтому давайте кратко остановимся здесь и позвольте мне направить вас на страницу руководства на случай, если вы хотите узнать больше об этом инструменте.

busctl (как и остальная часть системы) реализован с использованием API sd-bus. Таким образом, он раскрывает многие особенности самой sd-bus. Например, вы можете использовать его для подключения к удаленным или контейнерным шинам. Он поддерживает как kdbus, так и классический D-Bus, и многое другое!

sd-bus

Но хватит! Вернемся к теме, поговорим о самой sd-bus.

Набор API sd-bus в основном содержится в заголовочном файле sd-bus.h.

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

  • Поддерживает как kdbus, так и dbus1 в качестве серверной части.

  • Имеет высокоуровневую поддержку подключения к удаленным шинам по ssh и шинам локальных контейнеров ОС.

  • Мощная модель учетных данных для реализации аутентификации клиентов в сервисах. В настоящее время поддерживаются 34 отдельных поля, от PID клиента до контрольной группы или наборов возможностей.

  • Поддержка отслеживания жизненного цикла узлов для автоматического освобождения локальных объектов, когда все узлы, ссылающиеся на них, отключены.

  • Клиент строит эффективное дерево решений, чтобы определить, каким обработчикам доставить входящее сообщение шины.

  • Автоматически переводит ошибки D-Bus в ошибки стиля UNIX и обратно (хотя с потерями), чтобы обеспечить лучшую интеграцию D-Bus в низкоуровневые программы Linux.

  • Мощная, но легкая объектная модель для отображения локальных объектов на шине. При необходимости автоматически генерирует самоанализ.

В настоящее время API полностью не документирован, но мы работаем над завершением набора справочных страниц. Подробности смотрите на всех страницах, начинающихся с sd_bus_.

Вызов метода из Си с помощью sd-bus

Так много о библиотеке в целом. Вот пример подключения к шине и выполнения вызова метода:

#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;
}

Сохраните этот пример как bus-client.c, а затем соберите его с помощью:

$ gcc bus-client.c -o bus-client `pkg-config --cflags --libs libsystemd`

Будет сгенерирован исполняемый файл bus-client, который вы теперь можете запустить. Обязательно запускайте его как root, поскольку доступ к методу StartUnit является привилегированным:

# ./bus-client
Queued service job as /org/freedesktop/systemd1/job/3586.

И это уже наш первый пример. Он показал, как мы вызывали метод на шине. Фактически вызов метода очень близок к инструменту командной строки busctl, который мы использовали ранее. Я надеюсь, что отрывок из кода не требует дополнительных пояснений. Он должен дать вам представление о том, как писать клиентов D-Bus с помощью sd-bus. Для получения дополнительной информации, пожалуйста, просмотрите заголовочный файл, страницу руководства или даже исходники sd-bus.

Реализация сервиса на Си с помощью sd-bus

Конечно, простой вызов одного метода - довольно упрощенный пример. Давайте посмотрим, как написать сервис шины. Мы напишем небольшой сервис калькулятора. Он предоставляет объект реализующий интерфейс, который предоставляет два метода: один для умножения двух 64-битных целых чисел со знаком, а другой для деления одного 64-битного целого числа со знаком на другое.

#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;
}

Сохраните этот пример как bus-service.c, а затем соберите его с помощью:

$ gcc bus-service.c -o bus-service `pkg-config --cflags --libs libsystemd`

А теперь запустим:

$ ./bus-service

В другом терминале попробуем с ним поговорить. Обратите внимание, что эта служба теперь находится на пользовательской шине, а не на системной шине, как раньше. Мы делаем это для простоты: доступ к сервисам на системной шине строго контролируется, поэтому непривилегированные клиенты не могут запрашивать привилегированные операции. Однако на пользовательской шине все проще: поскольку только процессы пользователя, владеющего шиной, могут подключиться, дальнейшее применение политики не усложнит этот пример. Поскольку служба находится на пользовательской шине, мы должны передать параметр --user в командной строке busctl. Начнем с просмотра дерева объектов сервиса.

$ busctl --user tree net.poettering.Calculator
L-/net/poettering/Calculator

Как мы видим, в сервисе есть только один объект, что неудивительно, учитывая, что наш код выше зарегистрировал только один. Давайте посмотрим интерфейсы и члены, которые предоставляет этот объект:

$ busctl --user introspect net.poettering.Calculator /net/poettering/Calculator
NAME                                TYPE      SIGNATURE RESULT/VALUE FLAGS
net.poettering.Calculator           interface -         -            -
.Divide                             method    xx        x            -
.Multiply                           method    xx        x            -
org.freedesktop.DBus.Introspectable interface -         -            -
.Introspect                         method    -         s            -
org.freedesktop.DBus.Peer           interface -         -            -
.GetMachineId                       method    -         s            -
.Ping                               method    -         -            -
org.freedesktop.DBus.Properties     interface -         -            -
.Get                                method    ss        v            -
.GetAll                             method    s         a{sv}        -
.Set                                method    ssv       -            -
.PropertiesChanged                  signal    sa{sv}as  -            -

Как упоминалось выше, библиотека sd-bus автоматически добавила пару универсальных интерфейсов. Но первый интерфейс, который мы видим, на самом деле тот, который мы добавили! Он показывает наши два метода, и оба принимают «xx» (два 64-битных целых числа со знаком) в качестве входных параметров и возвращают один «x». Отлично! Но правильно ли это работает?

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Multiply xx 5 7
x 35

Вау! Мы передали два целых числа 5 и 7, и служба фактически умножила их для нас и вернула одно целое число 35! Попробуем другой метод:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 99 17
x 5

Ух ты! Он может даже делать целочисленное деление! Фантастика! Но давайте обманем его делением на ноль:

$ busctl --user call net.poettering.Calculator /net/poettering/Calculator net.poettering.Calculator Divide xx 43 0
Sorry, can't allow division by zero.

Отлично! Он прекрасно это обнаружил и вернул чистую ошибку. Если вы посмотрите на приведенный выше пример исходного кода, вы увидите, насколько точно мы сгенерировали ошибку.

И это действительно всё, что у меня есть на сегодня. Конечно, примеры, которые я показал, короткие, и я не буду вдаваться в подробности того, что именно делает каждая строка. Однако этот пост должен быть кратким введением в D-Bus и sd-bus, и это уже слишком много для него...

Надеюсь, этот пост был вам полезен. Если вы заинтересованы в использовании sd-bus для своих собственных программ, я надеюсь, он поможет вам. Если у вас есть дополнительные вопросы, посмотрите (неполные) страницы руководства и спросите нас в IRC или в списке рассылки systemd. Если вам нужно больше примеров, взгляните на дерево исходных текстов systemd, все многочисленные шинные сервисы systemd широко используют sd-bus.