SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-13

Історія systemd: чому він переміг

Дізнайтеся, чого не міг SysV init, що першими запропонували Upstart і launchd, чому дистрибутиви перейшли на systemd за чотири роки та які критичні аргументи справді слушні.

Чому systemd переміг

Історія systemd починається з двох завдань, які SysV init не міг виконати. SysV init (System V init, система запуску, яку Linux успадкував від Unix AT&T) не мав способу описати залежності сервісу та визначити, які процеси належать сервісу після його запуску. systemd розв’язав обидві проблеми за допомогою функцій ядра, недоступних для shell-скрипта: control groups для відстеження процесів і попередньо відкритих listening sockets для впорядкування запуску. Подальша історія полягає в тому, як ці два рішення поширилися на решту userland. Саме тоді почалися заперечення, і деякі з них були обґрунтованими.

Що насправді робив SysV init

У системі SysV PID 1 (ідентифікатор процесу 1, перший процес, який запускає ядро) читав /etc/inittab, вибирав рівень виконання та запускав скрипти для цього рівня. Скрипти зберігалися в /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. Це допомогло, але не усунуло глибшу проблему. Залежність усе ще визначалася завершенням скрипту. Повернення S20nginx зі значенням 0 означає, що функція shell завершилася успішно. Це не означає, що nginx приймає з’єднання.

П’ять речей, які не міг виправити жоден init-скрипт

  • Паралельний запуск. Сортування за іменами файлів задає повний порядок для кожного сервісу на машині, тому завантаження триває стільки, скільки сума часу запуску всіх компонентів.
  • Готовність. Скрипт запуску завершується після того, як розгалузив процес daemon, а не після того, як daemon може обробити запит. Тому наступний скрипт часто запускається зарано.
  • Контроль процесів. daemon виконує подвійне розгалуження, а його батьківський процес завершується. Це від’єднує daemon від термінала та перепризначає його батьківським процесом із PID 1. init бачить завершення дочірнього процесу, але не має надійного зв’язку з процесом, який продовжив роботу.
  • Запуск за запитом. inetd (суперсервер для інтернет-сервісів) міг запускати daemon після надходження з’єднання, але це була окрема система з власним конфігураційним файлом. Вона також не вирішувала проблему порядку запуску решти компонентів під час завантаження.
  • Контроль ресурсів. В init-скрипті не було засобу обмежити використання пам’яті сервісом або його частку процесорного часу. ulimit застосовувався до одного процесу, а nice впливав лише на планувальник. Тому дочірній процес сервісу, який неконтрольовано споживав ресурси, виглядав як будь-який інший процес у системі.

Найбільше в повсякденній роботі заважала відсутність контролю процесів. Обхідним рішенням був PID-файл: daemon записував свій ідентифікатор процесу до /run/nginx.pid, а функція зупинки зчитувала це значення з файлу. Якщо daemon примусово завершували, файл залишався. Потім ядро повторно використовувало цей номер для іншого процесу, і start-stop-daemon --stop --pidfile надсилав сигнал процесу, якому він належав на той момент. Застарілий 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, створений Scott James Remnant, вийшов у складі Ubuntu 6.10 у жовтні 2006 року. Його використовували Fedora 9–Fedora 14, а також RHEL 6 і Chrome OS. Він замінив рівень виконання подією, а 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

Коли кількість job зросла, проявилися дві проблеми. Перша — напрямок залежностей. Job повідомляє: «запусти мене, коли це станеться». Тому інформація про залежності зберігається не в тому файлі: сервіс знає, що йому потрібно, але не може знати, кому він знадобиться наступного року. Додавання сервісу часто вимагало редагувати наявний job, щоб він генерував нову подію.

Друга — відстеження процесів. Upstart відстежував daemon, який створював дочірні процеси, підраховуючи виклики fork() за допомогою ptrace. Це налаштовували як expect fork або expect daemon. Якщо неправильно вказати кількість дочірніх процесів, Upstart наглядає за процесом, який уже завершився, або очікує на дочірній процес, який уже було створено. Симптомом є зависання initctl start без помилки. Файл job не дає змоги пояснити таку поведінку.

Upstart також вимагав від учасників підписати угоду Canonical для контриб’юторів. Це не було інженерною помилкою, але вплинуло на те, хто працював над проєктом.

Переосмислення PID 1, квітень 2010 року

30 квітня 2010 року Lennart Poettering опублікував допис під назвою «Переосмислення PID 1». Kay Sievers працював над проєктом разом із ним. Аргументація складалася з чотирьох частин.

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

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

Чому cgroups зробили контроль служб надійним

