Embedded

Multi-process архитектура на embedded: разделение UI и логики устройства

Введение

Для сложных embedded-продуктов (промышленное оборудование, автомобильные системы) часто полезно разделить Qt-приложение на несколько отдельных процессов — например, процесс, управляющий критичной логикой устройства (взаимодействие с датчиками, безопасность), и отдельный процесс, отвечающий только за отображение пользовательского интерфейса. В заключительной статье цикла разберём, зачем нужно такое разделение и как организовать взаимодействие между процессами.

Концепция

Главные причины разделения на несколько процессов — изоляция отказов (краш интерфейса не должен останавливать критичную логику управления оборудованием, и наоборот) и разграничение прав доступа (процесс, работающий с интерфейсом, не должен требовать повышенных прав для прямого доступа к аппаратным интерфейсам, которые нужны только логике устройства). Взаимодействие между процессами на embedded Linux обычно реализуется через D-Bus (системную межпроцессную шину сообщений, хорошо интегрированную с Qt через модуль QtDBus) или через более простой механизм локальных сокетов (QLocalSocket/QLocalServer) для менее формализованного, но более легковесного обмена сообщениями.

Пример кода: Multi-process архитектура на embedded

// devicecontrolservice.h — отдельный процесс, управляющий аппаратной логикой,
// экспортирующий свой функционал через D-Bus
#pragma once
#include <QObject>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusAbstractAdaptor>

class DeviceControlService : public QObject
{
    Q_OBJECT
public:
    explicit DeviceControlService(QObject *parent = nullptr) : QObject(parent) {}

public slots:
    // Q_NOREPLY и обычные слоты автоматически становятся доступны через D-Bus адаптер
    double readSensorValue(const QString &sensorName)
    {
        // здесь реальное чтение значения с физического датчика через системные интерфейсы
        return 23.5;
    }

    bool setActuatorState(const QString &actuatorName, bool enabled)
    {
        qDebug() << "Установка состояния актуатора" << actuatorName << "в" << enabled;
        return true;
    }

signals:
    void sensorAlarmTriggered(const QString &sensorName, double value);
};
// main_device_service.cpp — точка входа отдельного процесса логики устройства
#include <QCoreApplication>
#include <QtDBus/QDBusConnection>
#include "devicecontrolservice.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    DeviceControlService service;

    // Регистрация объекта на системной шине D-Bus под конкретным именем сервиса
    QDBusConnection::systemBus().registerService("com.mycompany.DeviceControl");
    QDBusConnection::systemBus().registerObject("/DeviceControl", &service,
        QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals);

    return app.exec();
}
// main_ui.cpp — отдельный процесс интерфейса, обращающийся к сервису через D-Bus
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusReply>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // Интерфейсный процесс работает без повышенных прав доступа,
    // вся работа с аппаратными ресурсами происходит в отдельном, привилегированном процессе
    QDBusInterface deviceControlInterface(
        "com.mycompany.DeviceControl", "/DeviceControl",
        "", QDBusConnection::systemBus());

    QDBusReply<double> reply = deviceControlInterface.call("readSensorValue", "temperature_main");
    if (reply.isValid()) {
        qDebug() << "Получено значение датчика через D-Bus:" << reply.value();
    }

    QQmlApplicationEngine engine;
    engine.loadFromModule("EmbeddedDashboard", "Main");

    return app.exec();
}

Пояснения к коду

DeviceControlService экспортирует свой функционал на системную шину D-Bus через QDBusConnection::registerObject() с флагами ExportAllSlots/ExportAllSignals — это автоматически делает все публичные слоты (readSensorValue, setActuatorState) вызываемыми удалённо из других процессов, а сигналы (sensorAlarmTriggered) — доступными для подписки из других процессов, без необходимости вручную писать код сериализации/десериализации сообщений межпроцессного взаимодействия.

Процесс интерфейса (main_ui.cpp) обращается к сервису устройства через QDBusInterface, вызывая метод readSensorValue так, как если бы это был обычный, локальный вызов — QDBusReply инкапсулирует асинхронный по своей природе D-Bus вызов в удобный, синхронный (в данном примере) интерфейс для простоты. Разделение на два независимых процесса с собственными main()-функциями означает, что они компилируются как два отдельных исполняемых файла, запускаемых независимо (как правило, через отдельные systemd-юниты), и крах одного процесса (например, интерфейса) не приводит к остановке другого (логики управления устройством).

Подводные камни

  • Дополнительная сложность разработки и отладки multi-process архитектуры по сравнению с единым процессом. Распределённая отладка двух (или более) взаимодействующих процессов значительно сложнее отладки одного — для embedded-команд без предыдущего опыта в подобной архитектуре стоит явно оценить, оправдывает ли реальная потребность в изоляции отказов эту дополнительную сложность, прежде чем разделять приложение на несколько процессов «на всякий случай».
  • Задержка межпроцессного взаимодействия через D-Bus заметно выше прямого вызова функции в рамках одного процесса. Для задач, требующих очень быстрого, частого обмена данными (например, обновление показаний датчика 100 раз в секунду для плавной анимации на графике), накладные расходы D-Bus могут стать заметным узким местом — для таких высокочастотных сценариев иногда практичнее использовать более низкоуровневый, легковесный механизм (общая память, локальные сокеты с бинарным протоколом) вместо D-Bus.
  • Необходимость явного управления порядком запуска и временем жизни нескольких взаимозависимых процессов — если процесс интерфейса запускается раньше, чем процесс логики устройства успел зарегистрировать своё имя сервиса на D-Bus, первые вызовы QDBusInterface будут неудачными; нужна явная логика устойчивости к временной недоступности сервиса (повторные попытки подключения) или правильно настроенные зависимости запуска systemd-юнитов (After=, Requires= в конфигурации сервиса).
  • Дублирование прав доступа из-за неаккуратной настройки D-Bus политик безопасности. Системная шина D-Bus по умолчанию может иметь достаточно открытую политику доступа — без явной настройки D-Bus policy-файлов, ограничивающих, какие именно процессы/пользователи могут вызывать методы конкретного сервиса, любой процесс в системе теоретически может попытаться вызвать setActuatorState напрямую через D-Bus, что нарушает саму идею изоляции и разграничения прав доступа, ради которой затевалось разделение на несколько процессов.