Embedded HandbookПриложения
Все разделы › XXIV. Встраиваемый Linux
· уровень средний · 9 мин чтения
Открыть в интерактивном справочнике

193. Служба прибора: systemd, журнал, права и сторожевой таймер

Программа на микроконтроллере запускается сама: вы её прошили — она работает с первого такта. На приборе с Linux программу кто-то должен запустить, перезапустить после падения, увести в журнал её вывод и не дать ей лишних прав. Этим занимается systemd, и описывается всё одним текстовым файлом.

Опыты — в образе stm-linux:1: файл службы разбирает сам systemd (версия 252) командой systemd-analyze verify; сверяет _verify/linux191_check.py.

Как это называется в документации

Оригинал Перевод
unit, service unit единица настройки, служба
systemd target цель: набор служб, состояние системы
notify protocol, sd_notify уведомление службы менеджеру о готовности
watchdog сторожевой таймер: служба обязана отмечаться
journal журнал: единое место для вывода всех служб
udev rule правило назначения прав и имён устройствам
hardening ограничение прав службы средствами ядра
drop-in добавка к файлу службы, не трогающая оригинал

193.1 Файл службы целиком

Файл службы, который поставляет образ, кладут в /usr/lib/systemd/system/device.service, а /etc/systemd/system/ оставляют для правок на приборе — полных замен и добавок device.service.d/ (193.6). Три раздела: кто мы, что запускать, когда включаться.

[Unit]
Description=Прибор: сбор телеметрии и связь с микроконтроллером
After=network-online.target dev-ttyS0.device
BindsTo=dev-ttyS0.device
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/bin/device --port /dev/ttyS0 --config /etc/device.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=2
WatchdogSec=30
TimeoutStopSec=10
User=device
SupplementaryGroups=dialout gpio i2c spi
StateDirectory=device
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
Строка Что означает на приборе
BindsTo= и After= … dev-ttyS0.device ждать появления последовательного порта к микроконтроллеру и остановиться, если он пропал; один After= без зависимости ничего не ждёт
Wants=network-online.target сеть желательна, но её отсутствие не отменяет запуск
Type=notify служба сама скажет «я готова» — до этого зависимые не стартуют
Restart=always, RestartSec=2 упала или вышла — поднять через две секунды
WatchdogSec=30 не отметилась 30 секунд — считать зависшей и перезапустить
TimeoutStopSec=10 не завершилась за 10 секунд по SIGTERM — добить SIGKILL
User=device работает не от root
SupplementaryGroups= но получает доступ к /dev/ttyS0, gpiochip, i2c, spidev (раздел 192)
StateDirectory=device единственный каталог, куда служба пишет при ProtectSystem=strict, — /var/lib/device (193.5)
WantedBy=multi-user.target включается при обычной загрузке — это и есть автозапуск

Проверять файл нужно не запуском, а разбором: systemd-analyze verify читает его теми же правилами, что и менеджер служб. В опыте файл выше принят без единого замечания.

Но пользователей и группы verify не проверяет. В образе опыта не было ни пользователя device, ни групп gpio, i2c и spi — а замечаний ноль. Запуск той же службы в том же образе под настоящим systemd кончился кодом 217/USER, а с пользователем, но без групп — 216/GROUP. В чистом Debian этих трёх групп нет, их заводят вместе с пользователем:

useradd --system device
groupadd --system gpio
groupadd --system i2c
groupadd --system spi

Строки файла службы и что каждая значит на приборе: After ждёт последовательный порт, Wants сеть без обязательства, Type=notify — служба сама сообщает о готовности, Restart=always с RestartSec 2 поднимает после падения, WatchdogSec 30 ловит зависание, TimeoutStopSec 10 добивает SIGKILL, User=device с группами dialout, gpio, i2c, spi, WantedBy=multi-user.target включает автозапуск


193.2 Ошибку в файле видно до запуска

Две опечатки, каждая из которых на приборе стоила бы перезагрузки:

Что написали Что скажет systemd-analyze verify
Restart=всегда Failed to parse service restart specifier
WatchdogSec=30x Failed to parse sec value

⚠️ Код возврата verify не всегда отражает ошибку разбора. В опыте обе опечатки напечатаны в вывод, а код возврата остался нулевым. Поэтому в сборке проверяют вывод команды, а не только её код: пустой вывод — файл чист.


193.3 Сторожевой таймер службы

Жизненный цикл службы: systemd запускает, служба отмечается sd_notify, тишина 30 секунд — перезапуск

WatchdogSec=30 включает то же самое, что IWDG на микроконтроллере (раздел 12.2), только сторожем работает systemd. Служба обязана периодически отмечаться:

#include <systemd/sd-daemon.h>

sd_notify(0, "READY=1");                 /* для Type=notify: «я поднялась» */
for (;;) {
    do_work();
    sd_notify(0, "WATCHDOG=1");          /* отметка сторожу */
}

Отмечаться принято вдвое чаще срока: при WatchdogSec=30 — раз в 15 секунд. Тогда одна пропущенная отметка не убивает работающий прибор, а зависание всё равно замечено. Отметку делают в той же петле, что и полезную работу: отдельный поток-таймер будет исправно отмечаться и у повисшей службы — ровно та же ошибка, что «кормить IWDG из прерывания таймера» (раздел 12.7).

Перезапуск после зависания настраивают тут же: Restart=always поднимает службу и после сторожевого убийства. Если прибор обязан перезагрузиться целиком — добавляют StartLimitAction=reboot и предел попыток.

