SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-10-01

systemd Type=: simple, forking чи notify

Unit має статус active, але daemon зник? Дізнайтеся, коли обирати simple, exec, forking, oneshot або notify, і як знайти справжній головний PID.

Чому systemd показує unit як активний після завершення процесу

Unit служби systemd залишається active, доки живий процес, який systemd вважає головним. Параметр Type= у секції [Service] визначає, який саме це процес. Якщо вибрати неправильне значення, systemd стежитиме за shell-обгорткою або короткоживучим батьківським процесом, тоді як потрібний daemon завершиться всередині того самого unit. Unit правдиво повідомляє стан процесу, за яким йому наказано стежити.

Зміна політики перезапуску тут не допоможе. Restart= спрацьовує, коли головний процес завершується, тому Restart=always не спрацює, доки головний 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=, який неможливо розв’язати, тепер спричиняє помилку самого завдання запуску. systemd не повідомляє про успішний запуск із подальшим непомітним завершенням через мить.

Type=exec з’явився в systemd 240, тому він доступний у всіх актуальних серверних дистрибутивах. Станом на August 2026 Ubuntu 24.04 постачається з systemd 255, а Debian 13 — із systemd 257. Перевірте свою версію за допомогою systemctl --version.

Вартість — один додатковий крок синхронізації під час запуску. Перевага — коректний статус завершення від systemctl start. Для програми, що працює на передньому плані, віддавайте перевагу exec замість simple.

Type=forking і втрата основного PID

Type=forking повідомляє systemd, що процес у ExecStart= створить дочірній процес, а потім навмисно завершиться. systemd очікує завершення цього першого процесу й лише після цього вважає unit запущеним. Залишений дочірній процес є daemon. Такий підхід походить з епохи SysV, коли після завершення init script ніхто не контролював daemon, а PID file був єдиним записом про запущений процес. Це обмеження є однією з основних причин чому systemd замінив init scripts.

Проблема полягає в ідентифікації процесу. Процес, запущений 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-команду, завершується, а потім залишається active, оскільки запущені ним контейнери продовжують працювати. Unit типу oneshot також використовується для запуску за розкладом. Це інша частина теми запуск завдання за таймером systemd замість cron.

Type=notify дає змогу сервісу повідомити, коли він готовий

Type=notify передає це рішення сервісу. systemd утримує завдання запуску відкритим, доки процес не надішле READY=1 через Unix socket, шлях до якого він отримує зі змінної середовища NOTIFY_SOCKET. Інтерфейс C — це sd_notify(3), і багато серверів уже його підтримують.

Це точна відповідь на запитання «чи запущений сервіс». simple і exec повідомляють про запуск до того, як сервіс прочитає конфігурацію або відкриє socket для прослуховування. Тому залежний unit може запуститися надто рано й не встановити перше з’єднання. notify повідомляє про запуск у момент, коли сам сервіс оголошує себе готовим.

systemd приймає це повідомлення лише від головного процесу. Саме це означає NotifyAccess=main, а Type=notify задає цю поведінку. Якщо повідомлення надходить від дочірнього процесу або допоміжної програми, задайте NotifyAccess=all. Shell script може викликати systemd-notify --ready, але ця команда запускається як окремий короткоживучий процес. Тому їй потрібен NotifyAccess=all, а systemd може не пов’язати повідомлення з процесом, який уже завершився. Надійніше, коли сервіс сам підтримує цей протокол.

Варто знати ще два пов’язані параметри. Type=notify-reload, доступний починаючи з systemd 253, поширює той самий обмін повідомленнями на перезавантаження конфігурації. Тому systemctl reload повертається, коли сервіс повідомляє про завершення перезавантаження, а не одразу після надсилання сигналу. WatchdogSec= просить сервіс із підтримкою сповіщень надсилати повідомлення про підтримання працездатності через заданий інтервал. Якщо крайній термін пропущено, systemd вважає це помилкою.

Type=dbus і Type=idle

Type=dbus очікує, доки сервіс зареєструє ім’я в D-Bus — шині повідомлень, через яку системні та графічні служби взаємодіють між собою. Для нього потрібен BusName=. Він стає типовим значенням, щойно встановлено BusName=. Використовуйте його лише для сервісу, який справді реєструє ім’я на шині.

Type=idle працює як simple, але відкладає запуск програми, доки всі завдання в черзі не буде передано на виконання, із максимальним очікуванням 5 секунд. Цей тип потрібен, щоб вивід консолі під час завантаження не змішувався з повідомленнями про стан. Він не призначений для керування порядком запуску і не має використовуватися для звичайного сервісу.

