ОП.05Процессы: PID, состояния и сигналызанятие 7Материалы

Часть 6 из 7

Завершение процесса: код возврата и зомби

В этой части разберём, какой код возвращает процесс при завершении, как по коду узнать, что процесс был снят сигналом, и почему завершившийся процесс превращается в зомби.

  1. 6.1 Код завершения
  2. 6.2 Зомби

6.1 Код завершения

Каждый процесс, завершаясь, оставляет родителю число от 0 до 255 — код завершения. По соглашению 0 означает успех, любое другое число — ошибку. Какое число вернуть при ошибке, решает программа: ls возвращает 2, если файла нет, grep — 1, если строка не найдена. Оболочка сохраняет код последней команды в переменной $?.

Если процесс снят сигналом, оболочка показывает код 128 + номер сигнала. По этому числу видно, как завершилась программа, даже если она ничего не напечатала.

КодЧто произошло
0успех: true, команда выполнилась без ошибок
1, 2, …ошибка, номер выбирает программа: false — 1, ls без файла — 2
130128 + 2: прервано сигналом INT, Ctrl+C
137128 + 9: снято сигналом KILL
143128 + 15: завершено сигналом TERM

Встроенная команда wait PID ждёт завершения фонового процесса и возвращает его код. В опыте процесс запускается, получает сигнал, и wait сообщает код. Коды 137 и 143 часто встречаются в журналах контейнеров и служб: 137 — процесс сняли принудительно, например по нехватке памяти, 143 — его попросили завершиться.

В терминалеКоды завершения
  1. true; echo $?# успех
    что делает

    Что делает команда. Выполняет true и печатает его код завершения.

    По частям:

    true
    ничего не делать и завершиться с кодом 0
    ;
    разделитель: выполнить следующую команду после этой
    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
  2. false; echo $?# ошибка
    что делает

    Что делает команда. Выполняет false и печатает его код завершения.

    По частям:

    false
    ничего не делать и завершиться с кодом 1
    ;
    разделитель: выполнить следующую команду после этой
    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
  3. ls /nope; echo $?# ошибка ls
    что делает

    Что делает команда. Пытается показать несуществующий путь и печатает код ошибки ls.

    По частям:

    ls
    вывести содержимое каталога
    /nope
    что показать (абсолютный путь)
    ;
    разделитель: выполнить следующую команду после этой
    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
  4. sleep 100
    что делает

    Что делает команда. Запускает sleep на переднем плане, чтобы прервать его клавишами.

    По частям:

    sleep
    ничего не делать указанное число секунд
    100
    сколько секунд
  5. ^C# Ctrl+C
  6. echo $?# 128 + 2
    что делает

    Что делает команда. Печатает код завершения предыдущей команды.

    По частям:

    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
  7. sleep 100 & kill $!; wait $!; echo $?# 128 + 15
    что делает

    Что делает команда. Запускает процесс, посылает ему TERM, дожидается завершения и печатает код.

    По частям:

    sleep
    ничего не делать указанное число секунд
    100
    сколько секунд
    &
    запустить команду слева в фоне и сразу продолжить
    kill
    послать процессу сигнал
    $!
    какому процессу, PID последнего процесса, запущенного в фоне
    ;
    разделитель: выполнить следующую команду после этой
    wait
    встроенная команда оболочки: дождаться завершения фонового процесса и вернуть его код
    $!
    какой процесс ждать, PID последнего процесса, запущенного в фоне
    ;
    разделитель: выполнить следующую команду после этой
    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
  8. sleep 100 & kill -9 $!; wait $!; echo $?# 128 + 9
    что делает

    Что делает команда. Запускает процесс, посылает ему KILL, дожидается завершения и печатает код.

    По частям:

    sleep
    ничего не делать указанное число секунд
    100
    сколько секунд
    &
    запустить команду слева в фоне и сразу продолжить
    kill
    послать процессу сигнал
    -9
    послать сигнал 9 — KILL: завершить немедленно
    $!
    какому процессу, PID последнего процесса, запущенного в фоне
    ;
    разделитель: выполнить следующую команду после этой
    wait
    встроенная команда оболочки: дождаться завершения фонового процесса и вернуть его код
    $!
    какой процесс ждать, PID последнего процесса, запущенного в фоне
    ;
    разделитель: выполнить следующую команду после этой
    echo
    напечатать текст или значение переменной
    $?
    что напечатать, код завершения предыдущей команды
Результат

коды 0, 1, 2 для обычного завершения и 130, 143, 137 для процессов, снятых сигналами INT, TERM и KILL.

Снимок 6.1Ubuntu 24.04
  1. 10 — успех
  2. 21 — ошибка false
  3. 32 — ls не нашёл файл
  4. 4130 = 128 + 2: Ctrl+C
  5. 5143 = 128 + 15: TERM
  6. 6137 = 128 + 9: KILL
Коды завершения обычных команд и процессов, снятых сигналами.Весь снимок ↗

Коды завершения обычных команд и процессов, снятых сигналами.

Что показывает снимок. true вернул 0, false — 1, ls без файла — 2; процесс, прерванный Ctrl+C, вернул 130, снятый TERM — 143, снятый KILL — 137.