Но WatchdogSec следит за службой, а не за системой: зависнет ядро или сам systemd — перезапускать будет некому. Прямой аналог IWDG — аппаратный сторож процессора /dev/watchdog, и его systemd кормит сам, если в разделе [Manager] файла /etc/systemd/system.conf задать RuntimeWatchdogSec=30s. Зависло ядро — плата перезагружается. На приборе ставят оба уровня: железо смотрит за systemd, systemd — за службой.


193.4 Журнал вместо файла с логами

Всё, что служба печатает в stdout и stderr, забирает журнал. Свой файл заводить не нужно, и это выигрыш: на встраиваемой машине переполнение раздела логами — обычная причина отказа.

journalctl -u device -f            # смотреть в реальном времени
journalctl -u device -b            # только с этой загрузки
journalctl -u device -p err        # только ошибки
journalctl --disk-usage            # сколько занято журналом

Уровень строки задаётся префиксом: <3> — ошибка, <6> — сообщение, <7> — отладка. Печать printf("<3>нет ответа от контроллера\n") попадёт в журнал как ошибка, и journalctl -p err её покажет.

Одна тонкость: вывод службы идёт не в терминал, а в поток к журналу, и printf копит строки в буфере — в опыте 4096 байт. Строки приходят в журнал пачками, а при гибели службы пропадают: служба с двумя printf и abort() не донесла до журнала ни одной, с построчным буфером — обе. Так же пропали обе строки у зависшей службы, которую добил сторож: systemd убивает её тем же SIGABRT, и теряются как раз последние строки перед зависанием. Поэтому в начале main ставят

setvbuf(stdout, NULL, _IOLBF, 0);        /* сбрасывать буфер на каждой строке */

или вызывают fflush(stdout) после важных строк; stderr не буферизуется вовсе.

Размер журнала ограничивают в /etc/systemd/journald.conf:

[Journal]
# держать между перезагрузками
Storage=persistent
# больше не занимать
SystemMaxUse=64M
RuntimeMaxUse=16M

Комментарий в этих файлах — только отдельной строкой, с # или ; в начале. Хвост «# …» после значения становится частью значения. В опыте journald на такой файл ответил строками Failed to parse storage setting, ignoring и Failed to parse size value, ignoring и оставил значения по умолчанию: хранение auto вместо persistent и свой предел размера вместо 64 МБ.


193.5 Права: не от root

Служба из примера работает от пользователя device. Чтобы она при этом видела шины, нужны две вещи: группы в файле службы и правило udev, которое даёт группе доступ к символьным устройствам (раздел 192).

SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660"
SUBSYSTEM=="i2c-dev", GROUP="i2c", MODE="0660"
SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"

Три правила, права 0660: владелец и группа читают и пишут, остальные — нет. Файл кладут в /etc/udev/rules.d/99-device.rules.

Четыре строки ограничений в файле службы стоят того, чтобы их понимать:

Строка Что запрещает
NoNewPrivileges=yes получить права выше своих — даже через setuid-программу
ProtectSystem=strict писать куда-либо, кроме явно разрешённого
ProtectHome=yes видеть домашние каталоги
PrivateTmp=yes видеть чужой /tmp и оставлять там следы

Состояние (настройки, накопленные данные) пишут не «куда-нибудь», а в каталог, который заводит сам systemd: StateDirectory=device даёт /var/lib/device с нужным владельцем.


193.6 Обновление прибора

Файл службы не правят на месте — добавляют «добавку» (drop-in):

mkdir -p /etc/systemd/system/device.service.d
printf '[Service]\nEnvironment=DEVICE_LOG=debug\n' > /etc/systemd/system/device.service.d/log.conf
systemctl daemon-reload
systemctl restart device

Так обновление образа не затирает ваши правки, а systemctl cat device показывает и оригинал, и все добавки.

Обычный порядок обновления программы прибора: положить новый файл рядом, systemctl restart device, проверить systemctl is-active device и журнал; не поднялась — вернуть прежний файл. Обновление всего образа целиком (два раздела, переключение активного) — другая история, но и там служба описывается так же.


193.7 Грабли

Служба стартует раньше железа. After= только упорядочивает запуск и сам ничего не ждёт: в опыте служба с одним After=dev-ttyS9.device запустилась без порта, а с BindsTo= — встала ждать устройство. Для порта нужна пара BindsTo=dev-ttyS0.device и After=, для сети — Wants= и After=network-online.target (и сама цель должна быть включена).

Type=simple вместо notify. Менеджер считает службу готовой сразу после запуска процесса, и зависимые стартуют раньше времени.

Отметки сторожу из отдельного потока. Служба висит, а отметки идут: сторож бесполезен. Отмечаться — из рабочего цикла.

Restart=always при ошибке настройки. Служба падает мгновенно и перезапускается по кругу. Лечат пределом: StartLimitBurst и StartLimitIntervalSec, иначе прибор занят только перезапусками.

Логи в свой файл. Раздел кончится, и прибор встанет. Журнал с SystemMaxUse не переполнит диск.

Правку внесли в /etc/systemd/system, а служба не изменилась. Нужен systemctl daemon-reload: менеджер держит разобранные файлы в памяти.

Запуск от root «чтобы работало». Настоящая причина — права на /dev/...; лечится правилом udev и группами, а не правами суперпользователя.


193.8 Итоги

Дальше: раздел 194 «Связь с микроконтроллером: порт со стороны Linux, кадры COBS и CRC».

Открыть в интерактивном справочнике