SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-27

Почему systemd победил SysV init и Upstart

Узнайте, как systemd решил проблемы зависимостей и отслеживания процессов через cgroups. Разбор перехода дистрибутивов Linux на systemd и анализ обоснованности критики системы.

Почему победил systemd

История systemd началась с двух вещей, которые не умел делать SysV init. В SysV init (системе инициализации, унаследованной Linux от AT&T Unix) не было способа описать зависимости сервиса и определить, какие процессы принадлежат сервису после его запуска. systemd решил обе задачи с помощью функций ядра, недоступных для shell-скриптов: контрольных групп (cgroups) для отслеживания процессов и предварительно открытых сокетов для управления порядком запуска. Дальнейшая история — это то, как эти решения распространились на остальное пользовательское пространство, где и возникли возражения, причем некоторые из них были обоснованными.

Что на самом деле делал SysV init

В системе SysV процесс PID 1 (идентификатор процесса 1, первый процесс, запускаемый ядром) считывал /etc/inittab, выбирал уровень запуска (runlevel) и выполнял скрипты для этого уровня. Скрипты находились в /etc/init.d/. Символические ссылки в /etc/rc3.d/ определяли, какие из них будут запущены и в каком порядке, поэтому /etc/rc3.d/S20nginx указывала на /etc/init.d/nginx и вызывалась с аргументом start.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO

case "$1" in
  start)
    start-stop-daemon --start --quiet --pidfile /run/nginx.pid \
      --exec /usr/sbin/nginx
    ;;
  stop)
    start-stop-daemon --stop --quiet --retry TERM/30/KILL/5 \
      --pidfile /run/nginx.pid
    ;;
esac

Значение 20 в S20nginx — это позиция, а не зависимость. Оно указывает, что данный скрипт выполняется после S19 и перед S21. В нем не указана причина, поэтому ничто не может проверить это, и ничто не может безопасно запустить два несвязанных скрипта одновременно без участия человека, который должен определить безопасность такого действия.

Программа rc запускала каждый скрипт по очереди и ожидала его завершения. Скрипт, который блокировался на тридцать секунд в ожидании сетевого адреса, задерживал всю загрузку на тридцать секунд, даже для тех сервисов, которые не используют сеть.

Заголовок LSB (Linux Standard Base) в начале такого скрипта был попыткой исправить это изнутри. В Debian 6.0 в 2011 году insserv стал стандартом: он считывал Required-Start из каждого скрипта, строил граф и перенумеровывал символические ссылки. После этого Debian мог запускать независимые скрипты одновременно с помощью startpar. Это помогло, но не решило более глубокую проблему. Зависимость по-прежнему основывалась на завершении работы скрипта. Возврат значения 0 процессом S20nginx означает, что функция оболочки завершилась успешно. Это не означает, что nginx готов принимать соединения.

Пять проблем, которые не решал ни один init-скрипт

  • Параллельный запуск. Упорядочивание по имени файла создает линейную зависимость для всех служб в системе, поэтому скорость загрузки равна сумме времени запуска всех компонентов.
  • Готовность к работе. Скрипт запуска завершается сразу после форка демона, а не тогда, когда демон готов обрабатывать запросы. Из-за этого следующий скрипт часто запускается слишком рано.
  • Супервизия. Демон делает двойной форк, а родительский процесс завершается. Это отвязывает процесс от терминала и переподчиняет его PID 1. init видит завершение дочернего процесса и теряет надежную связь с выжившим демоном.
  • Запуск по требованию. inetd (интернет-суперсервер) мог запускать демон при поступлении соединения, но это была отдельная система со своим файлом конфигурации, которая никак не влияла на порядок загрузки остальных компонентов.
  • Контроль ресурсов. Init-скрипты не могли ограничить потребление памяти или долю CPU для службы. ulimit применялся только к одному процессу, а nice влиял лишь на планировщик, поэтому вышедший из-под контроля дочерний процесс службы выглядел для системы как любой другой процесс.