cgroup (control group) — це функція ядра для групування процесів, яку додали до Linux 2.6.24 у 2008 році. systemd поміщає кожну службу у власну cgroup. Дочірній процес успадковує cgroup батьківського процесу, а непривілейований процес не може перемістити себе з неї. Тому подвійне розгалуження процесів нічого не приховує: PID 1 постійно має точний набір процесів, що належать unit. Зупинка служби означає завершення всіх процесів у її 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 року.

Причини здебільшого були буденними, тому перехід відбувся швидко.

  • Один unit file працює в кожному дистрибутиві, тому upstream-проєкти почали постачати файл .service, а дистрибутиви перестали підтримувати окремий shell script для кожного пакета в кожному релізі.
  • Відстеження desktop-сесій перейшло до systemd-logind після того, як близько 2012 року ConsoleKit перестали підтримувати. GNOME потребував logind, тому дистрибутив без systemd мав знайти заміну. Цією заміною став elogind — logind із systemd, винесений в окремий проєкт і підтримуваний окремо.
  • udev, device manager, у квітні 2012 року об’єднали з деревом вихідного коду systemd. Дистрибутиви, які постачали udev, тепер відстежували репозиторій systemd. У відповідь Gentoo відокремив eudev у власну гілку.
  • Контейнери підвищили важливість надійного відстеження процесів і лімітів для окремих сервісів, оскільки обидві можливості надають cgroups. Питання про те, який supervisor володіє процесом контейнера, досі залишається актуальним, коли ви налаштовуєте автоматичний запуск стека Docker Compose після перезавантаження.

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

Заперечення, викладені коректно

Обсяг. Один проєкт тепер постачає PID 1, демон журналювання, керування сеансами входу, менеджер пристроїв, демон конфігурації мережі, резолвер DNS (системи доменних імен), клієнт NTP (протоколу мережевого часу), засіб запуску контейнерів і завантажувач. Звичне виправдання, що це окремі двійкові файли, які не обов’язково встановлювати, є правильним, але воно не спростовує заперечення. Коли desktop потребує logind, а logind випускають окремо від дерева systemd, вибір більше не є вільним. Саме це означало coupling у наведеному аргументі, і саме це сталося.

Двійковий журнал. journald записує індексований двійковий формат замість звичайного тексту. Ви отримуєте те, чого текстові журнали ніколи не надавали: фільтрацію за unit і пріоритетом, структуровані поля та метадані, які програма-відправник не може підробити, оскільки journald сам записує unit і 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 встановленим.

Налагоджуваність завантаження. Коли unit зависає, консоль показує один рядок і більше нічого:

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

Інструменти для подальшої діагностики існують: systemctl list-jobs, поки unit завис, systemd-analyze blame і systemd-analyze critical-chain після цього, а також systemd.log_level=debug у командному рядку ядра. Коректно сформульоване заперечення полягає в тому, що init script міг прочитати зверху вниз кожен, хто знав sh, тоді як для завислого unit потрібно знати, до якої з десятка команд звернутися. Це реальна вартість. Кожен адміністратор сплачує її один раз, але свого часу її одночасно сплатило багато адміністраторів.

Типове значення, яке змінюється для всіх. systemd 230 у 2016 році змінив типове значення logind, щоб під час виходу із сеансу завершувати всі залишкові процеси користувача. Від’єднані сеанси tmux і screen завершувалися, коли завершувався сеанс, у якому їх було запущено. Дистрибутиви постачали KillUserProcesses=no у /etc/systemd/logind.conf, а рекомендована відповідь — loginctl enable-linger <user>. Одне типове значення в одному проєкті змінило звичку, від якої залежали мільйони людей. Саме це на практиці означає «надто велика частина userland в одному місці».

Типова залежність є поверхнею атаки. У березні 2024 року бекдор у xz-utils націлювався на sshd у Debian та Ubuntu. Upstream OpenSSH не лінкує libsystemd. Ці дистрибутиви додали до нього патч, щоб sshd міг повідомляти systemd про готовність, а libsystemd підтягував liblzma, де містився бекдор. Сам протокол повідомлення про готовність — це одна дейтаграма, надіслана в socket, ім’я якого вказане в $NOTIFY_SOCKET, тому жодна бібліотека для цього ніколи не була потрібна. У відповідь systemd почав завантажувати бібліотеки стиснення за допомогою dlopen, тому за замовчуванням вони більше не лінкуються. Схожу структуру має інший клас помилок: у 2017 році значення User=, яке починалося з цифри, вважалося недійсним, і unit запускався від імені root замість того, щоб завершитися з помилкою. Отже, друкарська помилка ставала підвищенням привілеїв. Пізніші версії відмовляються запускати такий unit.

