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

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

Как устроена точка входа в интерфейс менеджера служб

Вся работа начинается с одного объекта, главной точки входа. У менеджера служб есть фиксированное имя на шине и фиксированный путь к объекту менеджера, по которым к нему обращаются. На этом объекте живёт интерфейс менеджера, собравший методы управления службами. Имена методов говорят сами за себя: запустить юнит, остановить, перезапустить, перезагрузить конфигурацию, мягко перезапустить или перезагрузить в зависимости от состояния. Каждый из них принимает понятные аргументы и возвращает структурированный ответ.

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

busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager StartUnit ss "cups.service" "replace"
# ss это сигнатура: два аргумента-строки, имя юнита и режим

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

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

Что означает режим вызова и зачем он нужен

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

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

Чем sd-bus удобнее обращения к шине вручную

Для работы с D-Bus из программ на C systemd предлагает собственную библиотеку sd-bus, пришедшую на смену более громоздким старым библиотекам шины. Она берёт на себя установку соединения с шиной, кодирование аргументов по сигнатуре, отправку вызова и разбор ответа. Программе остаётся открыть соединение с системной шиной, сформировать вызов метода и прочитать результат. В общих чертах запуск службы из кода на C выглядит так:

#include <systemd/sd-bus.h>

sd_bus *bus = NULL;
sd_bus_default_system(&bus);          /* подключаемся к системной шине */

sd_bus_message *reply = NULL;
sd_bus_error err = SD_BUS_ERROR_NULL;

sd_bus_call_method(bus,
    "org.freedesktop.systemd1",                 /* имя службы на шине */
    "/org/freedesktop/systemd1",                /* путь к объекту менеджера */
    "org.freedesktop.systemd1.Manager",         /* интерфейс */
    "RestartUnit",                              /* метод */
    &err, &reply, "ss", "nginx.service", "replace");

const char *job_path = NULL;
sd_bus_message_read(reply, "o", &job_path);     /* читаем путь задания */

Преимущество перед запуском команды оболочки очевидно. Нет порождения лишнего процесса, нет хрупкого разбора текста, есть строгая проверка типов и внятные коды ошибок в структуре ошибки шины. Если служба не нашлась или вызов запрещён правами, программа получит конкретную машиночитаемую ошибку, а не строку, которую пришлось бы разбирать. Освобождение ответа, ошибки и соединения обязательно, иначе утечёт память, и аккуратная программа закрывает все эти ресурсы после использования.

Есть и соображение производительности, незаметное на единичном вызове, но важное при частых обращениях. Запуск команды оболочки означает каждый раз порождение нового процесса, его загрузку, подключение к шине, выполнение и завершение, а это десятки миллисекунд накладных расходов на пустом месте. Прямое же обращение через sd-bus держит одно соединение с шиной открытым и гонит по нему вызов за вызовом без всякого порождения процессов. Для инструмента, опрашивающего или дёргающего службы часто, разница в нагрузке и отзывчивости становится решающей. Вдобавок открытое соединение можно встроить в общий событийный цикл программы, обрабатывая ответы и сигналы шины наравне с прочими источниками событий, чего с разовыми запусками команд не сделать в принципе.

Как следить за состоянием служб и ловить изменения

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

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

Эта связка вызовов и сигналов позволяет строить полноценные надстройки над менеджером служб: панели мониторинга, автоматические супервизоры, инструменты развёртывания, которые запускают службы и дожидаются их готовности, не прибегая к разбору текстового вывода и к холостому опросу.

Отдельная мощная возможность это запуск временных служб прямо из программы. Вместо того чтобы заранее писать файл описания юнита на диск, программа может попросить менеджер создать эфемерный юнит на лету, передав его свойства прямо в вызове. Так запускают разовые задачи под надзором менеджера: процесс получает все блага управляемой службы, попадает в свою контрольную группу, подчиняется лимитам ресурсов и аккуратно сворачивается по тем же правилам, но при этом не оставляет за собой никаких файлов и исчезает бесследно после завершения. Это удобно для фоновых заданий, разовых обработчиков и любой работы, которую хочется отдать под присмотр менеджера, не засоряя систему постоянными описаниями юнитов. Через тот же интерфейс задаются и ограничения ресурсов будущего юнита, что роднит этот приём с прямым управлением контрольными группами, только руками менеджера служб.

Какие права и ограничения сопровождают доступ к шине

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

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

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