SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-13

Почему systemd стал стандартом в Linux

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

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

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

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

В системе SysV процесс с PID 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) в начале такого скрипта был попыткой исправить ситуацию изнутри. В 2011 году в Debian 6.0 стандартным стал insserv: он считывал Required-Start из каждого скрипта, строил граф и перенумеровывал символические ссылки. После этого Debian мог запускать независимые скрипты одновременно с помощью startpar. Это помогло, но не решило более глубокую проблему. Зависимость по-прежнему основывалась на завершении работы скрипта. Если S20nginx возвращает 0, это означает, что успешно выполнилась функция оболочки. Это не означает, что nginx начал принимать соединения.

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

  • Параллельный запуск. Порядок загрузки, основанный на именах файлов, создает жесткую последовательность для всех служб, поэтому скорость загрузки равна сумме времени запуска всех компонентов.
  • Готовность к работе. Скрипт завершается сразу после запуска процесса демона, а не тогда, когда демон готов принимать запросы. Из-за этого следующий скрипт часто запускается слишком рано.
  • Супервизия. Демон выполняет двойной fork, после чего родительский процесс завершается. Это отвязывает демон от терминала и делает его дочерним процессом 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 требовал от участников подписания соглашения о передаче прав (contributor agreement) компании Canonical. Это не было техническим недостатком, но повлияло на состав разработчиков, работавших над проектом.

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

30 апреля 2010 года Леннарт Поттеринг опубликовал статью под названием "Rethinking 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-скрипта, чего никто не делал.

Почему все дистрибутивы перешли на него в период с 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 г.

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

  • Один unit-файл работает во всех дистрибутивах, поэтому разработчики upstream начали поставлять файл .service, а дистрибутивы перестали поддерживать отдельные shell-скрипты для каждого пакета и релиза.
  • Отслеживание сессий рабочего стола перешло на systemd-logind после того, как поддержка ConsoleKit прекратилась примерно в 2012 году. GNOME требовался logind, поэтому дистрибутиву без systemd пришлось искать замену. Этой заменой стал elogind — logind из состава systemd, выделенный и поддерживаемый отдельно.
  • udev, менеджер устройств, был объединен с деревом исходного кода systemd в апреле 2012 года. Дистрибутивы, поставляющие udev, теперь отслеживали репозиторий systemd. В ответ Gentoo создала форк eudev.
  • Контейнеризация сделала надежное отслеживание процессов и лимиты для отдельных сервисов более важными, так как обе эти функции основаны на cgroup. Вопрос о том, какой супервизор управляет процессом контейнера, остается актуальным всякий раз, когда вы настраиваете автоматический запуск стека 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-файл так и не появляется.
  • Супервизором стала 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 упорядочивал сервисы по именам файлов, что определяло позицию, а не зависимость, и терял контроль над демонами, которые порождали дочерние процессы, из-за чего устаревшие PID-файлы могли привести к завершению не того процесса. systemd решил проблему порядка через активацию сокетов и директивы зависимостей, а проблему отслеживания — через контрольные группы (cgroups). Причина сопровождения определила скорость перехода: один 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 files). Не редактируйте юнит в /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