Чому 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 9101

systemd записує 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.yaml

exec замінює 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.service

systemctl 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 -b

systemctl status виводить разом рядок стану та дерево cgroup, тому часто одразу відповідає на обидва запитання. journalctl -u з обмеженням до цього завантаження через -b показує події запуску й зупинки, які systemd зафіксував для unit, разом із кодами завершення, які він отримав. Якщо daemon записує дані у власний log-файл, а не в journal, прочитайте також цей файл, оскільки systemd може зафіксувати лише те, що надійшло до нього.

Якщо ви змінюєте Type=, виконайте reload і restart.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-analyze verify аналізує файл і повідомляє про параметри, які не може прийняти. daemon-reload змушує systemd повторно прочитати unit-файли з диска. Змінений Type= не застосовується до unit, який уже працює, тому restart є обов’язковим. Чим daemon-reload відрізняється від systemctl reload і коли reload достатньо замість restart, показано в посібнику з керування службами через systemctl.

Після цього перевірте зміну. Візьміть PID потрібного вам процесу з systemd-cgls і завершіть його. Одразу виконайте systemctl is-active app.service. Якщо Type= налаштовано правильно, unit вийде зі стану active. Якщо він залишиться active, systemd і далі стежить за іншим процесом.

Який Type= сервісу systemd слід використовувати

  • Програма, що працює у foreground: Type=exec.
  • Програма з підтримкою сповіщень про готовність: Type=notify, а notify-reload — якщо вона також підтверджує перезавантаження конфігурації.
  • Daemon, який примусово переходить у background: Type=forking з PIDFile= або його параметр переходу у foreground з Type=exec.
  • Скрипт, який виконує роботу й завершується: Type=oneshot, а RemainAfterExit=yes — якщо потрібно було залишити стан.
  • Launcher, який завершується, тоді як його дочірні процеси продовжують працювати: Type=simple з ExitType=cgroup.

Якщо ви не впевнені, який тип потрібен сторонньому daemon, спочатку прочитайте його unit file з пакета. Виконання systemctl cat для unit, який постачає дистрибутив, показує, який Type= вибрав upstream. Цей вибір перевірило більше користувачів, ніж ваш.

FAQ

Чому мій unit systemd залишається активним після завершення процесу?

Тому що процес, який systemd вважає головним, усе ще працює. systemd відстежує один PID для кожного сервісу, вибраний відповідно до Type=, а не всі процеси в cgroup unit. Зазвичай причиною є wrapper script, запущений за допомогою Type=simple: shell є головним PID, тому unit залишається активним, коли daemon, запущений shell у background, завершує роботу. Виконайте systemctl show -p MainPID app.service, потім перегляньте cgroup unit за допомогою systemd-cgls --unit=app.service і порівняйте результати.

У чому різниця між Type=simple і Type=exec?

Type=simple вважає unit запущеним одразу після створення процесу systemd, ще до виконання binary. Тому неправильний path у ExecStart= все одно призводить до успішного start job, після якого виникає помилка. Type=exec очікує успішного виконання, тому ця помилка повідомляється самим start job. В обох випадках той самий процес вважається головним PID. Для Type=exec потрібен systemd версії 240 або новішої.

Чи потрібен мені PIDFile= для Type=forking?

Так, якщо daemon створює такий файл. Без нього systemd використовує GuessMainPID=. Це лише припущення, яке надійне тільки для сервісу, що переходить до роботи в одному процесі. Якщо припущення неправильне або його неможливо зробити, виявлення помилок і автоматичний restart перестають працювати для цього unit. Задайте в PIDFile= точний path до файлу, який створює daemon, зазвичай у /run.

Коли слід використовувати RemainAfterExit=yes?

Коли призначення unit полягає у зміні стану системи, а не в підтриманні роботи процесу. Type=oneshot unit, який завантажує правила firewall або запускає container stack, завершується одразу після виконання роботи. Без RemainAfterExit=yes unit переходить у неактивний стан, тому systemctl stop не має що зупиняти і не може виконати cleanup через ExecStop=. З цим параметром unit залишається активним без процесів, і саме така поведінка тут потрібна.

Чи потрібен daemon-reload після зміни Type=?

Так, а також потрібен restart unit. systemctl daemon-reload змушує systemd повторно прочитати unit files з диска, але запущений instance продовжує використовувати Type=, з яким його було запущено. Виконайте sudo systemctl daemon-reload, а потім sudo systemctl restart app.service перед тестуванням. Інакше ви й далі спостерігатимете стару поведінку supervision.