Історія у вашому власному запрошенні 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= замінює wrapper script, а StartLimitBurst= не дає циклу аварійних перезапусків працювати нескінченно.
  • inetd став блоком .socket, розташованим поруч із блоком .service.
  • ulimit перетворився на MemoryMax=, CPUQuota= і TasksMax=.
  • Рядок su - appuser -c в init script перетворився на User=, NoNewPrivileges=yes і ProtectSystem=strict. Тому запуск сервісу від непривілейованого користувача є стандартною схемою unit, а не додатковою роботою.
[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 задає вимогу, а не порядок запуску: він означає, що ваш unit завершиться з помилкою, якщо Postgres не працює, але не означає, що Postgres потрібно запустити першим. Без After=postgresql.service обидва процеси стартують одночасно, і ваш сервіс підключається до порту, на якому ще ніхто не приймає з’єднання. Це навмисно розділені параметри, оскільки іноді потрібен лише один із них. ProtectSystem=strict монтує файлову систему для цього сервісу лише для читання. Саме тому потрібен StateDirectory=: він надає сервісу один доступний для запису шлях у /var/lib.

Найнаочніше побачити підхід 2005 року на сервері 2026 року можна на прикладі SSH в Ubuntu 24.04, де станом на August 2026 постачається systemd 255. ssh.service за замовчуванням активується через socket: ssh.socket утримує listening socket, а sshd запускається, коли надходить з’єднання. Тому Port 2222 у /etc/ssh/sshd_config не впливає на результат, оскільки саме sshd не відкриває порт. Зміни потрібно внести до socket unit.

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

Порожній ListenStream= очищає значення, успадковане від packaged unit. Якщо його не вказати, ви отримаєте обидва порти, оскільки systemd додає значення до списку, а не замінює його. Після цього застосуйте зміни та перевірте результат. Увесь час тримайте відкритим друге SSH-сеанс:

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

ss має показати один socket на порту 2222, власником якого є systemd, а не sshd. Це дизайн launchd двадцять років потому на вашому VPS. Якщо ви віддаєте перевагу старій поведінці, sudo systemctl disable --now ssh.socket, а потім sudo systemctl enable --now ssh.service запустить довготривалий sshd, який знову читатиме Port з власного config.

Які саме з цих деталей ви зустрінете, залежить від використовуваного release. Тому перед плануванням оновлення варто знати різницю між LTS і interim release Ubuntu. Якщо машин більше однієї, однаковий всюди unit file є причиною, чому керування кількома серверами з одного місця тепер є завданням конфігурації, а не shell scripting. А під час написання власних unit пара service і timer виконує роботу, яку у 2009 році довелося б розділити між init script і рядком cron.

FAQ

Чому дистрибутиви Linux замінили SysV init на systemd?

Є дві інженерні причини та одна причина, пов’язана з обслуговуванням. SysV init упорядковував сервіси за іменем файлу. Це визначає позицію, а не залежність. Крім того, він втрачав контроль над daemon, який відокремлювався від батьківського процесу через fork. Через це застарілий PID-файл міг завершити не той процес. systemd вирішив проблему порядку запуску за допомогою активації через сокети та директив залежностей, а проблему відстеження — за допомогою control groups. Причина, пов’язана з обслуговуванням, визначила швидкість переходу: один unit file працює в кожному дистрибутиві. Тому upstream-проєкти почали постачати файл .service, а мейнтейнери дистрибутивів перестали писати shell script для кожного пакета. 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. Ціна такого вибору — додаткова робота із сумісністю. Для desktop software, яке очікує logind, потрібен elogind — logind від systemd, що підтримується як окремий пакет. Крім того, дедалі більше server software постачається лише з файлом .service, тому startup script потрібно писати й обслуговувати самостійно.

Чому журнал зберігається у двійковому форматі, а не у звичайному текстовому файлі?

Тому що journald зберігає структуровані поля з індексом. Це дає змогу фільтрувати записи за unit і пріоритетом, а також використовувати метадані, які програма-відправник не може підробити. journald сам записує unit, cgroup і фактичний UID, а не покладається на рядок журналу. Недолік полягає в тому, що для читання журналу, зокрема із rescue system, потрібен journalctl. У rescue system потрібно вказати змонтований диск за допомогою journalctl --directory /mnt/var/log/journal. Якщо потрібен також текстовий формат, задайте ForwardToSyslog=yes у /etc/systemd/journald.conf.

Що замінило редагування мого скрипту /etc/init.d?

Drop-in files. Не редагуйте unit у /usr/lib/systemd/system/, оскільки оновлення пакета перезапише його. Виконайте sudo systemctl edit nginx.service, і systemd створить /etc/systemd/system/nginx.service.d/override.conf, який буде об’єднано з unit, що постачається пакетом. systemctl cat nginx.service показує об’єднаний результат, а systemd-delta перелічує всі override у системі. Після будь-якого ручного редагування виконайте 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