Процесс работает мимо systemd: кто его запустил и что будет после ребута
Заходите на сервер, смотрите ps aux, а там процесс, о котором никто не помнит: сайт вроде работает, но systemctl status его не знает, в /etc/systemd/system/ юнита нет, а в crontab — тем более. Кто-то когда-то запустил его руками через nohup или в screen-сессии, коллега уволился, документации не осталось. Проблема не только в неизвестности — такой процесс не переживёт перезагрузку сервера, и вы узнаете об этом в худший момент. Разберём, как найти такие процессы, понять их происхождение и аккуратно перевести под systemd без остановки сервиса.
Содержание
Как обнаружить процесс, запущенный мимо systemd
Первый шаг — сопоставить список реально работающих процессов со списком того, что знает systemd. Инструмент здесь — не догадки, а прямое сравнение.
Смотрим все процессы с деревом:
ps auxf
Флаг f (forest) показывает иерархию — сразу видно, кто чей родитель. Это важно: процесс, у которого родитель systemd (PID 1) или (sd-pam), скорее всего, управляется юнитом. А вот процесс, чей родитель — bash, sshd (без промежуточного tmux/screen) или он вовсе стал сиротой с PPID 1 из-за того, что родительский шелл давно закрылся — кандидат на «запущено руками».
Дальше — точечная проверка конкретного PID:
systemctl status <PID>
Если PID действительно принадлежит юниту, systemd покажет его имя и статус. Если процесс запущен мимо systemd, вы увидите что-то вроде:
PID 24817 does not belong to any known scope or service unit; core dumps and metrics are unavailable.
Это самый быстрый и надёжный маркер: если systemd не признаёт PID «своим», значит, процесс не управляется ни одним юнитом.
Второй способ — посмотреть cgroup процесса напрямую:
cat /proc/<PID>/cgroup
Если процесс под systemd, в пути будет что-то вроде /system.slice/nginx.service. Если процесс запущен вручную из интерактивной сессии, вы увидите /user.slice/user-1000.slice/session-3.scope — это явно указывает на то, что процесс живёт в пользовательской сессии, а не в system slice.
Ещё один полезный срез — по родительскому процессу через ps -o:
ps -o pid,ppid,pgid,sess,tty,cmd -p <PID>
Колонка tty часто выдаёт происхождение: если там pts/0 или похожее — процесс запущен из интерактивного терминала. Если ? — скорее всего, демонизирован через nohup ... & или отвязан от терминала явно.
Чтобы не искать по одному PID, а сразу увидеть всю картину — сравните список процессов со списком unit-файлов:
systemctl list-units --type=service --all | awk '{print $1}' > /tmp/known_units.txt
ps -eo comm= | sort -u > /tmp/running_comms.txt
Полного автоматического сопоставления это не даёт (имена процессов и unit-файлов не всегда совпадают буквально), но быстро сужает круг подозреваемых — особенно если у вас 5-10 сервисов, а не сотня.
Признаки, по которым узнаётся «ручной» запуск
На практике процессы, запущенные мимо systemd, попадают в одну из нескольких категорий, и у каждой свой набор улик.
Запуск через nohup в фоне. Классика: кто-то зашёл по SSH, набрал nohup python3 app.py > app.log 2>&1 & и вышел из сессии. Признаки:
- родитель —
init/systemdс PID 1, но это НЕ значит управление юнитом — это просто reparenting осиротевшего процесса ядром; ttyв выводеpsравен?;- рядом обычно лежит файл
nohup.outили указанный вручную лог в домашней директории пользователя, который запускал команду.
Процесс внутри tmux или screen. Проверить наличие таких сессий:
tmux ls
screen -ls
Если сессия отображается — процесс жив внутри неё, и tty у него будет pts/N, привязанный именно к этой сессии. При перезагрузке сервера или даже просто при завершении tmux-сервера процесс умрёт. Подробнее о том, как это работает и чем tmux отличается от screen в контексте таких сессий, можно почитать в статье про мультиплексоры терминала.
Процесс, запущенный через setsid или двойной fork. Более «продуманный» ручной запуск, когда автор явно отвязал процесс от терминала и родительской сессии (setsid command & или демонизация через двойной fork в самом приложении). Такой процесс выглядит почти неотличимо от системного демона — у него нет tty, PPID часто равен 1. Отличить от настоящего systemd-юнита можно только через cgroup (/proc/<PID>/cgroup) и systemctl status <PID>.
Процесс, запущенный из cron, но не как systemd timer. Если в crontab -e (у пользователя или в /etc/cron.d/) есть строка, которая запускает долгоживущий процесс (а не разовую задачу), а не просто периодическую джобу — он тоже не под systemd, хотя и не совсем «случайный»: у него хотя бы есть точка входа, которую можно найти через crontab -l -u <user> и /etc/cron.d/*.
Полезная привычка — держать эталонный снимок того, что должно работать на сервере, и сверяться с ним при подозрениях. Как это организовать системно, разобрано в статье про сверку процессов с нормой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSПочему это опасно: что произойдёт при перезагрузке
Главная проблема процесса, запущенного мимо systemd, — он существует только в оперативной памяти текущей загрузки ядра. Никакого persistent-описания того, как его поднять, в системе нет. При reboot, shutdown или аварийном падении сервера:
- процесс просто исчезает — ядро завершает все процессы при остановке;
- ничего не пытается запустить его заново, потому что systemd не знает о его существовании;
- если это был веб-сервис, сайт или API молча падает, и часто это замечают не по алертам (их тоже может не быть настроено — процесс же «неофициальный»), а по звонку от клиента.
Разбор того, что вообще происходит при загрузке и почему процессы без unit-описания в этот процесс не попадают, — в статье как работает systemd при загрузке.
Есть и менее очевидные риски, которые проявляются ещё до перезагрузки:
- Нет мониторинга через systemd.
systemctl status,journalctl -u <service>, автоматические рестарты при падении (Restart=on-failure) — всё это работает только для managed-юнитов. У «ручного» процесса ничего этого нет: упал — просто лежит, пока кто-то не заметит. - Нет ограничений ресурсов. Юнит-файл может задавать
MemoryMax,CPUQuota,TasksMax. Процесс, запущенный вручную, никак не ограничен (кроме системных дефолтов), и в случае утечки памяти или зацикливания может положить весь сервер. - Путаница при передаче дел. Новый сотрудник или подрядчик смотрит
systemctl list-unitsи не видит критичный процесс вообще — для него сервиса как будто не существует, хотя он держит продакшн. - Логи текут в произвольное место. Обычно в
nohup.outв домашней директории того, кто когда-то запускал команду, без ротации — со временем файл может разрастись и съесть место на диске. - Владение процессом привязано к сессии человека. Если запуск был через
setsidи не черезnohup, иногда процесс всё же может зависеть от переменных окружения или ограничений той сессии (ulimit, env), в которой был запущен — и это не всегда очевидно при диагностике.
Как безопасно перевести процесс в systemd-юнит
Главная задача — не потерять доступность сервиса при переводе. Правильный порядок: сначала подготовить и проверить юнит, затем аккуратно переключиться, а не «на живую» дёргать процесс в разные стороны.
Шаг 1. Собрать информацию о текущем запуске.
Нужно точно знать команду запуска, рабочую директорию, переменные окружения и пользователя, от имени которого работает процесс:
ps -o pid,ppid,user,cmd -p <PID>
cat /proc/<PID>/cwd -L # рабочая директория (через readlink)
readlink /proc/<PID>/cwd
cat /proc/<PID>/environ | tr '\0' '\n' # переменные окружения
ls -l /proc/<PID>/fd # какие файлы/сокеты открыты
Команда cat /proc/<PID>/environ выведет переменные окружения процесса построчно, разделённые нулевым байтом — tr '\0' '\n' превращает их в читаемый список. Это особенно важно, если приложение полагается на переменные вроде NODE_ENV, DATABASE_URL и т.п., заданные вручную в той сессии, где его запустили — их нужно будет прописать в юните явно.
Шаг 2. Написать unit-файл.
Базовый шаблон для типового сервиса (например, Node.js или Python-приложение):
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Environment=NODE_ENV=production
Environment=PORT=8080
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Ключевые поля:
| Поле | Зачем |
|---|---|
Type=simple | процесс не форкается сам — стандартный вариант для большинства скриптов |
User= | явно указывает, от чьего имени работает процесс — не root, если не обязательно |
WorkingDirectory= | важно, если приложение читает файлы по относительным путям |
Restart=on-failure | автоматический перезапуск при падении — то, чего не было раньше |
StandardOutput=journal | логи уходят в journalctl, а не в произвольный файл |
О том, почему для некоторых типов приложений Type=simple работает не так, как ожидается, и когда нужен Type=forking или Type=notify, — в статье как systemd понимает, что сервис поднялся. Про настройку автоматических рестартов при падении — в статье про автоматический перезапуск упавших сервисов.
Шаг 3. Проверить юнит без остановки старого процесса.
Здесь и заключается способ избежать простоя — если приложение можно временно поднять на другом порту:
systemctl daemon-reload
Если возможности проверить на другом порту нет (например, приложение слушает фиксированный сокет), проверяйте синтаксис и корректность юнита без старта:
systemd-analyze verify myapp.service
Эта команда проверит unit-файл на синтаксические ошибки и явные проблемы конфигурации, не запуская сам сервис.
Шаг 4. Переключиться с минимальным окном простоя.
Если приложение не поддерживает параллельный запуск на другом порту, единственный способ избежать простоя — сделать переключение максимально быстрым:
systemctl start myapp.service
# убедиться, что новый процесс поднялся и слушает порт
ss -tlnp | grep <PORT>
# только после этого остановить старый процесс
kill <старый_PID>
Если оба процесса не могут слушать один и тот же порт одновременно, стартовать новый через systemd нужно будет с задержкой после kill старого — тогда окно простоя равно времени старта приложения (обычно секунды, но зависит от приложения). Если для сервиса это критично, стоит держать балансировщик или reverse-proxy (nginx/haproxy) перед ним — тогда можно поднять новый процесс на соседнем порту, переключить upstream и только затем остановить старый, вообще без простоя для клиентов.
Шаг 5. Включить автозапуск и убедиться, что старого процесса не осталось.
systemctl enable myapp.service
ps aux | grep <старое_имя_процесса>
Второй командой стоит перепроверить, что не осталось «хвостов» — например, дочерних процессов, которые не завершились вместе с родителем.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как понять, что процесс запущен именно через nohup, а не как-то ещё?
Прямого стопроцентного признака нет, но комбинация: tty равен ? в выводе ps -o tty, PPID равен 1 (процесс осиротел после закрытия родительской сессии), а рядом в домашней директории пользователя лежит файл nohup.out — достаточно надёжный маркер именно nohup-запуска.
Можно ли просто добавить существующий PID в systemd без перезапуска процесса?
Нет, штатного способа «усыновить» уже работающий процесс systemd-юнитом не существует. systemd управляет жизненным циклом процесса от старта, поэтому юнит всегда запускает новый процесс — старый нужно будет остановить.
Что делать, если неизвестно, кто и когда запустил процесс?
Смотрите ls -la /proc/<PID> — там будет время создания директории (примерно соответствует времени старта процесса), а stat на исполняемый файл может показать, когда он был изменён. Если в системе настроен auditd, в его логах может остаться запись о запуске — но если аудит не был включён заранее, восстановить точную историю запуска обычно уже невозможно, придётся ориентироваться на догадки и опрос коллег.
Опасно ли просто оставить процесс как есть, если сервис не критичный?
Технически можно, но тогда стоит хотя бы задокументировать команду запуска и добавить проверку в мониторинг (например, простой cron-скрипт, который проверяет, жив ли процесс, и алертит, если нет) — иначе после ближайшей перезагрузки сервиса просто не будет, а причину придётся выяснять заново.
Юнит написан, но процесс не стартует при systemctl start — с чего начать диагностику?
Первым делом systemctl status myapp.service покажет код выхода и последние строки лога, а journalctl -u myapp.service -n 50 --no-pager — более полную картину. Чаще всего причина в неверном WorkingDirectory, отсутствующих переменных окружения (которые раньше задавались вручную в сессии) или правах пользователя, указанного в User=.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →