Как выбрать Type в systemd: simple, forking или notify
Юнит systemd отображается как active, хотя процесс завершился. Узнайте, как правильно настроить параметр Type для корректного отслеживания основного PID вашего демона.
Почему systemd сообщает, что юнит активен, хотя процесс завершился
Юнит службы systemd остается в состоянии active, пока жив процесс, который systemd считает основным. Параметр Type= в секции [Service] определяет, какой именно это процесс. Если выбрать неверное значение, systemd будет отслеживать оболочку-обертку или кратковременный родительский процесс, в то время как нужный вам демон внутри того же юнита завершится. Юнит сообщает верную информацию о процессе, который ему поручено отслеживать.
Изменение политики перезапуска здесь не поможет. Restart= срабатывает при выходе основного процесса, поэтому Restart=always никогда не активируется, пока основной PID (идентификатор процесса) принадлежит чему-то, что все еще выполняется. Сначала исправьте Type=. То, что делает systemd после фактического завершения основного процесса, — это отдельный вопрос, который рассматривается в руководстве по Restart= и RestartSec=.
На что именно влияет параметр Type=
Каждое значение Type= одновременно отвечает на два вопроса. Когда systemd считает юнит запущенным и какой процесс является основным.
Первый ответ определяет порядок запуска. Юнит, который ссылается на ваш в After=, ожидает, пока systemd не пометит ваш как запущенный. Если Type= сообщает о готовности слишком рано, зависимые юниты могут начать работу до того, как ваш сервис будет готов принимать запросы.
Второй ответ определяет управление процессом. systemd помещает каждый процесс, порожденный юнитом, в cgroup (control group) — механизм ядра, который объединяет процессы для ограничения ресурсов и их совместного завершения. Именно через cgroup systemctl stop выполняет очистку: KillMode= по умолчанию использует control-group, поэтому остановка юнита посылает сигнал каждому процессу внутри него. Основной PID — понятие более узкое. Это единственный процесс, завершение которого означает остановку юнита, а его код выхода становится результатом работы юнита. Путаница возникает, когда cgroup ошибочно принимают за основной PID.
Тип simple сообщает о запуске до выполнения бинарного файла
Type=simple используется по умолчанию, если задан ExecStart=, но отсутствуют Type= и BusName=. systemd создает процесс, сразу считает юнит запущенным и назначает этот процесс основным PID. Последующие юниты начинают работу немедленно, еще до того, как бинарный файл сервиса был исполнен.
Эта деталь объясняет распространенную неожиданность. Опечатка в пути ExecStart= приводит к тому, что задача запуска завершается успешно, а ошибка возникает чуть позже, когда выполнение файла не удается. systemd фиксирует такой случай с кодом завершения 203, который в его собственной таблице называется EXEC и определяется как ошибка выполнения бинарного файла сервиса. Таким образом, успешное завершение systemctl start не гарантирует, что ваш бинарный файл существует.
Используйте simple для программ, которые остаются на переднем плане и не переводят себя в фоновый режим. Это применимо к большинству современных демонов и практически ко всему, что вы пишете самостоятельно.
Тип Type=exec ожидает фактического запуска программы
Type=exec — это simple с одним дополнительным шагом. systemd считает юнит запущенным только после того, как успешно завершились и fork, и выполнение бинарного файла. Отсутствие исполняемого файла или User=, который невозможно разрешить, теперь приводит к сбою самой задачи запуска, вместо того чтобы сообщать об успехе и тихо завершаться с ошибкой чуть позже.
Type=exec появился в systemd 240, поэтому он есть во всех актуальных серверных дистрибутивах. Ubuntu 24.04 поставляется с systemd 255, а Debian 13 — с systemd 257 (по состоянию на август 2026 года). Проверьте свою версию с помощью systemctl --version.
Цена этого — один дополнительный шаг синхронизации при запуске. Выигрыш — достоверный статус завершения от systemctl start. Для программ, работающих в foreground, отдавайте предпочтение exec, а не simple.
Type=forking и проблема потери основного PID
Type=forking сообщает systemd, что процесс, указанный в ExecStart=, создаст дочерний процесс и намеренно завершится. systemd ожидает завершения этого первого процесса и только после этого считает юнит запущенным. Оставшийся дочерний процесс становится демоном.
Сложность заключается в идентификации. Процесс, запущенный systemd, завершился, поэтому systemd должен определить, какой из оставшихся процессов является основным. Установите PIDFile= в значение пути к файлу, в который демон записывает PID (обычно это путь внутри /run), и systemd прочитает PID из него. systemd также проверяет, относится ли PID из этого файла к процессу, который уже принадлежит данному сервису, поэтому устаревший файл с PID постороннего процесса будет отклонён, а не принят на веру.
Если PIDFile= не задан, применяется GuessMainPID=, значение которого по умолчанию — yes. Этот метод угадывания надёжен только в том случае, если сервис состоит из одного процесса. В документации прямо указано ограничение: если демон состоит из нескольких процессов, результат может быть неверным, а механизм обнаружения сбоев перестанет работать. Юнит также может получить основной PID 0, что означает, что у systemd нет процесса для контроля.
Большинство демонов, использующих fork, имеют параметр для работы в фоновом режиме. Используйте этот параметр в Type=exec и удалите строку PIDFile=. Меньшее количество компонентов означает меньше шансов потерять PID.
Type=oneshot для задач, которые завершаются
Type=oneshot ожидает, что процесс выполнится и завершится. systemd считает юнит запущенным только после его выхода, что делает oneshot подходящим вариантом для всего, чего должны дожидаться другие юниты. Это также подразумеваемое значение по умолчанию, если в юните не указаны ни Type=, ни ExecStart=.
Для oneshot характерны два специфических поведения. Это единственный тип, который принимает более одной строки ExecStart=, и эти строки выполняются последовательно. Тайм-аут запуска для него по умолчанию отключен, поэтому oneshot, который завис, будет ждать бесконечно, если вы не настроите TimeoutStartSec= самостоятельно.
После завершения процесса юнит возвращается в состояние inactive. RemainAfterExit=yes сохраняет его в состоянии active, при этом сам процесс не запущен. Это намеренная версия симптома, описанного в начале этой страницы, и она корректна, когда задачей юнита было оставить после себя состояние, а не поддерживать работу чего-либо: загрузка набора правил firewall или запуск стека контейнеров. Это шаблон, лежащий в основе стека Docker Compose, который восстанавливается после перезагрузки, где юнит выполняет команду compose, завершается и остается активным, поскольку запущенные им контейнеры продолжают работать после его выхода. Юнит oneshot также является тем, что запускает расписание, что составляет вторую часть запуска задачи по таймеру systemd вместо cron.
Тип Type=notify позволяет сервису самостоятельно сообщать о готовности
Type=notify переносит принятие решения на сторону сервиса. systemd удерживает задачу запуска открытой до тех пор, пока процесс не отправит READY=1 через Unix-сокет, путь к которому он получает в переменной окружения NOTIFY_SOCKET. Интерфейс на языке C — это sd_notify(3), и многие серверы уже поддерживают его.
Это точный ответ на вопрос «запущен ли сервис». simple и exec сообщают о запуске до того, как сервис прочитает конфигурацию или откроет слушающий сокет, поэтому зависимый юнит может запуститься слишком рано и получить ошибку при первом соединении. 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= и становится значением по умолчанию, как только задан BusName=. Используйте его только для служб, которые действительно регистрируют имя в шине.
Type=idle работает аналогично simple, но откладывает запуск программы до тех пор, пока не будут выполнены все поставленные в очередь задачи, с ограничением по времени в пять секунд. Этот тип существует для того, чтобы вывод в консоль при загрузке не перемешивался с сообщениями о статусе служб. Он не является инструментом управления порядком запуска и не предназначен для обычных служб.
Почему скрипт-обертка приводит к тому, что 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. Оболочка остается активной, пока exporter выполняется на переднем плане. Если server завершается, оболочка этого не замечает, поэтому основной PID остается активным, юнит по-прежнему находится в состоянии active, и Restart= не на что реагировать. Оба процесса все это время находятся в cgroup юнита, поэтому systemctl stop по-прежнему корректно выполняет очистку. Нарушается именно процесс контроля (supervision), а не очистка.
Решение зависит от того, сколько долгоживущих процессов на самом деле содержит юнит.
Если процесс один, замените им оболочку.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec заменяет оболочку указанной программой и сохраняет тот же PID, поэтому PID, записанный systemd, теперь принадлежит демону. Еще лучше — удалите обертку. Environment= и EnvironmentFile= позволяют передать переменные, а ExecStartPre= — выполнить этап настройки, поэтому systemd может запустить демон напрямую и знать его PID по факту запуска.
Если процессов два, ни один PID не представляет юнит целиком. Разделите их на два юнита и настройте порядок их запуска с помощью After= и Wants=. Один юнит на один процесс — это конфигурация, которую systemd контролирует эффективно, и это единственный способ обеспечить для каждого процесса собственную логику перезапуска.
Изменения в ExitType=cgroup
ExitType= был добавлен в systemd 250. Значение по умолчанию — main: юнит считается остановленным, когда завершается основной процесс. При ExitType=cgroup юнит считается запущенным до тех пор, пока жив любой процесс в его cgroup.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherЭто решает одну конкретную проблему. Если программа-запускатор (launcher) инициирует основную работу и завершается, то при ExitType=main systemd считает юнит остановленным и убивает оставшиеся процессы. С ExitType=cgroup юнит отслеживает всю группу целиком.
Важно понимать, чего это не решает. ExitType=cgroup поддерживает активность юнита, пока жив хотя бы один процесс, поэтому юнит, управляющий двумя демонами, останется активным после падения одного из них. Это исправляет ситуацию с запускатором. Это не превращает один юнит в супервизор для нескольких независимых процессов. ExitType= также нельзя использовать совместно с Type=oneshot.
В cgroup также происходит учет ресурсов, поэтому ограничения, такие как MemoryMax= и CPUQuota=, применяются ко всем процессам, порожденным юнитом, независимо от того, что Type= говорит об основном PID. Эта сторона вопроса описана в ограничении памяти и CPU сервиса средствами systemd.
Как найти процесс, за которым на самом деле следит systemd
Выполните следующие шаги для юнита, который вы отлаживаете, строго по порядку. Прочитайте конфигурацию, которую загрузил systemd, затем проверьте, какие процессы он отслеживает, и сравните это с таблицей процессов.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat выводит файл юнита вместе со всеми применяемыми к нему 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 юнита. Строка ps описывает единственный процесс, который контролирует systemd. Изучите их вместе. Значение MainPID, равное 0, означает, что у systemd нет процесса для отслеживания. Если MainPID указывает на оболочку (shell), а в cgroup при этом находится ваш демон, значит, имеет место случай с оберткой (wrapper), описанный выше. Если в cgroup процессов больше, чем вы ожидали, значит, задействован лаунчер или демон, использующий fork.
systemctl status app.service
journalctl -u app.service -bsystemctl status выводит строку состояния и дерево cgroup одновременно, поэтому часто отвечает на оба вопроса сразу. journalctl -u с ограничением по текущей загрузке через -b показывает события запуска и остановки, которые systemd зафиксировал для юнита, вместе с кодами завершения. Если демон пишет в собственный лог-файл вместо journal, прочитайте и этот файл, так как systemd может записать только то, что до него дошло.
Когда вы меняете Type=, выполните перезагрузку конфигурации и перезапуск сервиса.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify анализирует файл и сообщает о настройках, которые он не может принять. daemon-reload заставляет systemd заново прочитать файлы юнитов с диска. Изменения в Type= не применяются к уже запущенному юниту, поэтому перезапуск обязателен, а не опционален.
Затем протестируйте изменения. Возьмите PID процесса, который вас интересует, из systemd-cgls и завершите его командой kill. Сразу после этого выполните systemctl is-active app.service. Если Type= настроен верно, юнит перейдет в неактивное состояние. Если он остается активным, значит, systemd все еще следит за чем-то другим.
Какой параметр Type= в systemd следует использовать
- Программа, которая работает в интерактивном режиме (foreground):
Type=exec. - Программа, поддерживающая уведомление о готовности:
Type=notify, а такжеnotify-reload, если она подтверждает и перезагрузку. - Демон, который принудительно переходит в фоновый режим:
Type=forkingсPIDFile=или его переключатель для работы в интерактивном режиме сType=exec. - Скрипт, который выполняет задачу и завершается:
Type=oneshot, плюсRemainAfterExit=yes, если целью было сохранение состояния. - Запускающий процесс (launcher), который завершается, пока его дочерние процессы продолжают работу:
Type=simpleсExitType=cgroup.
Если вы не уверены, какой тип требуется стороннему демону, сначала изучите его unit-файл из пакета. Выполнение systemctl cat для юнита, предоставленного дистрибутивом, покажет Type=, выбранный разработчиками, и этот выбор был протестирован большим количеством пользователей, чем ваш собственный.
FAQ
Почему мой юнит systemd остается в состоянии active, хотя процесс завершился?
Потому что процесс, который systemd считает основным, все еще жив. systemd отслеживает один PID на сервис, выбранный согласно Type=, а не все процессы в cgroup юнита. Обертка-скрипт, запущенная через Type=simple, — типичная причина: оболочка (shell) является основным PID, поэтому юнит остается активным, когда демон, запущенный оболочкой в фоновом режиме, завершается. Выполните systemctl show -p MainPID app.service, затем выведите список cgroup юнита с помощью systemd-cgls --unit=app.service и сравните их.
В чем разница между Type=simple и Type=exec?
Type=simple считает юнит запущенным сразу после того, как systemd создал процесс, еще до выполнения бинарного файла. Поэтому неверный путь в ExecStart= все равно приведет к успешному завершению задачи запуска, за которым последует сбой. Type=exec ожидает успешного выполнения, поэтому ошибка будет сообщена самой задачей запуска. Оба типа считают один и тот же процесс основным PID. Type=exec требует systemd версии 240 или новее.
Нужен ли мне по-прежнему PIDFile= при Type=forking?
Да, если демон его создает. Без него systemd переходит к GuessMainPID=, что является лишь предположением и надежно работает только для сервиса, который переходит в состояние единственного процесса. Когда предположение неверно или невозможно, обнаружение сбоев и автоматический перезапуск для этого юнита перестают работать. Укажите в PIDFile= точный путь, куда демон записывает файл, обычно это каталог /run.
Когда следует использовать RemainAfterExit=yes?
Когда цель юнита — изменить состояние системы, а не поддерживать работу процесса. Юнит Type=oneshot, который загружает правила брандмауэра или запускает стек контейнеров, завершается сразу после выполнения своей работы. Без RemainAfterExit=yes юнит переходит в состояние inactive, из-за чего systemctl stop нечего останавливать и невозможно выполнить очистку через ExecStop=. С этой опцией юнит остается активным без запущенных процессов, что в данном случае является целевым поведением.
Требует ли изменение Type= выполнения daemon-reload?
Да, а также перезапуска самого юнита. systemctl daemon-reload заставляет systemd перечитать файлы юнитов на диске, но запущенный экземпляр сохраняет Type=, с которыми он был запущен. Выполните sudo systemctl daemon-reload, а затем sudo systemctl restart app.service перед тестированием, иначе вы по-прежнему будете наблюдать старое поведение системы контроля.