Введение
Для сложных 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, что нарушает саму идею изоляции и разграничения прав доступа, ради которой затевалось разделение на несколько процессов.