Где это пригодится. Коды 137 и 143 встречаются в журналах служб и контейнеров: по ним видно, что процесс снят сигналом: 137 — KILL, 143 — TERM.

6.2 Зомби

Когда процесс завершается, ядро освобождает его память и файлы, но оставляет запись в таблице процессов: в ней хранится код завершения. Запись удаляется, только когда родитель прочитает код вызовом wait. Пока этого не случилось, процесс — зомби: он уже ничего не делает и ресурсов почти не занимает, но его PID остаётся занятым.

Зомби нельзя завершить сигналом: процесса уже нет, сигнал доставлять некому, и даже KILL ничего не меняет. Убирает зомби только его родитель. Если родитель не вызывает wait из-за ошибки в программе, завершают родителя: зомби становится сиротой, его усыновляет PID 1 или процесс-приёмник сеанса, а тот сразу читает код и удаляет запись.

Один зомби занимает только запись в таблице процессов. Если родитель постоянно порождает потомков и не забирает их коды, зомби накапливаются, и в какой-то момент система не сможет выдать новый PID — число PID ограничено.

В терминалеЗомби и его родитель
  1. bash -c 'sleep 1 & exec -a lab-parent sleep 300' &# родитель, который не ждёт потомков
    что делает

    Что делает команда. Запускает родителя lab-parent, который порождает потомка и не ждёт его: потомок станет зомби.

    По частям:

    bash -c '…'
    запустить bash и выполнить команды из строки
    sleep 1 &
    потомок: спит секунду в фоне и завершается
    exec -a lab-parent sleep 300
    заменить оболочку программой sleep 300 под именем lab-parent; она не ждёт потомков
    &
    запустить всё в фоне
  2. sleep 2
    что делает

    Что делает команда. Ждёт две секунды, чтобы потомок успел завершиться.

    По частям:

    sleep
    ничего не делать указанное число секунд
    2
    сколько секунд
  3. ps -o pid,ppid,stat,cmd --ppid $! -p $!# родитель и потомок
    что делает

    Что делает команда. Выводит родителя lab-parent и его потомков.

    По частям:

    ps
    вывести сведения о процессах
    -o pid,ppid,stat,cmd
    выводить столбцы pid,ppid,stat,cmd
    --ppid $!
    выбрать потомков процесса $!
    -p $!
    выбрать процесс с PID $!
  4. kill -9 $(pgrep -P $!)# KILL зомби
    что делает

    Что делает команда. Посылает KILL потомку lab-parent — зомби.

    По частям:

    kill
    послать процессу сигнал
    -9
    послать сигнал 9 — KILL: завершить немедленно
    $(pgrep -P $!)
    подставить вывод команды pgrep -P $!
  5. ps -o pid,ppid,stat,cmd --ppid $!# зомби на месте
    что делает

    Что делает команда. Выводит потомков lab-parent.

    По частям:

    ps
    вывести сведения о процессах
    -o pid,ppid,stat,cmd
    выводить столбцы pid,ppid,stat,cmd
    --ppid $!
    выбрать потомков процесса $!
  6. kill $!; sleep 0.5# завершить родителя
    что делает

    Что делает команда. Посылает последнему фоновому процессу TERM и полсекунды ждёт, чтобы оболочка успела сообщить о завершении.

    По частям:

    kill
    послать процессу сигнал
    $!
    какому процессу, PID последнего процесса, запущенного в фоне
    ;
    разделитель: выполнить следующую команду после этой
    sleep
    ничего не делать указанное число секунд
    0.5
    сколько секунд
  7. ps -o pid,ppid,stat,cmd --ppid $!# зомби убран
    что делает

    Что делает команда. Выводит потомков lab-parent.

    По частям:

    ps
    вывести сведения о процессах
    -o pid,ppid,stat,cmd
    выводить столбцы pid,ppid,stat,cmd
    --ppid $!
    выбрать потомков процесса $!
Результат

KILL не убирает зомби; после завершения родителя зомби исчезает.

Снимок 6.2Ubuntu 24.04
  1. 1Z: потомок завершился, родитель lab-parent его не ждёт
  2. 2после kill -9 зомби остался: процесса уже нет
  3. 3родитель завершён сигналом TERM
  4. 4потомков у родителя больше нет: запись убрана
Зомби не снимается сигналом и исчезает после завершения родителя.Весь снимок ↗

Зомби не снимается сигналом и исчезает после завершения родителя.

Что показывает снимок. Потомок lab-parent стал зомби Z; kill -9 его не убрал; после завершения родителя у lab-parent больше нет потомков — запись удалена.

Где это пригодится. Если зомби копятся, ищут их родителя в столбце PPID и перезапускают его; посылать сигналы самим зомби бесполезно.

Схема · что происходит с завершившимся процессом

    Итоги части 6

    • Код завершения: 0 — успех, другие числа — ошибка; оболочка хранит его в $?.
    • Процесс, снятый сигналом, получает код 128 + номер сигнала: 130 — Ctrl+C, 143 — TERM, 137 — KILL.
    • Зомби — завершившийся процесс, код которого родитель ещё не прочитал вызовом wait.
    • Зомби не снимается сигналом; его убирает родитель или, после завершения родителя, PID 1.

    В отчётСнимок с кодами 130, 143 и 137 и объяснение каждого числа.

    Открыть в отдельной вкладке ↗