Проблема отсутствия супервизии была самой болезненной в повседневной работе. Временным решением был PID-файл: демон записывал свой идентификатор процесса в /run/nginx.pid, а функция остановки считывала его обратно. Если демон завершался аварийно, файл оставался на диске. Ядро могло повторно использовать этот номер для другого процесса, и start-stop-daemon --stop --pidfile отправлял сигнал тому, кому этот PID был назначен теперь. Устаревший PID-файл — это причина, по которой init-скрипт может завершить не тот процесс.

launchd первым решил проблему сокетов

Apple выпустила launchd в составе Mac OS X 10.4 в 2005 году; автором кода стал Dave Zarzycki. Один процесс заменил собой init, rc, xinetd, crond и watchdogd.

Идея, которую стоило перенять — активация через сокеты. launchd сначала создает все слушающие сокеты, а затем запускает демоны. Клиент, подключающийся к еще не запущенному демону, не получает ошибку соединения, так как ядро удерживает запрос в очереди backlog этого сокета до тех пор, пока демон не вызовет accept(). Порядок запуска двух демонов перестает быть чем-то, что должен определять человек. Эту задачу берет на себя сокет.

launchd был построен на базе Mach IPC (межпроцессное взаимодействие), которое является частью ядра XNU от Apple и не имеет аналогов в Linux. Портирование кода было нереализуемо. Тем не менее, сама идея получила распространение.

В Upstart единицей работы стали события

Upstart от Canonical, написанный Скоттом Джеймсом Ремнантом, был выпущен в составе Ubuntu 6.10 в октябре 2006 года. Его использовали в Fedora с 9 по 14 версию, а также в RHEL 6 и Chrome OS. Он заменил уровни запуска (runlevels) событиями, а задание (job) определяло, какие события должны его запускать и останавливать.

# /etc/init/example.conf
start on filesystem and net-device-up IFACE!=lo
stop on runlevel [!2345]
respawn
respawn limit 10 5
exec /usr/local/bin/exampled

По мере роста количества заданий проявились две проблемы. Первая — это направление зависимостей. Задание содержит инструкцию «запусти меня, когда произойдет это», поэтому информация о том, что от чего зависит, находится не в том файле: сервис знает, что нужно ему, но не может знать, кому он понадобится в будущем. Добавление нового сервиса часто требовало редактирования существующего задания, чтобы оно генерировало новое событие.

Вторая проблема — отслеживание процессов. Upstart отслеживал форкающиеся демоны путем подсчета вызовов fork() с помощью ptrace, которые вы настраивали как expect fork или expect daemon. Если ошибиться в количестве форков, Upstart будет контролировать процесс, который уже завершился, или ждать форка, который уже произошел. Симптомом этого является зависание initctl start без вывода ошибки, которую невозможно объяснить средствами файла задания.

Кроме того, Upstart требовал от участников подписания соглашения о передаче авторских прав Canonical. Это не было техническим недостатком, но повлияло на состав разработчиков.

Переосмысление PID 1, апрель 2010 г.

30 апреля 2010 года Леннарт Поттеринг опубликовал статью под названием «Переосмысление PID 1». Кей Сиверс работал над этим проектом вместе с ним. Аргументация состояла из четырех частей.

  • Запускать меньше процессов. Многие службы могут ожидать момента, когда они действительно потребуются.
  • Не объявлять порядок запуска там, где его можно определить через сокет. Открыть все сокеты за один проход, а затем запустить всё одновременно.
  • Отслеживать процессы с помощью control groups вместо PID-файлов.
  • Описывать службу в декларативном файле, чтобы одно описание работало во всех дистрибутивах.

Первый релиз последовал в том же году. Fedora 14 включила systemd в качестве опции в ноябре 2010 года, а в Fedora 15 он стал стандартным компонентом в мае 2011 года.

