1. Введение
Продолжаем серию о Linux. В прошлой части разбирали права доступа и теперь переходим к одной из самых важных тем в Linux — процессам. Любая программа в системе в конечном итоге существует как процесс: nginx, postgres, docker, sshd, systemd, ваш shell и даже потоки ядра. Понимание того, как процессы создаются, живут, взаимодействуют с ядром и завершаются это база для понимания и диагностики Linux-систем. Цель этой статьи, рассказать кратко и на простом языке всю нужную информацию для начинающих так и для опытных пользователей и админов освежить их знания. Важно: для практики если обучаетесь то лучше всего использовать виртуалку с Linux.
Серия в основном под Ubuntu/Debian.
2. Что такое процесс и поток
Процесс — это запущенная программа. Когда вы запускаете контейнер nginx или python app.py или браузер, ядро Linux создаёт процесс: выделяет ему память, даёт номер (PID), и начинает его выполнять. Пока программа не запущена это программный файл, отличается он только наличием доступа x (т. е. возможности исполнения), но как только вы его запускаете, он становится процессом, под управлением ядра Linux. Про поток (thread) достаточно запомнить что это задача внутри процесса, разделяющая с ним адресное пространство и файловые дескрипторы. В ядре представлен той же task_struct, что и процесс, но с флагами CLONE_VM / CLONE_FILES / CLONE_SIGHAND. В утилитах (ps, htop) отображаются как LWP (Light Weight Process) с общим PID и уникальным TID. Сбой в одном потоке затрагивает весь процесс.
У каждого процесса есть своё окружение — набор ресурсов и атрибутов, которые ядро выделяет при создании:
- Виртуальная память, где у процесса своё адресное пространство. Он не видит и не может напрямую изменить память другого процесса.
- Таблица открытых файлов, через неё процесс работает с файлами, сокетами, пайпами и др. По умолчанию открыты 3 дескриптора (stdin), 1 (stdout), 2 (stderr). В остальном процесс открывается сам по ситуации.
- Каталог (cwd), директория, в которой процесс сейчас находится, от неё отсчитываются все относительные пути.
- Переменные окружения, набор пар ключей (или по-другому KEY=VALUE), которые процесс получил от родителя. Через них передают конфиги: PATH, HOME, LANG и другие (к примеру иногда чувств. данные (токены, пароли). Меняются до запуска процесса, внутри только если процесс сам их перезапишет.
- UID / GID, идентификаторы пользователя и группы, от имени которых запущен процесс. По ним идет проверка прав: может ли процесс прочитать файл, открыть порт, выполнить какую-то операцию и т. д.
- Код возврата, целое число 0–255, которое процесс возвращает при завершении: 0 = успех, 1–255 = ошибка. Сохраняется в ядре до тех пор, пока родитель не считает его через команду
wait() / waitpid(). Если родитель не забирает код = процесс остаётся зомби до очистки
3. Жизненный цикл: рождение и смерть
Как рождается процесс: в Linux новый процесс создаётся клонированием существующего через команды fork() и exec(). fork() создаёт точную копию родителя с новым PID, а exec() заменяет её память на код новой программы, сохраняя PID и наследуя ресурсы.
Что наследует дочерний процесс: открытые файловые дескрипторы, переменные окружения, текущий каталог, UID/GID, обработчики сигналов.
Не наследует: PID, очередь сигналов и статистику выполнения.
Как умирает процесс: процесс вызывает exit() (т. е. мы сообщаем ядру информацию о том что программа внутри процесса завершила все свои действия), ну и соответственно освобождаются ресурсы (файлы/память/сокеты), процесс переходит в состояние зомби (Z), ядро отправляет родителю сигнал (SIGCHLD), родитель вызывает wait() и забирает код возврата = зомби удаляется. Если родитель завершается раньше, то дочерний процесс усыновляется systemd (PID 1), который сам выполнит wait() при его завершении.
Когда зомби становится проблемным: по ситуации, несколько зомби, к примеру, это норма, если они живут миллисекунды пока родитель не вызовет wait(). Проблема основная возникает когда он его не вызывает никогда, из-за чего будет накопление зомби, pid-пространство и т. д. — система в итоге перестаёт создавать новые процессы.
В качестве примеров команды:
# Найти зомби
ps aux | awk '$8 == "Z"'
# Найти родителя зомби и «напомнить» ему убраться
kill -CHLD <PPID зомби>
# Если родитель мёртво завис — убить его (дети усыновятся systemd)
kill -TERM <PPID>
4. Состояния процесса
Каждый процесс находится в одном из состояний, отображаемых в колонке STAT (ps aux).
Основные состояния:
| Символ |
Название |
Описание |
| R |
Running |
Выполняется или в очереди на CPU. |
| S |
Sleeping |
Прерываемое ожидание (ввод, сеть, таймер). Реагирует на сигналы. |
| D |
Uninterruptible sleep |
Ожидание I/O, который не прерывается сигналами. |
| T |
Stopped |
Процесс приостановлен сигналом SIGSTOP или SIGTSTP (Ctrl+Z). Не выполняется и не потребляет CPU. Можно возобновить через SIGCONT. |
| Z |
Zombie |
Завершён, но родитель не забрал код возврата через wait() |
| I |
Idle |
Неактивный поток ядра |
Модификаторы состояния:
< — высокий приоритет
N — низкий приоритет
s — лидер сессии
l — многопоточный
+ — на переднем плане
Пример: Ssl — спит, лидер сессии, многопоточный.
5. Идентификаторы: PID, PPID, UID и другие
PID (Process ID) — уникальный номер процесса, который назначает ядро. Не повторяется, пока не исчерпается диапазон.
PPID (Parent PID) — PID процесса, который создал данный. Каждый процесс (кроме systemd) имеет ровно 1 родителя. При завершении родителя его дочерние процессы переходят под управление systemd (PID 1).
# Посмотреть PID/PPID/Имя процесса
ps -ef | head -10
# Дерево процессов (наглядная иерархия. Рекомедую посмотреть данный пример)
pstree -p
# Максимальный PID в системе
cat /proc/sys/kernel/pid_max
Особые процессы: PID 1 и PID 2, эти процессы создаются ядром сразу после загрузки. Они не запускаются пользователем и не появляются через команды как обычные программы. Про PID 1 уже несколько раз упомянули, но всё же стоит отдельно вынести в терминологию.
PID 1 (systemd) — 1-ый процесс в пользовательском пространстве. От него ведут начало все остальные процессы от сессий до демонов, если процесс теряет родителя — PID 1 его усыновляет, забирая коды возврата завершившихся процессов, предотвращая накопление зомби. Убить их нельзя, в основном можно завершить/убить их через перезагрузку системы.
PID 2 (kthreadd) — прародитель всех потоков ядра. От него создаются системные воркеры (к примеру kworker). В ps и htop их видно по квадратным скобкам [kworker/0:0]. Эти процессы работают в пространстве ядра и отвечают за фоновые задачи, такие как обработку прерываний, работу с диском итд.
# Увидеть иерархию от systemd
pstree -p | head -20
# Посмотреть потоки ядра (в квадратных скобках)
ps -eo pid,comm | grep '\['
# Проверить родителя у любого процесса
ps -o pid,ppid,comm -p <PID>
6. Сигналы
Сигнал — это асинхронное уведомление процессу от ядра или другого процесса. Процесс может поймать сигнал, проигнорировать его или выполнить действие по умолчанию.
Главные сигналы
| Сигнал |
Номер |
По умолчанию |
Когда использовать |
| SIGTERM |
15 |
Завершить |
Первый выбор для остановки, процесс может перехватить и завершиться корректно: сохранить данные, закрыть соединения. |
| SIGKILL |
9 |
Убить |
Крайняя мера. Процесс не успевает сохранить состояние |
| SIGHUP |
1 |
Завершить |
Для демонов (перечитывать к примеру конфиг nginx без перезапуска) |
| SIGINT |
2 |
Завершить |
Прерывание (Ctrl+C) |
| SIGCONT |
18 |
Приостановить/Продолжить |
Возобновить приостановленный процесс |
| SIGCHLD |
17 |
Игнорировать |
Уведомление родителю о завершении дочернего процесса |
| SIGSTOP |
19 |
Остановить |
Приостановить процесс |
Наверное появится вопрос "почему SIGKILL это крайняя мера?" Потому что процесс не успеет сделать свои нужды.(сохранить данные/записать файлы итд). Поэтому сначала пробуйте SIGTERM.
Что процесс может сделать с сигналом? Поймать, игнорировать, принять действие по умолчанию. Это означает следующие:
- Поймать, значит выполнить свой обработчик (например, nginx перечитывает конфиг на SIGHUP)
- Игнорировать (ну тут и так понятно)
- Принять действие по умолчанию, обычное завершение.
Стоит ещё отметить что поймать или проигнорировать SIGKILL и SIGSTOP нельзя, т. к. они всегда обрабатываются ядром.
Примеры как отправить сигнал и завершить процесс.
# По PID
kill -TERM 1234
kill -9 1234 # то же что kill -KILL
# По имени процесса
pkill -TERM nginx
pkill -HUP sshd # перечитать конфиг
# По полной командной строке
pkill -f "python app.py"
# Всей группе процессов (минус перед номером)
kill -TERM -<PGID>
# Проверить, жив ли процесс (без отправки сигнала)
kill -0 1234 && echo "жив" || echo "мёртв"
PID=1234
TIMEOUT=30
# Шаг 1: попросить завершиться
kill -TERM "$PID"
# Шаг 2: подождать
for i in $(seq 1 $TIMEOUT); do
kill -0 "$PID" 2>/dev/null || { echo "завершился за ${i}с"; exit 0; }
sleep 1
done
# Шаг 3: если не вышел — убиваем
echo "не завершился за ${TIMEOUT}с, применяем SIGKILL"
kill -KILL "$PID"
7. /proc — заглянуть внутрь любого процесса
Виртуальная файловая система (/proc) — это интерфейс, через который ядро отдаёт информацию о процессах и системе. Файлы здесь не занимают место на диске: когда делаем, к примеру, cat /proc/1234/status, ядро прямо в этот момент формирует текстовый ответ и отдаёт его вам. Здесь лежат данные обо всём: какие процессы запущены, сколько памяти используется и т. д.
Главные файлы /proc/<PID> и другие примеры
# 1. cmdline: командная строка запуска
cat /proc/1/cmdline | tr '\0' ' '
# 2. exe: показывает, какой файл запущен. Если обновили пакет, а процесс не перезагрузили — будет (deleted, означает что этот процесс был запущен, которого больше нет на диске.)
ls -la /proc/1/exe
# 3. cwd: текущий рабочий каталог процесса.
ls -la /proc/1/cwd
# 4. environ: переменные окружения
tr '\0' '\n' </proc/1/environ
# 5. status: состояние/память/UID/сигналы.
cat /proc/1/status
# или с выбором нужных строк
cat /proc/1/status | grep -E "Name|State|Uid|VmRSS|Threads"
# 6. wchan: на каком системном вызове процесс ждёт / завис
cat /proc/1/wchan
# 7. stack: стек ядра (требует рут)
sudo cat /proc/1/stack
Файловые дескрипторы
# 1. список открытых дескрипторов
ls -la /proc/1/fd/
# 2. сколько открыто
ls /proc/1/fd | wc -l
# 3. сравнить с лимитом
cat /proc/1/limits | grep "open files"
если числов fd близко к лимиту, возможно утечка. Частая причина: too many open files
Память и I/O
# 1. Потребление памяти
cat /proc/1/status | grep -E "VmRSS|VmSize|VmSwap"
#2. Статистика ввода-вывода
cat /proc/1/io
VmRSS (Resident Set Size) показатель реального потребления памяти. Разница между rchar и read_bytes показывает насколько эффективен page cache для этого процесса.
8. Планировщик и приоритеты
Linux делит время CPU между процессами через планировщик CFS (Completely Fair Scheduler). Планировщик решает кто получает ресурсы и на сколько.
Nice: что это и как работает?
Nice — это атрибут процесса, и утилита для его изменения.
Как атрибут: число от -20 до +19, которое хранится в ядре для каждого процесса. По умолчанию стоит 0, но чем меньше значение — тем выше приоритет.
Как утилита: nice запускает команду с заданным приоритетом, renice меняет приоритет уже работающего процесса.
Правила Nice и Политика Планировщика
Правила Nice основные довольно простые, уменьшать nice (что означает повышать приоритет) может только root, обычный пользователь может только увеличивать (т. е. снижать приоритет): с 0 до 10 = можно, с 0 до -5 = нельзя. Отмечу что nice влияет только на распределение процессорного времени. Он не даёт гарантий, не резервирует CPU и не работает для real-time задач.
Примеры использования nice
# 1. Запуск команды с низким приоритетом (не будет мешать другим)
nice -n 15 rsync -av /data /backup
# 2. Запустить с высоким приоритетом (root)
sudo nice -n -10 ./service
# 3. Просмотр nice всех процессов (колонка NI)
ps -eo pid,ni,comm | sort -k2 -n | head -20
Теперь про политику планировщика. Если сравнивать, то nice — это мягкое влияние, для более жёсткого контроля есть политики планировщика. Они определяют, как именно процесс конкурирует за CPU.
chrt — утилита для управления политиками планировщика.
| Политика |
Описание |
Когда использовать |
| SCHED_OTHER |
Стандартная, на основе nice. Процессы вытесняют друг друга. |
Обычные приложения, демоны, пользовательские задачи. |
| SCHED_BATCH |
Как OTHER, но оптимизирована для пакетной обработки: не мешает интерактивным задачам. |
Фоновые расчёты, бэкапы, конвертация видео. |
| SCHED_IDLE |
Самый низкий приоритет. Процесс получает CPU только когда система полностью свободна. |
Очень фоновые задачи, которые не должны влиять ни на что. |
| SCHED_FIFO |
Real-time. Процесс выполняется, пока сам не уступит или не завершится. Не вытесняется другими. |
Аудио/видео обработка, промышленная автоматизация. |
| SCHED_RR |
Real-time с квантами времени. Процесс выполняется фиксированными отрезками, затем уступает очередь. |
Задачи с жёсткими временными рамками, но без риска «зависнуть навсегда». |
Приоритет: для real-time политик (FIFO, RR) — число от 1 до 99. Для остальных политик приоритет игнорируется. Будьте осторожны, real-time политики могут повесить систему, если процесс войдёт в бесконечный цикл и не будет уступать CPU (Тестировать и практиковаться рекомендуется через виртуальную машину!)
Примеры использования chrt
# 1. Посмотреть политику и приоритет процесса
chrt -p 1234
# 2. Изменить политику работающего процесса
sudo chrt -f -p 50 1234
# 1234 это пример pid
ionice — утилита для управления приоритетом доступа процесса к диску.
Классы.
- 1 (real-time): первый в очереди диска. Опасно: может заблокировать систему.
- 2 (best-effort): по умолчанию. Приоритет 0–7 внутри класса (0 = высший)
- 3 (idle): работает только когда никто другой не запрашивает диск.
Примеры использования ionice
# 1. Запуск с минимальным i/o-приоритетом.
ionice -c 3 rsync -av /data /backup
# 2. посмотреть i/o-приоритет процесса
ionice -p 1234
taskset — утилита для просмотра и установки привязки процесса к конкретным ядрам процессора. Зачем он нужен? к примеру изолировать критичный сервис от других процессов, которые потребляют много ресурсов.
Примеры использования taskset
# 1. Запуск процесс только на ядрах 0 и 1
taskset -c 0,1 ./myapp
# 2. Запустить на ядрах 2-4 (диапазон)
taskset -c 2-4 ./worker
# 3. Привязка рабочего процесса к ядрам 0,2,4
taskset -cp 0,2,4 1234
# 4. Посмотреть текущую привязку.
taskset -cp 1234
9. OOM Killer — кто умрёт первым
Когда памяти не хватает — ядро убивает кого-то, жертву выбирает OOM Killer по очкам (oom_score), чем больше жрёт памяти, тем ближе к смерти. Примеры и частые практики для чего нужно знать OOM Killer.
# Кто сейчас под угрозой
for pid in /proc/[0-9]*/oom_score; do
score=$(cat $pid 2>/dev/null)
[ "${score:-0}" -gt 100 ] 2>/dev/null || continue
comm=$(cat ${pid%/oom_score}/comm 2>/dev/null)
echo "$score $comm"
done | sort -rn | head -10
# Проверить, был ли кто убит
dmesg | grep -i oom
journalctl -k | grep "killed process"
# Защитить какой-то процесс от убийства
echo -500 | sudo tee /proc/$(pgrep postgres | head -1)/oom_score_adj
# Проверить, был ли кто убит
sudo dmesg | grep -i oom
# Защитить процесс от убийства
echo -500 | sudo tee /proc/$(pgrep postgres | head -1)/oom_score_adj
# Сделать процесс главной мишенью
echo 1000 | sudo tee /proc/1234/oom_score_adj
Кратко ещё стоит добавить про ulimit и cgroups — ограничение ресурсов
ulimit задаёт ограничения для процессов в текущей сессии шелла и их потомков: сколько файлов можно открыть, сколько памяти занять, сколько процессов родить и т. д., но закрыв терминал — всё сбросится, если не прописали постоянно это в limits.conf. ( путь: /etc/security/limits.conf ), пропишите в конфиге все нужные вам ограничения чтобы изменения пережили перезапуск. Проверить текущие ограничения можно примерно так: systemctl show myapp.service | grep -E "Memory|CPU|Tasks|IO"
cgroups более низкоуровневый механизм ядра, который ограничивает ресурсы для произвольной группы процессов независимо от сессий. systemd автоматически создаёт cgroups для каждого сервиса, поэтому это рекомендуемый (моя рекомендация) способ ограничения ресурсов: прописал условный MemoryMax в unit-файлах и всё, ядро следит за лимитами даже если сервис форкает сотню таких процессов/дочерних процессов.
Но вот вопрос, когда что использовать?
Давайте как пример пару ситуаций:
- Лимит для пользователя или сессии: достаточно ulimit и настройка конфига.
- Лимит для systemd-сервиса: systemd + cgroups
- Лимит для контейнера, к примеру в Docker это можно сделать через yaml (docker compose) у них под капотом cgroups.
Примеры команд
# посмотреть все лимиты
ulimit -a
# макс. открытых файлов
ulimit -n
# макс. процессов
ulimit -u
# макс. виртуальной памяти
ulimit -v
# макс. cpu
ulimit -t
# поднять лимит файлов прямо сейчас
ulimit -n 65536
# лимиты конкретного процесса
cat /proc/1/limits
# иерархия cgroups
systemd-cgls
# посмотреть потребление ресурсов по сервисам в реальном времени
systemd-cgtop
# найти в какой cgroup сидит процесс
cat /proc/1/cgroup
10. Мониторинг: top, htop и другие.
top — инструмент, показывающий процессы, нагрузку на процессор, память и среднюю загрузку системы. Можно запустить top сразу, а можно и написать флаги, к примеру обновление каждую секунду top -d 1 или следить за конкретным процессом top -p 1234 (несколь процессов через запятую писать).
Возможно есть ещё флаги, но полезные флаги я выделю вот такие:
Для запуска и отображения:
-d <секунды> — интервал обновления. По умолчанию 3 секунды. -n <число> — завершить после N обновлений. Полезно в скриптах. -b — пакетный режим, выводит результат в stdout без интерактива. Используется вместе с -n,-1 — показать каждое ядро процессора отдельно.
Для фильтрации:
-p <PID> — следить только за указанным процессами, до 20 через запятую. -u <пользователь> — показывать только процессы конкретного пользователя. -U <пользователь> — то же, но включает процессы где пользователь указан в любом из полей.
Прочее:
-H — показывать потоки вместо процессов. Каждый поток отображается отдельной строкой.
-c — показывать полную командую строку вместо короткого имени процесса.
-i — скрыть простаивающие процессы, показывать только активные.
-s — безопасный режим, отключает опасные команды (к примеру r и k).
Заключение
Мы разобрали базу процессов (за рамками остались межпроцессное взаимодействие, strace и т. д.) они войдут в следующие части серии. Самая лучшая практика для вас (если вы обучаетесь к примеру) рекомендую запустить виртуалку и попробуйте создать сценарии используя команды и знания из статьи. Удачи вам!