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
193.2 Ошибку в файле видно до запуска
Две опечатки, каждая из которых на приборе стоила бы перезагрузки:
| Что написали | Что скажет systemd-analyze verify |
|---|---|
Restart=всегда |
Failed to parse service restart specifier |
WatchdogSec=30x |
Failed to parse sec value |
⚠️ Код возврата
verifyне всегда отражает ошибку разбора. В опыте обе опечатки напечатаны в вывод, а код возврата остался нулевым. Поэтому в сборке проверяют вывод команды, а не только её код: пустой вывод — файл чист.
193.3 Сторожевой таймер службы
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 Итоги
- Служба прибора — один текстовый файл:
Afterзадаёт порядок,WantsиBindsTo— зависимости,ExecStart— что запускать,WantedBy=multi-user.target— автозапуск. - Проверять файл нужно
systemd-analyze verify: в опыте правильный файл принят без замечаний,Restart=всегдадалFailed to parse service restart specifier,WatchdogSec=30x—Failed to parse sec value. Код возврата при этом нулевой — в сборке смотрят вывод. WatchdogSec=30— тот же сторож, чтоIWDG, только снаружи: отмечатьсяsd_notify(0, "WATCHDOG=1")вдвое чаще срока и из рабочего цикла. За ядром и самимsystemdсмотрит аппаратный сторож:RuntimeWatchdogSec=вsystem.conf.- Вывод службы идёт в журнал; уровень строки задаётся префиксом
<3>, размер ограничиваетсяSystemMaxUse.stdoutслужбы — с построчным буфером (setvbuf), иначе строки перед падением теряются. - Права дают группой и правилом
udev(MODE="0660"), а не запуском отroot;NoNewPrivileges,ProtectSystem,ProtectHome,PrivateTmpограничивают службу четырьмя строками. - Правки вносят добавкой в
device.service.d/, а не правкой файла из образа.
Дальше: раздел 194 «Связь с микроконтроллером: порт со стороны Linux, кадры COBS и CRC».