Почему cgroups сделали управление процессами надежным

Cgroup (control group) — это функция ядра для группировки процессов, включенная в состав Linux 2.6.24 в 2008 году. systemd помещает каждый сервис в отдельную cgroup. Дочерний процесс наследует cgroup родителя, и непривилегированный процесс не может самостоятельно выйти из нее. Поэтому двойной fork ничего не скрывает: PID 1 всегда содержит точный набор процессов, относящихся к юниту. Остановка сервиса означает завершение всех процессов в его cgroup, что и делает по умолчанию KillMode=control-group.

systemctl status выводит эту группу:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-08-11 09:14:22 UTC; 3min ago
   Main PID: 1042 (nginx)
      Tasks: 3 (limit: 4653)
     Memory: 6.1M (peak: 7.4M)
     CGroup: /system.slice/nginx.service
             ├─1042 "nginx: master process /usr/sbin/nginx"
             ├─1043 "nginx: worker process"
             └─1044 "nginx: worker process"

Этот блок — исчерпывающий ответ на проблему устаревших PID-файлов. Здесь нет файла, который мог бы устареть, так как список является состоянием ядра.

Это же дерево позволяет устанавливать ограничения, поскольку cgroups изначально создавались для учета ресурсов, прежде чем их начали использовать для отслеживания процессов. MemoryMax=, CPUQuota= и TasksMax= занимают всего по одной строке. Установка жесткого лимита памяти и CPU для сервиса сегодня выполняется через drop-in файл, а в 2009 году это потребовало бы написания патча для shell-скрипта, чего никто не делал.

Почему все дистрибутивы перешли на systemd в период с 2011 по 2015 год

  • Fedora 15, май 2011.
  • openSUSE 12.1, ноябрь 2011.
  • Mageia 2, май 2012.
  • Arch Linux, стандарт для новых установок с октября 2012.
  • RHEL 7, июнь 2014.
  • SLES 12, октябрь 2014.
  • Debian 8, апрель 2015.
  • Ubuntu 15.04, апрель 2015.

Срок поддержки RHEL 7 оказался дольше, чем у остальных версий, поскольку CentOS 7 пересобрал его, и именно там большинство администраторов впервые столкнулись с unit file — одним из этапов долгой истории, которая прошла путь от Red Hat Linux через CentOS к Rocky и AlmaLinux.

Причины были по большей части техническими, поэтому переход произошел быстро.

  • Один unit-файл работает во всех дистрибутивах, поэтому разработчики upstream начали поставлять файл .service, а дистрибутивы перестали поддерживать отдельные shell-скрипты для каждого пакета и релиза.
  • Отслеживание сессий рабочего стола перешло на systemd-logind после того, как поддержка ConsoleKit прекратилась примерно в 2012 году. GNOME требовался logind, поэтому дистрибутивам без systemd пришлось искать замену. Этой заменой стал elogind — logind из состава systemd, выделенный в отдельный проект.
  • udev, менеджер устройств, был объединен с деревом исходного кода systemd в апреле 2012 года. Дистрибутивы, поставляющие udev, стали зависеть от репозитория systemd. В ответ Gentoo создала форк eudev.
  • Контейнеризация сделала надежное отслеживание процессов и лимиты для отдельных сервисов более важными, так как обе эти функции основаны на cgroups. Вопрос о том, какой супервизор управляет процессом контейнера, остается актуальным, когда вы настраиваете автозапуск Docker Compose после перезагрузки.

Решение Debian было самым резонансным. Технический комитет проголосовал в феврале 2014 года, голоса разделились поровну, и председатель Bdale Garbee отдал решающий голос в пользу systemd. Через несколько дней Ubuntu объявила, что последует за Debian, а не продолжит использовать Upstart. Группа разработчиков Debian создала форк дистрибутива под названием Devuan в ноябре 2014 года и выпустила Devuan 1.0 в мае 2017 года.

Объективные возражения, изложенные беспристрастно

