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. Сбой в одном потоке затрагивает весь процесс.

У каждого процесса есть своё окружение — набор ресурсов и атрибутов, которые ядро выделяет при создании:

  1. Виртуальная память, где у процесса своё адресное пространство. Он не видит и не может напрямую изменить память другого процесса.

  2. Таблица открытых файлов, через неё процесс работает с файлами, сокетами, пайпами и др. По умолчанию открыты дескрипторы: 0 (stdin), 1 (stdout), 2 (stderr). Остальные процесс открывает сам по мере необходимости.

  3. Каталог (cwd) — директория, в которой процесс сейчас находится. От неё отсчитываются все относительные пути.

  4. Переменные окружения — набор пар ключей (или, по-другому, KEY=VALUE), которые процесс получил от родителя. Через них передают конфиги: PATH, HOME, LANG и другие (к примеру, иногда чувствительные данные: токены, пароли). Меняются до запуска процесса, внутри — только если процесс сам их перезапишет.

  5. UID / GID — идентификаторы пользователя и группы, от имени которых запущен процесс. По ним идёт проверка прав: может ли процесс прочитать файл, открыть порт, выполнить какую-то операцию и т. д.

  6. Код возврата — целое число от 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

Неактивный поток ядра

Модификаторы состояния:

  1. < — высокий приоритет

  2. N — низкий приоритет

  3. s — лидер сессии

  4. l — многопоточный

  5. + — на переднем плане

Пример: 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) — первый процесс в пользовательском пространстве. От него ведут начало все остальные процессы: от сессий до демонов. Если процесс теряет родителя, 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.

Что процесс может сделать с сигналом? Поймать, игнорировать, принять действие по умолчанию. Это означает следующие:

  1. Поймать, значит выполнить свой обработчик (например, nginx перечитывает конфиг на SIGHUP).

  2. Игнорировать (ну тут и так понятно).

  3. Принять действие по умолчанию, обычное завершение.

Стоит ещё отметить, что поймать или проигнорировать 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> и другие примеры

# cmdline: командная строка запуска
cat /proc/1/cmdline | tr '\0' ' '

# exe: показывает, какой файл запущен. Если обновили пакет, а процесс не перезагрузили – будет (deleted, означает что этот процесс был запущен, которого больше нет на диске.) 
ls -la /proc/1/exe

# cwd: текущий рабочий каталог процесса.
ls -la /proc/1/cwd

# environ: переменные окружения
tr '\0' '\n' </proc/1/environ

# status: состояние/память/UID/сигналы.
cat /proc/1/status 
# или с выбором нужных строк
cat /proc/1/status | grep -E "Name|State|Uid|VmRSS|Threads"

# wchan: на каком системном вызове процесс ждёт / завис
cat /proc/1/wchan

# stack: стек ядра(требует рут)
sudo cat /proc/1/stack

Файловые дескрипторы

# Cписок открытых дескрипторов
ls -la /proc/1/fd/

# Cколько открыто
ls /proc/1/fd | wc -l

# Cравнить с лимитом
cat /proc/1/limits | grep "open files"

Если число в fd близко к лимиту, возможна утечка. Частая причина ошибки: too many open files.

Память и I/O

# Потребление памяти
cat /proc/1/status | grep -E "VmRSS|VmSize|VmSwap"

# Статистика ввода-вывода
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

# Запуск команды с низким приоритетом(не будет мешать другим)
nice -n 15 rsync -av /data /backup

# Запустить с высоким приоритетом (root)
sudo nice -n -10 ./service

# Просмотр 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

# Посмотреть политику и приоритет процесса
chrt -p 1234

# Изменить политику работающего процесса
sudo chrt -f -p 50 1234

# 1234 это пример pid

ionice — утилита для управления приоритетом доступа процесса к диску.

Классы.

  • 1 (real-time): первый в очереди диска. Опасно: может заблокировать систему.

  • 2 (best-effort): по умолчанию. Приоритет 0-7 внутри класса(0 = высший)

  • 3 (idle): работает только когда никто другой не запрашивает диск.

Примеры использования ionice

# Запуск с минимальным i/o-приоритетом.
ionice -c 3 rsync -av /data /backup

# посмотреть i/o-приоритет процесса
ionice -p 1234

taskset — утилита для просмотра и установки привязки процесса к конкретным ядрам процессора. Зачем она нужна? К примеру, чтобы изолировать критичный сервис от других процессов, которые потребляют много ресурсов.

Примеры использования taskset

# Запуск процесс только на ядрах 0 и 1
taskset -c 0,1 ./myapp

# Запустить на ядрах 2-4 (диапазон)
taskset -c 2-4 ./worker

# Привязка рабочего процесса к ядрам 0,2,4
taskset -cp 0,2,4 1234

# Посмотреть текущую привязку.
taskset -cp 1234

Ещё пример: на 8-ядерном сервере привяжите базу данных к ядрам 0-3, а веб-сервер — к 4-7. Это снизит конкуренцию за кэш и память, уменьшит миграцию и повысит стабильность.

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-файлах — и всё, ядро следит за лимитами, даже если сервис форкает сотню процессов или дочерних процессов.

Но вот вопрос: когда что использовать?

Давайте как пример пару ситуаций:

  1. Лимит для пользователя или сессии: достаточно ulimit и настройка конфига.

  2. Лимит для systemd-сервиса: systemd + cgroups

  3. Лимит для контейнера, к примеру в 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 и т. д.), они войдут в следующие части серии. Самая лучшая практика для вас (если вы, к примеру, обучаетесь) — запустить виртуалку и попробовать создать сценарии, используя команды и знания из статьи. Удачи вам!

© 2026 ООО «МТ ФИНАНС»

Комментарии (2)


  1. Granulex
    02.06.2026 16:10

    Статья сама предупреждает про «чувствительные данные» в env-переменных – но не договаривает: они затем лежат в /proc/PID/environ и читаются любым процессом с нужными правами. Docker по умолчанию это и делает.


    1. opensophy Автор
      02.06.2026 16:10

      Здравствуйте! Согласен, это хороший нюанс. В статье я только кратко упомянул, что в env могут лежать чувствительные данные, но не стал углубляться в вопросы доступа через /proc и особенности контейнерных окружений, счёл эту информацию на другую часть статьи по безопасности Linux.