systemd Type=: simple, forking, notify і main PID
Unit має статус active, але daemon уже завершився? Порівняйте Type=simple, exec, forking, oneshot і notify та визначте справжній main PID.
Чому systemd повідомляє, що unit активний, коли процес завершив роботу
Unit служби systemd залишається active, доки працює єдиний процес, який systemd вважає головним. Параметр Type= у секції [Service] визначає, який саме це процес. Якщо вибрати неправильне значення, systemd стежитиме за shell-обгорткою або короткоживучим батьківським процесом, тоді як потрібний вам daemon завершить роботу всередині того самого unit. Unit достовірно повідомляє стан процесу, за яким йому наказали стежити.
Зміна політики перезапуску тут не допоможе. Restart= спрацьовує, коли головний процес завершує роботу, тому Restart=always не спрацьовує, доки main PID (ідентифікатор процесу) належить процесу, який ще працює. Спочатку виправте Type=. Дії systemd після фактичного завершення головного процесу визначаються окремо. Це питання розглянуто в посібнику щодо Restart= і RestartSec=.
Що насправді визначає Type=
Кожне значення Type= одночасно відповідає на два запитання: коли systemd може вважати цей unit запущеним і який процес є головним.
Перша відповідь визначає порядок запуску. Unit, у якому ваш unit указано в After=, очікує, доки systemd не оголосить ваш unit запущеним. Type=, який повідомляє про стан «запущено» надто рано, дає залежним unit змогу запуститися до того, як ваш сервіс зможе відповідати на їхні запити.
Друга відповідь визначає нагляд за процесами. systemd поміщає кожен процес, запущений unit, у cgroup (control group) — функцію ядра, яка групує процеси, щоб їх можна було спільно обмежувати та завершувати. Саме cgroup використовує systemctl stop для очищення: KillMode= за замовчуванням має значення control-group, тому під час зупинки unit надсилає сигнал кожному процесу всередині нього. Основний PID має вужче значення. Це єдиний процес, завершення якого зупиняє unit, а його код завершення стає результатом роботи unit. Плутанина виникає тоді, коли cgroup сприймають як основний PID.
Тип Type=simple повідомляє про запуск до фактичного виконання бінарного файлу
Type=simple є значенням за замовчуванням, якщо задано ExecStart= і не вказано Type= або BusName=. systemd створює процес, одразу вважає unit запущеним і трактує цей процес як основний PID. Залежні unit запускаються негайно, ще до виконання бінарного файлу сервісу.
Саме це часто спричиняє несподівану поведінку. Навіть помилка в шляху ExecStart= дає змогу завданню запуску завершитися успішно, а збій виникає лише через мить, коли виконання завершується помилкою. systemd фіксує такий випадок із кодом завершення 203. У власній таблиці systemd цей код має назву EXEC і визначає його як помилку виконання бінарного файлу сервісу. Тому успішне завершення systemctl start не доводить, що ваш бінарний файл існує.
Використовуйте simple для програми, яка працює у foreground і не переводить себе у background. Це стосується більшості сучасних daemon і майже всіх програм, які ви пишете самостійно.
Type=exec очікує фактичного запуску програми
Type=exec — це simple з додатковим кроком. systemd вважає unit запущеним лише після успішного виконання fork і запуску бінарного файлу. Відсутній бінарний файл або User=, який не вдається розв’язати, тепер спричиняє помилку самого завдання запуску, а не повідомлення про успіх із тихим завершенням через мить.
Type=exec з’явився в systemd 240, тому він є в кожному актуальному серверному дистрибутиві. Станом на серпень 2026 року Ubuntu 24.04 постачається із systemd 255, а Debian 13 — із systemd 257. Перевірте свою версію за допомогою systemctl --version.
Недолік — один додатковий крок синхронізації під час запуску. Перевага — коректний код завершення від systemctl start. Для програми, що працює у foreground, використовуйте exec, а не simple.
Type=forking і як втрачається основний PID
Type=forking повідомляє systemd, що процес у ExecStart= створить дочірній процес, а потім навмисно завершиться. systemd чекає завершення цього першого процесу і лише після цього вважає unit запущеним. Залишений дочірній процес є daemon.
Проблема полягає в ідентифікації. Процес, запущений systemd, уже завершився, тому systemd має визначити, який із процесів, що залишилися, є основним. Встановіть PIDFile= у файл, який записує daemon, зазвичай у шлях під /run, і systemd зчитає з нього PID. systemd також перевіряє, чи належить процес із PID у цьому файлі саме цьому сервісу. Тому застарілий файл із PID стороннього процесу буде відхилено, а не використано без перевірки.
Без PIDFile= застосовується GuessMainPID=, значення якого за замовчуванням — yes. Така оцінка надійна лише тоді, коли сервіс зрештою працює як один процес. У документації це обмеження сформульовано прямо: якщо daemon складається з кількох процесів, визначення може бути неправильним, і виявлення збоїв припиняє працювати. Unit також може отримати основний PID 0. Це означає, що systemd взагалі не має процесу для контролю.
Більшість daemon, які створюють дочірні процеси, також мають параметр для роботи у foreground. Використайте цей параметр із Type=exec і видаліть рядок PIDFile=. Менше компонентів означає менше способів втратити PID.
Type=oneshot для завдань, які завершуються
Type=oneshot передбачає, що процес має запуститися та завершитися. systemd вважає unit запущеним лише після завершення процесу, тому oneshot підходить для всього, чого має дочекатися інший unit. Це також неявне значення за замовчуванням, якщо в unit не вказано ні Type=, ні ExecStart=.
Для oneshot характерні дві особливості. Це єдиний тип, який допускає більше одного рядка ExecStart=, і ці рядки виконуються послідовно. Тайм-аут запуску для нього також за замовчуванням вимкнено, тому oneshot, який завис, чекатиме безкінечно, якщо ви самостійно не встановите TimeoutStartSec=.
Після завершення процесу unit повертається до стану inactive. RemainAfterExit=yes залишає його у стані active, хоча жоден процес узагалі не працює. Це навмисний варіант симптому, описаного на початку цієї сторінки. Він правильний, коли завдання unit полягає в тому, щоб залишити певний стан, а не підтримувати роботу процесу: завантажити набір правил firewall або запустити стек контейнерів. Саме за цим шаблоном працює стек Docker Compose, який відновлюється після перезавантаження, де unit виконує команду compose, завершується та залишається активним, оскільки запущені ним контейнери працюють незалежно від нього. Unit типу oneshot також запускається розкладом. Це інша частина сценарію запуску завдання за таймером systemd замість cron.
Type=notify дає змогу сервісу повідомити, коли він готовий
Type=notify передає це рішення самому сервісу. systemd утримує завдання запуску відкритим, доки процес не надішле READY=1 через Unix-сокет, шлях до якого він отримує зі змінної середовища NOTIFY_SOCKET. Інтерфейс C має назву sd_notify(3), і багато серверів уже його підтримують.
Це точна відповідь на запитання «чи запущений сервіс». simple і exec повідомляють про запуск до того, як сервіс прочитає конфігурацію або відкриє сокет для прослуховування. Через це залежний unit може запуститися надто рано й отримати помилку під час першого підключення. notify повідомляє про запуск у момент, коли сам сервіс каже, що готовий.
systemd приймає це повідомлення лише від головного процесу. Саме це означає NotifyAccess=main, і це передбачає Type=notify. Якщо повідомлення надходить від дочірнього процесу або допоміжної програми, задайте NotifyAccess=all. Shell-скрипт може викликати systemd-notify --ready, але ця команда запускається як окремий короткоживучий процес. Тому їй потрібен NotifyAccess=all, а systemd може не визначити джерело повідомлення, якщо процес-відправник уже завершив роботу. Надійніше, коли сервіс сам підтримує цей протокол.
Варто знати ще два пов’язані параметри. Type=notify-reload, доступний починаючи з systemd 253, поширює той самий механізм підтвердження на перезавантаження конфігурації. Тому systemctl reload повертається після того, як сервіс повідомляє про завершення перезавантаження, а не одразу після надсилання сигналу. WatchdogSec= просить сервіс із підтримкою сповіщень надсилати keep-alive повідомлення через заданий інтервал. Якщо граничний час пропущено, systemd вважає сервіс таким, що завершився з помилкою.
Type=dbus і Type=idle
Type=dbus очікує, доки сервіс зареєструє ім’я в D-Bus — шині повідомлень, через яку системні служби та служби робочого столу обмінюються даними. Для цього потрібен BusName=, а Type=dbus стає типовим значенням, щойно встановлено BusName=. Використовуйте його лише для сервісу, який справді реєструє ім’я на шині.
Type=idle працює як simple, але відкладає запуск програми, доки не буде оброблено поставлені в чергу завдання, із обмеженням у п’ять секунд. Цей тип потрібен, щоб вивід у консоль під час завантаження не змішувався з повідомленнями про стан. Це не інструмент упорядкування запуску, тому його не слід використовувати для звичайного сервісу.
Чому wrapper script залишає systemd із неправильним PID
Ось схема, яка спричиняє початкову проблему.
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd записує shell як основний PID. Shell залишається активним, поки exporter працює у foreground. Якщо server завершується аварійно, shell цього не помічає. Тому основний PID і далі активний, unit усе ще має стан active, а Restart= не має чого обробляти. Обидва процеси весь цей час перебувають у cgroup unit, тому systemctl stop і далі коректно виконує очищення. Порушено саме нагляд, а не очищення.
Виправлення залежить від кількості довготривалих процесів, які насправді запускає unit.
Якщо процес один, замініть ним shell.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec замінює shell вказаною програмою та зберігає той самий PID. Тому PID, який записав systemd, тепер належить daemon. Ще краще — видаліть wrapper. Environment= і EnvironmentFile= передають змінні, а ExecStartPre= виконує крок підготовки. Завдяки цьому systemd може безпосередньо запустити daemon і одразу знати його PID.
Якщо процесів два, жоден окремий PID не представляє весь unit. Розділіть їх на два unit і впорядкуйте запуск за допомогою After= та Wants=. Окремий unit для кожного процесу — це схема, яку systemd добре контролює. Лише так кожен процес може мати власну поведінку під час перезапуску.
Що змінює ExitType=cgroup
ExitType= додали в systemd 250. Типово використовується main: unit вважається зупиненим, коли завершується головний процес. За ExitType=cgroup unit вважається активним, доки живий хоча б один процес у його cgroup.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherЦе вирішує одну конкретну проблему. Launcher, який запускає фактичну роботу, а потім завершується, за ExitType=main змусив би systemd вважати unit зупиненим і завершити процеси, що залишилися. За ExitType=cgroup unit натомість відстежує всю групу.
Важливо розуміти, чого цей параметр не вирішує. ExitType=cgroup залишає unit активним, доки працює хоча б один процес, тому unit із двома daemon-процесами залишатиметься активним після завершення одного з них. Це виправляє випадок із launcher. Але параметр не перетворює один unit на supervisor для кількох незалежних процесів. ExitType= також не можна поєднувати з Type=oneshot.
У cgroup також надходять дані обліку ресурсів, тому обмеження на кшталт MemoryMax= і CPUQuota= застосовуються до кожного процесу, запущеного unit, незалежно від того, що Type= визначає для головного PID. Цю тему розглянуто в матеріалі обмеження використання пам’яті та CPU сервісом за допомогою systemd.
Як знайти процес, за яким systemd фактично стежить
Виконуйте ці дії послідовно для unit, який ви налагоджуєте. Спочатку перевірте, що завантажив systemd, потім — за чим він стежить, а далі порівняйте це зі списком процесів.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat виводить файл unit разом з усіма drop-in-файлами, що застосовуються до нього. Так ви читаєте конфігурацію, яку завантажив systemd, а не файл, який, на вашу думку, редагували. systemctl show виводить фактичні значення, зокрема значення за замовчуванням, які ви явно не задавали. Перед переходом далі запишіть значення MainPID.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls виводить усі процеси в cgroup unit. Рядок ps описує єдиний процес, за яким systemd безпосередньо стежить. Читайте ці два результати разом. Значення MainPID, що дорівнює 0, означає, що systemd не має процесу для моніторингу. Якщо MainPID вказує на shell, а в cgroup також є ваш daemon, це описаний вище випадок із wrapper. Якщо в cgroup більше процесів, ніж ви очікували, використовується launcher або daemon, який створює дочірні процеси.
systemctl status app.service
journalctl -u app.service -bsystemctl status одночасно виводить рядок стану та дерево cgroup, тому часто одразу відповідає на обидва запитання. journalctl -u з обмеженням лише поточним завантаженням через -b показує події запуску й зупинки, які systemd записав для unit, разом із кодами завершення, які він отримав. Якщо daemon записує дані у власний файл журналу, а не в journal, прочитайте також цей файл, оскільки systemd може зафіксувати лише те, що надійшло до нього.
Після зміни Type= виконайте reload і restart.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify аналізує файл і повідомляє про параметри, які не може прийняти. daemon-reload змушує systemd повторно прочитати unit-файли з диска. Змінений Type= не застосовується до unit, який уже працює, тому restart є обов’язковим.
Потім перевірте зміну. Візьміть PID процесу, який вас фактично цікавить, із systemd-cgls і завершіть цей процес. Одразу після цього виконайте systemctl is-active app.service. Якщо Type= налаштовано правильно, unit вийде з активного стану. Якщо він залишиться активним, systemd і далі стежить за іншим процесом.
Який параметр Type= systemd слід використовувати
- Програма, яка працює у foreground:
Type=exec. - Програма з підтримкою сповіщень про готовність:
Type=notify, аnotify-reload— якщо вона також підтверджує перезавантаження конфігурації. - Демон, який примусово переходить у background:
Type=forkingізPIDFile=або з його параметром переходу у foreground —Type=exec. - Скрипт, який виконує роботу й завершується:
Type=oneshot, аRemainAfterExit=yes— якщо потрібно було залишити стан після завершення. - Запускач, який завершується, тоді як його дочірні процеси продовжують працювати:
Type=simpleізExitType=cgroup.
Якщо ви не впевнені, який параметр потрібен сторонньому демону, спочатку прочитайте його unit-файл, наданий пакетом. Виконання systemctl cat для unit-файлу, який постачається дистрибутивом, показує параметр Type=, вибраний розробниками upstream. Цей вибір перевірило більше людей, ніж ваш.
FAQ
Чому мій unit systemd залишається активним після завершення процесу?
Тому що процес, який systemd вважає головним, усе ще працює. systemd відстежує один PID для кожного сервісу, вибраний відповідно до Type=, а не кожен процес у cgroup цього unit. Зазвичай причиною є wrapper script, запущений через Type=simple: shell стає головним PID, тому unit залишається активним, коли daemon, запущений shell у фоні, завершується. Виконайте systemctl show -p MainPID app.service, потім перелічіть cgroup unit за допомогою systemd-cgls --unit=app.service і порівняйте результати.
У чому різниця між Type=simple і Type=exec?
Type=simple вважає unit запущеним одразу після створення процесу systemd, ще до виконання binary. Тому неправильний шлях у ExecStart= спочатку дає успішне виконання start job, а потім завершується помилкою. Type=exec очікує успішного виконання, тому цю помилку повертає сам start job. В обох випадках той самий процес вважається головним PID. Type=exec потребує systemd версії 240 або новішої.
Чи потрібен мені PIDFile= з Type=forking?
Так, якщо daemon записує такий файл. Без нього systemd використовує GuessMainPID=. Це припущення, яке надійно працює лише для сервісу, що зрештою переходить до одного процесу. Якщо припущення неправильне або його неможливо зробити, виявлення помилок і автоматичний перезапуск цього unit припиняють працювати. Вкажіть у PIDFile= точний шлях, за яким daemon записує файл, зазвичай під /run.
Коли слід використовувати RemainAfterExit=yes?
Коли призначенням unit була зміна стану системи, а не підтримання роботи процесу. Type=oneshot unit, який завантажує правила firewall або запускає container stack, завершується одразу після виконання роботи. Без RemainAfterExit=yes unit переходить у неактивний стан, через що systemctl stop не має що зупиняти та не може виконати очищення за допомогою ExecStop=. З цим параметром unit залишається активним без процесів, і саме цього тут очікують.
Чи потребує зміна Type= виконання daemon-reload?
Так, а також перезапуску unit. systemctl daemon-reload змушує systemd повторно прочитати unit files з диска, але запущений instance продовжує використовувати Type=, з яким його було запущено. Виконайте sudo systemctl daemon-reload, а потім sudo systemctl restart app.service перед тестуванням. Інакше ви й далі спостерігатимете стару поведінку нагляду за процесом.