Область охвата. Один проект теперь поставляет PID 1, демон логирования, управление сеансами входа, менеджер устройств, демон настройки сети, DNS-резолвер, NTP-клиент, среду выполнения контейнеров и загрузчик. Обычный аргумент в защиту, что это отдельные бинарные файлы, которые не обязательно устанавливать, верен, но он не снимает возражение. Как только для работы графической оболочки требуется logind, а logind выделен из дерева исходного кода systemd, выбор перестает быть свободным. Именно это подразумевалось под связностью в спорах, и это произошло.

Бинарный журнал. journald записывает данные в индексированном бинарном формате вместо обычного текста. Вы получаете возможности, недоступные для текстовых файлов: фильтрацию по юнитам и приоритетам, структурированные поля и метаданные, которые отправляющая программа не может подделать, так как journald сам фиксирует юнит и cgroup. journalctl -u nginx -p err --since "-1h" заменяет grep с регулярным выражением для даты. Однако есть и реальные издержки. На машине, которая не загружается, вы не можете прочитать лог с помощью less из rescue shell. Вместо этого вы указываете journalctl путь к примонтированному диску:

sudo journalctl --directory /mnt/var/log/journal --boot -1 --priority err

Здесь есть вторая ловушка, в которую люди попадают один раз. journald хранит логи в /run/log/journal, то есть в оперативной памяти, если не существует /var/log/journal. На машине, где этот каталог отсутствует, journalctl -b -1 не покажет ничего после перезагрузки — именно в тот момент, когда это необходимо. Проверьте и исправьте это:

journalctl --disk-usage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

journalctl --disk-usage теперь должен показывать архивированные журналы в /var/log/journal. Если вы хотите дополнительно сохранять логи в текстовом виде, установите ForwardToSyslog=yes в /etc/systemd/journald.conf и оставьте установленным rsyslog.

Отладка загрузки. Когда юнит зависает, консоль выводит одну строку и больше ничего:

[  *** ] A start job is running for Wait for Network to be Configured (1min 32s / no limit)

Инструменты для дальнейшего анализа существуют: systemctl list-jobs во время зависания, systemd-analyze blame и systemd-analyze critical-chain после него, а также systemd.log_level=debug в командной строке ядра. Справедливая часть претензии заключается в том, что init-скрипт мог прочитать от начала до конца любой, кто знает sh, тогда как для зависшего юнита нужно знать, какую из десятка команд применить. Это реальные издержки. Каждый администратор платит их один раз, и множество администраторов заплатили их одновременно.

Настройки по умолчанию, меняющиеся для всех. В systemd 230 в 2016 году поведение logind по умолчанию было изменено: оставшиеся пользовательские процессы стали завершаться при выходе из системы. Отсоединенные сеансы tmux и screen завершались, когда заканчивался сеанс, в котором они были запущены. Дистрибутивы поставляли KillUserProcesses=no в /etc/systemd/logind.conf, а официальным решением стала настройка loginctl enable-linger <user>. Одно изменение в одном проекте нарушило привычку, на которую полагались миллионы людей, — это и есть то, что на практике означает «слишком много компонентов пользовательского пространства в одном месте».

Зависимость по умолчанию как поверхность атаки. В марте 2024 года бэкдор в xz-utils был нацелен на sshd в Debian и Ubuntu. Оригинальный OpenSSH не линкуется с libsystemd. Дистрибутивы добавили эту связь, чтобы sshd мог сообщать о готовности в systemd, а libsystemd подтягивала liblzma, где и находился бэкдор. Сам протокол уведомления о готовности — это один датаграмма, отправляемая в сокет, указанный в $NOTIFY_SOCKET, поэтому никакая библиотека для этого не требовалась. Ответ systemd заключался в загрузке библиотек сжатия через dlopen, чтобы они больше не линковались по умолчанию. Похожий класс ошибок имеет ту же природу: в 2017 году значение User=, начинающееся с цифры, обрабатывалось как неверное, и юнит запускался от имени root вместо того, чтобы завершиться с ошибкой; таким образом, опечатка приводила к повышению привилегий. Более поздние версии отказываются запускать такой юнит.

