Обзор
systemd — это init-система и менеджер сервисов, работающий как PID 1 практически на всех major Linux-дистрибутивах. Разработан Lennart Poettering и Kay Sievers (Red Hat) с 2010 года, впервые включён по умолчанию в Fedora 15 (2011). systemd принципиально отличается от предшественника SysVinit: вместо последовательного выполнения shell-скриптов он использует декларативные unit-файлы с явным описанием зависимостей и параллельным запуском сервисов.
Масштаб systemd выходит далеко за рамки init: проект включает journald (логирование — см. статью 10), networkd/resolved (сеть — см. статью 3), logind (управление сессиями), tmpfiles, timedated, hostnamed и десятки других компонентов. Это основная причина критики: systemd нарушает принцип UNIX «do one thing well», концентрируя огромную функциональность вокруг PID 1. Однако именно эта интеграция обеспечивает согласованное поведение: cgroups-tracking, socket activation, структурированное логирование и декларативное управление ресурсами работают вместе, а не как разрозненные инструменты.
Архитектура
PID 1 и его роль
Ядро запускает systemd как первый userspace-процесс (PID 1). У PID 1 особая роль в UNIX: он становится приёмным родителем для всех осиротевших процессов и получает сигналы, не убивающие обычные процессы. systemd использует это: он отслеживает все дочерние процессы и через Linux cgroups гарантирует, что ни один процесс не «сбежит» от контроля (проблема double-fork, которая преследовала SysVinit).
PID 1 systemd выполняет три ключевые функции: загружает и парсит unit-файлы, строит граф зависимостей (DAG — направленный ациклический граф), и управляет транзакционной очередью jobs (запуск, остановка, перезапуск сервисов). Коммуникация с PID 1 идёт по протоколу D-Bus, но не обязательно через системную шину: systemctl разговаривает с менеджером по приватному сокету /run/systemd/private, поэтому управлять сервисами можно и тогда, когда dbus-daemon ещё не поднялся или упал, — в том числе чтобы починить саму шину.
Units: базовая абстракция
Всё, чем управляет systemd, — это unit. Тип unit определяется суффиксом файла:
.service — процесс или демон. Основной тип, с которым работают чаще всего. .timer — планировщик, аналог cron (подробнее ниже). .socket — сокет для socket activation: systemd создаёт сокет и передаёт его сервису при первом подключении. .mount и .automount — точки монтирования (генерируются из /etc/fstab или описываются вручную). .path — триггер на события файловой системы (inotify). .target — группа units, точка синхронизации (аналог runlevel). .device — устройство из udev. .slice — иерархия cgroups для управления ресурсами. .scope — группа внешне-созданных процессов (например, сессия пользователя).
Приоритет загрузки unit-файлов
Unit-файлы загружаются из нескольких директорий, приоритет от высшего к низшему:
/etc/systemd/system/ — администраторские override'ы и кастомные units. /run/systemd/system/ — units, созданные в runtime. /usr/lib/systemd/system/ — units, поставляемые пакетным менеджером. Transient units от systemd-run лежат не здесь, а в отдельном каталоге /run/systemd/transient/, приоритет которого выше /etc/systemd/system/.
Файл в /etc/ полностью перекрывает одноимённый из /usr/lib/. Для частичного переопределения используются drop-in директории: /etc/systemd/system/<unit>.d/*.conf — фрагменты, которые дополняют или переопределяют отдельные директивы оригинального unit-файла. Это каноничный способ кастомизации: не трогать файлы пакетного менеджера, а создавать drop-in override.
Зависимости: ordering vs requirement
systemd разделяет два ортогональных понятия, которые часто путают:
Requirement (что запускать) — директивы Wants= и Requires=. Wants=B.service — при активации текущего unit'а systemd попытается запустить B, но если B упадёт, текущий unit продолжит работать. Requires=B.service — жёсткая зависимость: если B не удастся запустить, текущий unit тоже будет помечен как failed.
Ordering (в каком порядке) — директивы After= и Before=. After=B.service — текущий unit запустится после B. Без After=/Before= units с Wants=/Requires= могут стартовать параллельно.
Критический нюанс: Wants= и Requires= не определяют порядок. Если написать Wants=network.target без After=network.target, сервис может запуститься одновременно с поднятием сети. Канонический паттерн для сетевых сервисов: After=network-online.target + Wants=network-online.target.
Есть и обратные зависимости: BindsTo= (как Requires, но unit останавливается если зависимость останавливается), PartOf= (unit перезапускается/останавливается вместе с зависимостью), Conflicts= (активация одного unit'а вызывает деактивацию другого).
Targets как точки синхронизации
Targets заменяют runlevels SysVinit. Они не выполняют никакой работы сами — это точки синхронизации, которые собирают зависимости:
multi-user.target — полностью загруженная система без GUI (аналог runlevel 3). graphical.target — включает multi-user.target + display manager (runlevel 5). rescue.target — однопользовательский режим (runlevel 1). emergency.target — минимальная оболочка, даже без монтирования ФС. reboot.target, poweroff.target — завершение работы.
default.target — симлинк на целевой target, обычно graphical.target или multi-user.target. Загрузка системы — это рекурсивная активация default.target и всех его зависимостей.
Анатомия service unit
Секции unit-файла
Каждый .service файл состоит из трёх секций:
[Unit]
Description=My Application Server
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.toml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
WatchdogSec=30s
# hardening (см. статью 7)
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/myapp
[Install]
WantedBy=multi-user.target
Type= и момент готовности
Выбор Type= определяет, когда systemd считает сервис «запущенным» и начинает стартовать зависимые units. Это один из самых важных и неочевидных параметров:
Type=simple (default) — systemd считает сервис запущенным немедленно после fork(). Процесс не должен уходить в background. Подходит для современных демонов, которые остаются на переднем плане. Проблема: systemd не знает, действительно ли сервис готов принимать запросы — зависимые units могут запуститься раньше, чем сервис инициализируется.
Type=exec — как simple, но systemd ждёт успешного exec() (а не просто fork()). Чуть надёжнее: если бинарник не найден, unit сразу переходит в failed.
Type=forking — для классических UNIX-демонов, которые делают fork() и завершают родительский процесс. Рекомендуется указывать PIDFile=, чтобы systemd мог отследить основной процесс. Унаследованная модель — для нового софта лучше использовать notify.
Type=notify — сервис явно сообщает systemd о готовности через sd_notify(3) (вызов sd_notify(0, "READY=1")). Самый надёжный вариант: зависимые units стартуют только когда сервис действительно готов. Требует поддержки в коде приложения.
Type=notify-reload (появился в systemd 253) — то же самое плюс протокол перезагрузки конфигурации: на systemctl reload менеджер шлёт главному процессу SIGHUP и ждёт RELOADING=1 с меткой MONOTONIC_USEC=, а затем READY=1. Это делает reload синхронным и избавляет от ExecReload=, который завершается раньше, чем сервис реально перечитал конфиг.
Type=oneshot — для скриптов, которые выполняются и завершаются. systemd ждёт завершения и только потом активирует зависимые units. С RemainAfterExit=yes unit остаётся в состоянии active после завершения процесса — полезно для настроечных скриптов и shutdown hooks. Единственный Type, допускающий несколько директив ExecStart= (выполняются последовательно).
Type=dbus — как simple, но готовность определяется по появлению заданного имени на D-Bus шине.
Рекомендация из актуальной systemd.service(5) звучит иначе, чем принято думать: для долгоживущих сервисов советуют exec, потому что он корректно отслеживает ошибки запуска — отсутствующий бинарник или несуществующего пользователя. Там, где момент готовности нужно определить точно, предпочтительны notify, notify-reload или dbus (последний — если сервис предоставляет интерфейс на шине). А forking в документации прямо помечен как discouraged: PID-файлы в новом софте рекомендуют не заводить вовсе. Любой тип кроме simple потенциально задерживает загрузку, потому что systemd ждёт готовности.
Restart и watchdog
Restart= контролирует автоперезапуск: on-failure (при ненулевом exit code, сигнале, timeout), always (при любом завершении), on-abnormal (только при сигнале или timeout, но не при чистом exit). RestartSec= задаёт паузу перед перезапуском (по умолчанию 100 мс) — важно для предотвращения restart-storm. С systemd 254 паузу можно наращивать экспоненциально: RestartSteps= задаёт число шагов от RestartSec= до RestartMaxDelaySec=, что заметно гуманнее фиксированного интервала для сервиса, чья зависимость лежит уже полчаса.
WatchdogSec= — сервис должен периодически вызывать sd_notify(0, "WATCHDOG=1"). Если вызова нет в течение указанного интервала, systemd считает сервис зависшим и применяет Restart-политику. Это мощный механизм для обнаружения deadlock'ов, недоступный в SysVinit.
Socket activation
Socket activation — одна из ключевых инноваций systemd. Идея: systemd создаёт сокет заранее, а сервис запускается только при первом подключении. Преимущества: параллелизация загрузки (сокеты создаются одновременно, зависимости разрешаются «ленивым» запуском), on-demand запуск (сервис не потребляет ресурсы, если к нему никто не обращается).
Пример: sshd.socket + sshd@.service — SSH-демон запускается только при подключении клиента. На сервере с редкими SSH-подключениями это экономит ресурсы.
# sshd.socket
[Socket]
ListenStream=22
Accept=yes
[Install]
WantedBy=sockets.target
Таймеры как замена cron
systemd timers — декларативная альтернатива cron с рядом преимуществ: интеграция с journald (логи таймера доступны через journalctl), Persistent= (выполнение пропущенных заданий), зависимости от других units, sandboxing через директивы безопасности в [Service].
Таймер — это пара файлов: .timer (расписание) и .service (что выполнять). По умолчанию timer ищет service с тем же именем.
Два типа таймеров: monotonic — относительно события (OnBootSec=, OnUnitActiveSec=, OnStartupSec=). Calendar — по расписанию (OnCalendar=), аналог cron expression.
Формат OnCalendar= — более читаемый чем cron:
OnCalendar=daily # каждый день в 00:00
OnCalendar=Mon..Fri *-*-* 09:00 # будни в 9:00
OnCalendar=*-*-* *:0/15 # каждые 15 минут
OnCalendar=*-01-01 00:00:00 # раз в год, 1 января
Валидация расписания перед применением:
systemd-analyze calendar "Mon..Fri *-*-* 09:00" --iterations=5
Полный пример таймера для бэкапа:
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Backup job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Nice=19
IOSchedulingClass=idle
RandomizedDelaySec= — случайная задержка, предотвращающая thundering herd если много таймеров с одинаковым OnCalendar. Persistent=true — если система была выключена во время запланированного запуска, таймер сработает при следующем включении. AccuracySec= (по умолчанию 1min) — точность срабатывания; systemd намеренно группирует таймеры в окна для экономии энергии (coalescing wake-ups). Для высокой точности — AccuracySec=1us.
Важный gotcha: oneshot сервис с RemainAfterExit=yes не подходит для повторяющихся таймеров — systemd не перезапустит уже active сервис. Для таймеров используйте oneshot без RemainAfterExit (default).
Практика
Основные операции systemctl:
# Жизненный цикл
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl reload nginx # HUP, без перезапуска процесса
systemctl reload-or-restart nginx
# Автозапуск при загрузке
systemctl enable nginx # создаёт симлинк в target.wants/
systemctl disable nginx
systemctl enable --now nginx # enable + start одной командой
# Статус и инспекция
systemctl status nginx # статус + последние строки лога
systemctl is-active nginx # exit code 0 если active
systemctl is-enabled nginx
systemctl show nginx # все свойства unit'а
systemctl cat nginx # показать unit-файл(ы)
# mask — жёсткое отключение (симлинк на /dev/null)
systemctl mask nginx # ни start, ни enable не сработают
systemctl unmask nginx
Различие enable/disable vs mask/unmask: disable снимает симлинк из target.wants (сервис не стартует при загрузке, но можно запустить вручную). mask создаёт симлинк на /dev/null — сервис не может быть запущен никак, пока не unmask.
Drop-in override без редактирования unit-файла пакетного менеджера:
# Создаёт /etc/systemd/system/nginx.service.d/override.conf
systemctl edit nginx
# В открывшемся редакторе можно переопределить отдельные директивы
# Пример: увеличить лимит открытых файлов
# [Service]
# LimitNOFILE=65536
systemctl daemon-reload # нужен после ручной правки файлов; systemctl edit делает reload сам
Диагностика загрузки:
systemd-analyze # общее время загрузки
systemd-analyze blame # время каждого unit'а
systemd-analyze critical-chain # цепочка критического пути
systemd-analyze plot > boot.svg # визуализация параллелизма
# Проверка unit-файла на ошибки
systemd-analyze verify /etc/systemd/system/myapp.service
# Просмотр дерева зависимостей
systemctl list-dependencies multi-user.target
systemctl list-dependencies --reverse nginx.service # кто зависит от nginx
Работа с таймерами:
systemctl list-timers --all # все таймеры и их следующее срабатывание
systemctl status backup.timer
journalctl -u backup.service # логи выполнений
Transient units — одноразовое выполнение с полным sandboxing без создания файлов:
# Запуск команды в одноразовом cgroup с лимитами
systemd-run --unit=my-task --scope -p MemoryMax=512M /usr/local/bin/heavy-job
# Запуск пользовательского таймера
systemd-run --user --on-calendar="*:0/30" /usr/local/bin/check-something
Нюансы и trade-off'ы
daemon-reload и его стоимость. systemctl daemon-reload пересериализует состояние всех units. На системе с тысячами units (крупные k8s-ноды, контейнерные хосты) это может занять секунды. Не стоит вызывать reload в tight loops — лучше batch'ить изменения.
Ordering vs readiness race. After=network.target не гарантирует, что сеть работает. network.target активируется когда начата настройка сети, а не когда она завершена. Для сервисов, которым действительно нужна работающая сеть, используйте After=network-online.target и убедитесь, что NetworkManager-wait-online.service или systemd-networkd-wait-online.service активен.
Потеря stdout/stderr. Если сервис пишет в stdout/stderr, journald собирает эти данные. Но если journald перегружен или unit имеет StandardOutput=null, логи теряются. Для критичных сервисов рассмотрите дополнительное логирование в файл через приложение.
ExecStartPre и порядок. Если ExecStartPre завершается с ошибкой (без префикса -), unit переходит в failed и ExecStart не выполняется. Префикс - перед командой игнорирует ненулевой exit code: ExecStartPre=-/usr/bin/cleanup-maybe. Аналогично для ExecStartPost.
cgroups и scope of control. systemd помещает каждый сервис в отдельный cgroup. Это значит: systemctl stop убивает все процессы сервиса, включая дочерние (через SIGTERM → SIGKILL). В SysVinit дочерние процессы могли «сбежать». Это же позволяет ставить лимиты ресурсов через unit-файл: MemoryMax=, CPUQuota=, TasksMax= — без ручной настройки cgroups. Выбора между иерархиями больше нет: в systemd 258 (сентябрь 2025) поддержка cgroup v1 — и legacy, и hybrid — удалена совсем, unified-иерархия монтируется всегда, а минимальная версия ядра поднята до 5.4. Подробнее о cgroups — в статье 6.
User units. systemd поддерживает пользовательские units (без root): файлы в ~/.config/systemd/user/, управление через systemctl --user. Пользовательский systemd-инстанс запускается при логине (или через loginctl enable-linger — без логина). Полезно для пользовательских демонов и таймеров.
Portability скриптов. Unit-файлы непортабельны за пределы systemd. Если нужна совместимость с несколькими init-системами, придётся поддерживать отдельные механизмы запуска (init-скрипт + unit-файл). На практике это актуально для софта, работающего на Alpine (OpenRC) или Void (runit). Обратный мостик, наоборот, разобран: до недавнего времени systemd подхватывал legacy-скрипты из /etc/init.d/ через systemd-sysv-generator, но в версии 260 (март 2026) генератор удалён вместе с systemd-sysv-install и rc-local.service, так что нативный unit-файл теперь обязателен.
Альтернативы systemd
OpenRC (Gentoo, Alpine, Artix) — dependency-based init, работает поверх SysVinit. Shell-скрипты вместо декларативных unit-файлов. Поддерживает параллельный запуск сервисов. Не привязан к Linux — работает на FreeBSD. Не берёт на себя логирование, сеть и прочее — каждый компонент отдельный. Trade-off: проще и модульнее, но нет cgroups-tracking, socket activation, watchdog, структурированного логирования.
runit (Void Linux, используется внутри Docker-контейнеров) — минималистичный process supervisor. Сервис = директория с исполняемым скриптом run. Если процесс умирает, runit немедленно перезапускает его. Три стадии загрузки: init, supervision, shutdown. Нет dependency management как такового — порядок определяется скриптами. Код PID 1 — 18KB (vs ~1.7MB у systemd). Чрезвычайно надёжен, но требует ручного управления зависимостями.
s6/s6-rc (проект Skarnet, используется в 66 init system) — модульная supervision suite с отдельным service manager (s6-rc). Акцент на correctness: гарантирует, что сервис не будет запущен до готовности зависимостей (в отличие от простых supervisor'ов). Поддержка readiness notification. Маленький codebase, пригоден для embedded. Trade-off: сложнее в освоении чем runit, меньше community.
На практике выбор init-системы определяется дистрибутивом. Переключение между ними — серьёзная операция, затрагивающая весь стек (инструменты мониторинга, deployment-скрипты, конфигурация).
Сравнение с FreeBSD
FreeBSD использует rc-систему: shell-скрипты в /etc/rc.d/ (base system) и /usr/local/etc/rc.d/ (порты), управляемые через /etc/rc.conf. Включение сервиса — одна строка: nginx_enable="YES" в rc.conf. Зависимости описываются через REQUIRE/BEFORE/KEYWORD в заголовке скрипта.
Ключевые различия:
Декларативность. systemd unit-файл — декларативное описание «что должно быть». rc-скрипт FreeBSD — императивная программа «как это сделать». Unit-файл проще парсить, валидировать, генерировать. rc-скрипт гибче: это полноценный shell, можно написать любую логику.
Supervision. systemd отслеживает процессы через cgroups и может перезапускать упавшие сервисы. rc-система FreeBSD — fire-and-forget: если демон упал, его нужно перезапустить вручную (или использовать daemon(8) с -r). Service jails в FreeBSD (появились сравнительно недавно) дают процессную изоляцию через один параметр в rc.conf, но это изоляция, а не supervision.
Логирование. systemd захватывает stdout/stderr сервисов в journald автоматически. В FreeBSD логирование — ответственность самого демона (через syslog или файлы). Это проще в плане архитектуры, но неудобнее для диагностики.
Таймеры. В FreeBSD нет встроенного аналога systemd timers — используется cron (с periodic(8) как обёрткой для системных задач). cron проще, но не имеет dependency management, persistent catch-up, и sandboxing.
Единая конфигурация. FreeBSD rc.conf — один файл для всей системы: hostname, сеть, сервисы, firewall. Это удобно для обзора, но не масштабируется: на сложной системе rc.conf превращается в длинный плоский список. systemd распределяет конфигурацию по отдельным unit-файлам — сложнее для обзора, но лучше модуляризировано.
Service jails в FreeBSD заслуживают отдельного упоминания: они позволяют изолировать сервис в jail одной строкой в rc.conf (<service>_svcj="YES"). Это аналог sandbox-директив systemd (ProtectSystem=, PrivateTmp= и т.д.), но реализованный через jail-механизм ядра FreeBSD — потенциально сильнее изоляция, но с меньшей гранулярностью настроек.
Источники
- systemd.service(5) — полное описание service units
- systemd.timer(5) — описание timer units
- systemd.unit(5) — общие директивы для всех типов units
- systemd releases и NEWS — первоисточник по удалениям и новым директивам: cgroup v1 в 258, SysV-скрипты в 260
- systemd — ArchWiki — обширное практическое руководство
- systemd/Timers — ArchWiki — таймеры как замена cron
- Lennart Poettering, "Rethinking PID 1" — оригинальная мотивация дизайна systemd
- EWONTFIX: Systemd has 6 service startup notification types — критический анализ Type= от Rich Felker (автор musl)
- Red Hat: Working with systemd unit files — enterprise-ориентированная документация
- s6-rc: why? — мотивация альтернативного подхода к service management
- Comparison of init systems — Gentoo Wiki — сравнительная таблица init-систем
- FreeBSD Handbook: rc(8) — для сравнения
- Klara Systems: rc(8) FreeBSD Services and Automation — обзор rc-системы FreeBSD
Связанные заметки
- Linux — файловая иерархия и FHS — где лежат unit-файлы и runtime-данные
- Linux — обновление и управление загрузкой — systemd-boot и порядок инициализации
- Linux — контейнеризация — cgroups как общий фундамент systemd и контейнеров
- FreeBSD — rc-система — init через shell-скрипты, для сравнения
- Linux — сводный cheatsheet — быстрая справка по systemctl и journalctl
- Домашний десктоп на CachyOS — пример пользовательских скоупов: uwsm запускает приложения сессии
- Linux — пользователи, группы, PAM и systemd-logind — logind, сессии, linger и пользовательский экземпляр менеджера
- Резервное копирование: стратегия и проверка восстановления — таймеры и
LoadCredentialEncrypted=на практике: ночное задание с секретом - Linux — время и синхронизация — почему
OnCalendar=в локальном поясе ломается при переходе на летнее время - Наблюдаемость: Prometheus, node_exporter, Grafana, Loki — юниты экспортеров и директивы изоляции на практике
- Эксплуатация локальных ИИ-сервисов — юнит для сервиса инференса: почему готовность не выражается через
.deviceиType=notify - Загрузка: от питания до первой инструкции ядра — как ядро вообще запускает первый процесс и что происходит до передачи управления init
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.