systemd — PID 1, units, targets, journalctl
В уроке 13.1 мы видели, что после switch_root kernel запускает PID 1. На современных Linux (Ubuntu, Debian, RHEL, Fedora, Arch с 2015+) — это systemd. Это не просто init, а целая платформа управления системой: запуск сервисов, dependency-graph, mount-юниты, timers (cron-replacement), journal logs, network management. Понимание systemd — обязательное для всех админов и DE.
В этом уроке: что такое unit (.service, .target, .timer, .mount, .socket), как они зависят друг от друга, что такое target (заменяет old runlevels), как читать journalctl для дебага, и базовые команды systemctl.
systemd как PID 1: 4 главные функции
systemd — огромный проект (~2 миллиона строк кода), который заменил много отдельных утилит: SysV init, cron, syslog, automount, inetd, и др. Это вызывает споры в Linux-сообществе (“monolithic, too much”), но de-facto стандартизация позволила админам учить один tool вместо десятка.
Units: основной концепт
В systemd всё — unit. Это файл с метаданными о сервисе/mount/timer/socket/target. Файлы в нескольких местах:
/usr/lib/systemd/system/ # ставится пакетами (apt, dnf)
/etc/systemd/system/ # admin custom (приоритет выше /usr/lib)
~/.config/systemd/user/ # user-units (нет root)
/run/systemd/system/ # runtime-сгенерированные (после reboot исчезают)
Типы юнитов (расширение определяет):
.service # сервисы (nginx.service, postgresql.service)
.target # группы units, аналог runlevels (multi-user.target)
.socket # socket activation (start service когда socket-connection пришёл)
.mount # mount filesystem (генерируется автоматически из /etc/fstab)
.timer # periodic execution (cron-replacement)
.path # watch filesystem path (start service когда файл изменился)
.swap # swap area
.device # device units (создаются udev'ом)
.slice # cgroup hierarchies для resource limits
.scope # external processes managed by systemd
Список загруженных юнитов:
$ systemctl list-units | head
UNIT LOAD ACTIVE SUB DESCRIPTION
proc-sys-fs-binfmt_misc.automount loaded active running ...
sys-devices-pci0000:00-...service loaded active running ...
boot.mount loaded active mounted ...
postgresql.service loaded active running PostgreSQL
nginx.service loaded active running nginx
sshd.service loaded active running OpenSSH
multi-user.target loaded active active Multi-User System
ACTIVE — состояние unit: active/inactive/failed. SUB — более точное состояние (running, exited, dead, listening).
.service unit — основа основ
Простой пример service-unit для Python-скрипта:
[Unit]
Description=My ETL Script
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=etl
Group=etl
WorkingDirectory=/opt/etl
ExecStart=/opt/etl/venv/bin/python /opt/etl/main.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=/opt/etl/secrets.env
# Security
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
# Resource limits
MemoryMax=2G
CPUQuota=200%
[Install]
WantedBy=multi-user.target
Три секции:
- [Unit] — метаданные и зависимости.
- [Service] — что и как запускать (для .service).
- [Install] — куда подключать при
enable.
Ключевые поля:
Description— человеческое описание.After=— запустить после (но не required).Wants=/Requires=— запустить также эти units (Requires строже — если они failed, наш тоже).ExecStart=— АБСОЛЮТНЫЙ путь к команде (PATH не наследуется).User=/Group=— от какого юзера (НЕ root для production).Restart=on-failure— автоперезапуск при exit-code != 0.RestartSec=10s— задержка перед перезапуском.MemoryMax=,CPUQuota=— через cgroups, защита от runaway процесса.WantedBy=multi-user.target— при enable сделать symlink в multi-user.target.wants/.
После создания файла:
# Создаём в /etc/systemd/system/etl.service
sudo systemctl daemon-reload # перечитать unit-файлы
sudo systemctl start etl.service # запустить сейчас
sudo systemctl enable etl.service # автозапуск при boot
sudo systemctl status etl.service # статус
После любой правки unit-файла НУЖЕН sudo systemctl daemon-reload. systemd кэширует юниты в памяти, правка на диске не подхватывается. После daemon-reload restart применит изменения. systemd при restart показывает ‘Warning: unit changed on disk’ — это сигнал что забыли daemon-reload.
Targets — что заменили runlevels
В классическом SysV были runlevels — 0 (halt), 1 (single user), 3 (multi-user no GUI), 5 (multi-user GUI), 6 (reboot). Переключались через init 5.
systemd использует targets — более гибкие группировки. Каждый target — unit-файл, который через Requires= / Wants= подтягивает другие units.
Стандартные targets:
poweroff.target # выключение (= runlevel 0)
rescue.target # rescue (= runlevel 1, single user)
multi-user.target # обычный server, без GUI (= runlevel 3)
graphical.target # GUI desktop (= runlevel 5)
reboot.target # перезагрузка (= runlevel 6)
# Кроме того:
emergency.target # ещё минимальнее rescue (без mount других ФС)
default.target # symlink на multi-user или graphical
Команды:
# Текущий default-target (загружается по умолчанию):
$ systemctl get-default
multi-user.target
# Сменить (например, после установки GUI):
sudo systemctl set-default graphical.target
# Переключиться сейчас (без reboot):
sudo systemctl isolate rescue.target # выгоняет всех в rescue
sudo systemctl isolate multi-user.target # обратно
# Что входит в target:
systemctl list-dependencies multi-user.target
В normal flow boot: systemd запускает default.target, который через зависимости подтягивает все нужные сервисы.
journalctl — просмотр логов
systemd-journald собирает stdout/stderr всех сервисов, kernel-сообщения, syslog — в единый бинарный journal (/var/log/journal/). journalctl — интерфейс query.
Базовые команды:
# Все логи с начала journal:
journalctl
# Live tail (как tail -f):
journalctl -f
# Только текущая загрузка:
journalctl -b
# Предыдущий boot:
journalctl -b -1
# Конкретный сервис:
journalctl -u nginx.service
# Сервис + live:
journalctl -u nginx.service -f
# С момента времени:
journalctl --since "10 minutes ago"
journalctl --since "2026-05-18 09:00"
journalctl --since today --until "1 hour ago"
# Только kernel:
journalctl -k
journalctl -k -b -1 # kernel предыдущей загрузки
# По приоритету (level):
journalctl -p err -b # только errors с текущего boot
journalctl -p warning # warnings и выше
# Конкретный PID:
journalctl _PID=1234
# По UID:
journalctl _UID=33
# Полный output (без обрезания строк):
journalctl --no-pager
# Реверс (новейшие первыми):
journalctl -r | head -20
# Только сегодня, по сервису, последние 50 строк:
journalctl -u sshd --since today -n 50
journalctl имеет встроенный pager (less). Поэтому можно листать стрелками, искать через /, и т.д. Для скриптов — --no-pager.
Persistent journal
По умолчанию на некоторых дистрибутивах journal — volatile (только RAM, /run/log/journal/). После reboot всё стирается. Для persistent:
# Создать директорию (systemd сам начнёт писать):
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo killall -USR1 systemd-journald
# Проверить:
journalctl --disk-usage
# Archived and active journals take up 230.4M in the file system.
Конфиг — /etc/systemd/journald.conf:
[Journal]
Storage=persistent # auto | persistent | volatile | none
Compress=yes
SystemMaxUse=2G # лимит на disk
SystemMaxFileSize=128M
MaxRetentionSec=1month
После — sudo systemctl restart systemd-journald.
Для немедленной чистки:
sudo journalctl --vacuum-time=7d # удалить старше 7 дней
sudo journalctl --vacuum-size=2G # оставить только 2 ГБ
Dependency graph
systemd при boot строит граф зависимостей и запускает units параллельно где можно. Зависимости задаются:
Requires=X— X должен быть active, иначе наш unit fails. Жёсткая связь.Wants=X— запустить X (если не запущен), но если он failed — наш всё равно работает.After=X— запускать после X.Before=X— запускать до X.Conflicts=X— если X active, наш не может быть запущен (и наоборот).
Визуализация:
# Полное дерево зависимостей:
systemctl list-dependencies multi-user.target
# Что зависит от меня:
systemctl list-dependencies --reverse postgresql.service
# Критическая цепочка времени запуска:
systemd-analyze critical-chain
.timer — cron-replacement
Cron-задачи можно (и лучше) делать через systemd-timers. Это пара unit-файлов: .service (что делать) + .timer (когда).
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup-db.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup at 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Включить:
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
systemctl list-timers # все таймеры с next/last activation
Преимущества над cron:
Persistent=true— если в 03:00 машина была off, запустится сразу после boot (cron не умеет).RandomizedDelaySec=— стохастика, снимает thundering herd на cron-кластерах.- Структурированные logs в journal (
journalctl -u backup.service). - Зависимости через
After=postgresql.service.
.mount и .socket
.mount — генерируется автоматически из /etc/fstab при boot. Можно создать вручную для специфических случаев (NFS с особыми опциями, lazy mount). Имя файла — из mount path: /data/backup -> data-backup.mount.
.socket — socket activation. Когда кто-то подключается на socket, systemd запускает сервис. Используется для inetd-style сервисов:
# foo.socket
[Socket]
ListenStream=8080
[Install]
WantedBy=sockets.target
# foo.service -- запустится при connection
[Service]
ExecStart=/usr/local/bin/foo-server
Это даёт lazy loading (сервис запускается только когда нужен) и atomic upgrade (sockets открыты systemd, переинсталляция сервиса без потери connections).
Пишем systemd .service unit для Python ETL daemonsystemctl: основные команды
# Жизненный цикл:
systemctl start SVC # запустить
systemctl stop SVC # остановить
systemctl restart SVC # рестарт
systemctl reload SVC # reload config (если поддерживает)
systemctl reload-or-restart SVC # reload если можно, иначе restart
# Автозапуск:
systemctl enable SVC # автозапуск при boot
systemctl disable SVC # убрать автозапуск
systemctl is-enabled SVC # проверить (вернёт enabled/disabled)
systemctl mask SVC # запретить полностью (даже manual start)
systemctl unmask SVC
# Статус и информация:
systemctl status SVC # статус + последние logs
systemctl is-active SVC # active/inactive/failed
systemctl cat SVC # показать unit-файл
systemctl show SVC # все properties (длинный output)
systemctl edit SVC # override unit (создаст /etc/systemd/system/SVC.service.d/override.conf)
systemctl edit --full SVC # full override (копия unit-файла)
# Listing:
systemctl list-units # все active
systemctl list-units --all # включая inactive
systemctl list-units --failed # только failed
systemctl list-unit-files # все доступные
# Targets:
systemctl get-default
systemctl set-default multi-user.target
systemctl isolate rescue.target
# After config change:
systemctl daemon-reload # перечитать unit-файлы (после правок)
Попробуй сам
# 1. Все active services сейчас:
systemctl list-units --type=service --state=running | head -15
# 2. Статус критичного сервиса:
systemctl status sshd
# 3. Логи sshd за последний час:
journalctl -u sshd --since "1 hour ago"
# 4. Failed services:
systemctl --failed
# 5. Default target:
systemctl get-default
# 6. Что в multi-user.target:
systemctl list-dependencies multi-user.target | head -20
# 7. Размер journal:
journalctl --disk-usage
# 8. Все таймеры:
systemctl list-timers
# 9. Время boot:
systemd-analyze
systemd-analyze blame | head -10
# 10. (С root) создать минимальный service:
cat <<EOF | sudo tee /etc/systemd/system/hello.service
[Unit]
Description=Hello world
[Service]
ExecStart=/bin/echo "Hello from systemd"
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl start hello
sudo systemctl status hello
# Удалить:
sudo systemctl stop hello
sudo rm /etc/systemd/system/hello.service
sudo systemctl daemon-reload