История в вашей собственной командной строке systemctl

Каждая описанная выше проблема теперь решается одной директивой в файле, который вы можете прочитать.

  • Последовательная загрузка стала After= и Wants=, а systemd-analyze critical-chain показывает, что именно задержало вашу загрузку.
  • Готовность сервиса стала Type=notify, где сервис записывает READY=1 в $NOTIFY_SOCKET, когда он готов принимать запросы. Type=forking с PIDFile= по-прежнему существует для старых демонов, и именно этот тип завершается с ошибкой start operation timed out. Terminating., если PID-файл так и не появляется. Выбор неверного типа приводит к тому, что юнит сообщает о статусе active, хотя запущенный им демон уже завершился, поэтому стоит знать какой Type= соответствует способу запуска вашего демона, прежде чем писать эту строку.
  • Супервизия стала cgroup, поэтому Restart=on-failure с RestartSec= заменяет скрипт-обертку, а StartLimitBurst= останавливает бесконечный цикл перезапусков при сбоях.
  • inetd стал юнитом .socket, расположенным рядом с юнитом .service.
  • ulimit стал MemoryMax=, CPUQuota= и TasksMax=.
  • Строка su - appuser -c в init-скрипте стала User=, NoNewPrivileges=yes и ProtectSystem=strict, поэтому запуск сервиса от непривилегированного пользователя является стандартным видом юнита, а не дополнительной работой.
[Unit]
Description=Example API
Wants=network-online.target
After=network-online.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
ExecStart=/usr/local/bin/exampled
Restart=on-failure
RestartSec=2
MemoryMax=512M
CPUQuota=50%
TasksMax=128
User=exampled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
StateDirectory=exampled

[Install]
WantedBy=multi-user.target

Одна строка в этом файле — это ошибка, которую совершает каждый хотя бы раз. Requires=postgresql.service — это требование, а не порядок: она означает, что ваш юнит завершится с ошибкой, если упадет Postgres, но она не говорит о том, что Postgres нужно запустить первым. Без After=postgresql.service оба сервиса запустятся одновременно, и ваш сервис попытается подключиться к порту, на котором еще никто не слушает. Эти две директивы разделены намеренно, так как иногда вам нужно одно без другого. ProtectSystem=strict монтирует файловую систему в режиме только для чтения для этого сервиса, поэтому там есть StateDirectory=: она предоставляет сервису один путь для записи внутри /var/lib.

Самое наглядное место, где можно увидеть подходы 2005 года на сервере 2026 года — это SSH в Ubuntu 24.04, которая поставляется с systemd 255 по состоянию на август 2026 года. ssh.service по умолчанию активируется через сокет: ssh.socket удерживает слушающий сокет, а sshd запускается при поступлении соединения. Поэтому Port 2222 в /etc/ssh/sshd_config не дает никакого эффекта, так как sshd не является процессом, открывшим порт. Изменения нужно вносить в юнит сокета.

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=2222

Пустое значение ListenStream= очищает значение, унаследованное от пакетного юнита. Если его пропустить, вы получите оба порта, так как systemd добавляет значения в список, а не заменяет их. Затем примените настройки и проверьте их, не закрывая вторую SSH-сессию:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -lntp | grep 2222

ss должен показать один сокет на порту 2222, принадлежащий systemd, а не sshd. Это дизайн launchd, спустя двадцать лет, на вашем VPS. Если вы предпочитаете старое поведение, sudo systemctl disable --now ssh.socket, за которым следует sudo systemctl enable --now ssh.service, даст вам постоянно запущенный sshd, который снова читает Port из своего собственного конфигурационного файла.

С какими именно деталями вы столкнетесь, зависит от используемого релиза, поэтому стоит знать разницу между LTS и промежуточным релизом Ubuntu, прежде чем планировать обновление. При работе с несколькими машинами тот факт, что файл юнита везде идентичен, является причиной, по которой управление несколькими серверами из одного места теперь является задачей конфигурации, а не написания shell-скриптов. А когда вы пишете свои собственные юниты, пара сервис и таймер выполняет ту работу, которую в 2009 году вы бы разделили между init-скриптом и строкой в cron.

FAQ

Почему дистрибутивы Linux заменили SysV init на systemd?

По двум инженерным причинам и одной причине, связанной с сопровождением. SysV init упорядочивал сервисы по именам файлов, что является позиционным методом, а не методом зависимостей. Кроме того, он терял контроль над демонами, которые создавали дочерние процессы (fork), из-за чего устаревшие PID-файлы могли привести к завершению не того процесса. systemd решил проблему порядка запуска с помощью socket activation и директив зависимостей, а проблему отслеживания — с помощью control groups. Причина сопровождения определила скорость перехода: один unit-файл работает во всех дистрибутивах. Проекты начали поставлять .service-файл, и мейнтейнеры дистрибутивов перестали писать отдельные shell-скрипты для каждого пакета. Fedora 15 перешла на systemd в мае 2011 года, а Ubuntu 15.04 стала последним крупным дистрибутивом, который сделал это в апреле 2015 года.

Является ли systemd одним огромным бинарным файлом?

Нет. Исходный код собирается во множество отдельных программ. PID 1 — это /usr/lib/systemd/systemd, в то время как journald, logind и udevd являются отдельными процессами со своими бинарными файлами. Запустите ls /usr/lib/systemd/, чтобы увидеть их на своей системе. Критика, которая сохраняется до сих пор, касается не размера бинарных файлов, а связанности релизов: эти программы выпускаются вместе и используют внутренние интерфейсы, поэтому дистрибутивы обычно включают их как единый набор, а программное обеспечение, например GNOME, стало ожидать наличия именно logind.

Можно ли до сих пор использовать Linux без systemd?

Да. Devuan поставляется с sysvinit, в Gentoo по умолчанию используется OpenRC, Void использует runit, Alpine использует busybox init с OpenRC, а Slackware сохраняет скрипты в стиле BSD. Цена этого — необходимость работы над совместимостью. Десктопное ПО, ожидающее logind, требует elogind — это logind из состава systemd, поддерживаемый как отдельный пакет. Кроме того, всё больше серверного ПО поставляется только с .service-файлом, поэтому вам придётся писать и поддерживать скрипт запуска самостоятельно.

Почему журнал хранится в бинарном формате, а не в виде обычного текстового файла?

Потому что journald сохраняет структурированные поля с индексом. Это обеспечивает фильтрацию по юнитам, по приоритету и по метаданным, которые отправляющая программа не может подделать: journald сам записывает юнит, cgroup и реальный UID, не доверяя строке лога. Плата за это — необходимость использования journalctl для чтения, в том числе из системы восстановления, где вы указываете путь к примонтированному диску с помощью journalctl --directory /mnt/var/log/journal. Если вам нужен и текстовый формат, установите ForwardToSyslog=yes в /etc/systemd/journald.conf.

Что пришло на смену редактированию скриптов в /etc/init.d?

Drop-in файлы. Не редактируйте юнит в /usr/lib/systemd/system/, так как обновление пакета перезапишет его. Запустите sudo systemctl edit nginx.service, и systemd создаст /etc/systemd/system/nginx.service.d/override.conf, который будет объединён с упакованным юнитом. systemctl cat nginx.service показывает итоговый объединённый результат, а systemd-delta выводит список всех переопределений в системе. После любого ручного редактирования выполните sudo systemctl daemon-reload, иначе следующая команда выведет Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk..

#systemd#linux#init#sysvinit#history