Обработка потомков

Вопрос или проблема

Я пытаюсь создать контейнер процессов. Контейнер будет запускать другие программы. Например, bash-скрипт, который запускает фоновые задачи с использованием ‘&’.

Важной особенностью, которую я хочу реализовать, является следующее: когда я убиваю контейнер, все, что было запущено под ним, должно быть убито. Не только прямые потомки, но и их потомки тоже.

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

Я считаю, что то, что я хочу, достижимо, потому что, когда вы закрываете xterm, все, что выполнялось внутри него, убивается, если оно не было запущено с nohup. Это включает в себя процессы-сироты. Это то, что я хочу воссоздать.

У меня есть идея, что то, что я ищу, связано с unix-сессиями.

Если бы существовал надежный способ идентифицировать всех потомков процесса, было бы полезно иметь возможность отправлять им произвольные сигналы, например, SIGUSR1.

Если вы отправите сигнал процессу, этот процесс будет убит. Интересно, как возник слух, что убийство процесса также убивает и другие процессы, это кажется особенно нелогичным.

Однако есть способы убить более одного процесса. Но вы не будете отправлять сигнал одному процессу. Вы можете убить всю группу процессов отправив сигнал -1234, где 1234 — это PGID (идентификатор группы процессов), который является PID лидера группы процессов. Когда вы запускаете конвейер, весь конвейер стартует как группа процессов (приложения могут изменить это, вызвав setpgid или setpgrp).

Когда вы запускаете процессы в фоновом режиме (foo &), они находятся в своей собственной группе процессов. Группы процессов используются для управления доступом к терминалу; обычно только группа процессов переднего плана имеет доступ к терминалу. Фоновые задания остаются в той же сессии, но нет средства, чтобы убить всю сессию или даже перечислить группы процессов или процессы в сессии, поэтому это особо не помогает.

Когда вы закрываете терминал, ядро посылает сигнал SIGHUP всем процессам, которые имеют его в качестве управляющего терминала. Эти процессы формируют сессию, но не все сессии имеют управляющий терминал. Для вашего проекта одна из возможностей — это запуск всех процессов в их собственном терминале, созданном script, screen и т.д. Убейте процесс эмулятора терминала, чтобы убить содержащиеся процессы (если только они не были отделены с помощью setsid).

Можно обеспечить большую изоляцию, запустив процессы от имени их собственного пользователя, который больше ничем не занимается. Тогда можно легко убить все процессы: запустить kill (вызов системы или утилиту) от имени этого пользователя и использовать -1 в качестве аргумента PID для убийства, что означает «все процессы этого пользователя».

Можно обеспечить еще большую изоляцию, но с значительно большей настройкой, запустив содержащиеся процессы в настоящем контейнере.

Надежный способ идентифицировать всех потомков процесса — использовать команду pstree <pid>, где pid — это id вашего родительского процесса.

Прочитайте man-страницу pstree здесь.

Для отправки сигнала всем членам группы процессов: killpg(<pgrp>, <sig>);
где pgrp — это номер группы процессов, а sig — это сигнал.

Для ожидания дочерних процессов в указанной группе процессов: waitpid(-<pgrp>, &status, ...);

В качестве альтернативы вы можете запустить ваш контейнер процессов в новой оболочке bash. Создайте новую оболочку bash с помощью команды bash и затем запустите ваши процессы. Когда вы захотите завершить все процессы, выйдите из оболочки командой exit.

Внутри родительского сценария перехватите сигнал убийства и обеспечьте, чтобы все дочерние процессы также были убиты. Например,

#!/bin/bash
# убить родителя и детей вместе
trap "kill 0" EXIT
# создать всех детей
for n in $(seq 1 100)
do
    ( echo "begin $n"; sleep 60; echo "end $n" ) &
done
# дождаться завершения детей
wait

Можно использовать такой скрипт оболочки:

set -m; (
# запустить процессы в этом контейнере:
...
) & set +m; pid=$!
...
# завершить контейнер:
kill -- -"$pid"

Трюк заключается в том, чтобы включить управление заданиями для дочерней оболочки контейнера, чтобы процессы, выполняемые там, получили общий уникальный идентификатор группы процессов, который можно использовать с помощью данной команды kill для убийства всех процессов.

Используйте

unshare -fp --kill-child -- yourprogram

Если вы убьете unshare, все дочерние процессы (которые мог породить yourprogram) будут убиты.

Это стало возможным с util-linux 2.32; я реализовал это в upstream. Это требует либо пользовательских пространств имен (настройка ядра CONFIG_USER_NS=y), либо привилегий суперпользователя. См. также здесь.

Команда rkill из пакета pslist посылает указанный сигнал (или SIGTERM по умолчанию) указанному процессу и всем его потомкам:

rkill [-SIG] pid/name...

Другой вариант для завершения всех потомков оболочки: jobs -p | xargs -n 1 pkill -P

По крайней мере, если вы используете Bash (5.0.17), вы можете использовать следующее:

    #!/bin/bash -m
    set +m
    trap 'kill -- -$$' EXIT
    process1 &
    process2 &
    ....
    wait

Однако есть некоторые проблемы, если вы отправите сигнал USR1 группе процессов.

Если вы используете dash (0.5.10.2), set +m переключит группу процессов так, чтобы она совпадала с группой процессов вызывающего процесса, таким образом уничтожение этой группы процессов убьет и вызывающий процесс.

Проблема в том, что это работает по-разному для root и обычного пользователя.

Пример воспроизведен в 2024 году с той же проблемой.

Когда программа была переведена с работы под root’ом на работу под обычным пользователем, все эти описанные эффекты стали проявляться, включая продолжение работы подпрограмм и прослушивание сокета, например, что вызывает проблемы для следующего процесса, который пытается открыть сокет. Если использовать под root’ом – магически это избавит вас от головной боли и необходимости скриптования для обработки этого.

Резюме. Что, если прервать скрипт с помощью intr = ^C?

Обычный пользователь: Порожденные процессы останутся живыми

Суперпользователь: Порожденные процессы будут также убиты

К сожалению, использование групп процессов и групп сессий имеет множество условий и различий в поведении в зависимости от того, является ли оболочка интерактивной, включено ли управление заданиями, используется ли bash/zsh/fish и т.д.

Вместо этого добавьте уникальную экспортируемую переменную окружения в вашу среду перед запуском любых дочерних процессов, например export MAGIC_MARKER=1. Все дочерние процессы и их подчиненные процессы и т.д. унаследуют эту переменную в своих собственных окружениях*.

Позже, при очистке, вы можете искать любые процессы с этой переменной в их окружении и убивать их:

for pid in $(ps -o pid=); do
  if [[ "$pid" != "$$" ]] && [[ -f /proc/"$pid"/environ ]] && grep -q "MAGIC_MARKER" /proc/"$pid"/environ; then
    echo "$pid: MARKER найден"
    echo kill "$pid"
  fi
done

(*) Обратите внимание, что это уязвимо для потомков, явно удаляющих переменную MAGIC_MARKER из их окружения. Это маловероятно. Если вам нужна абсолютная надежность, вы, вероятно, должны рассмотреть использование cgroups или контейнеров.

pgrep -P ppid: Ограничить совпадения процессами с ID родительского процесса в списке ppid, разделенном запятыми.

# Код ZSH
function ps-children() {
    pgrep -P "$1"
}
function ps-grandchildren() {
  local children=( $(ps-children "$1") ) pid

  for pid in $children[@]
  do
    "$0" "$pid"
  done

  print -r -- "${(F)children}"
}
function kill-withchildren() {
    setopt localoptions re_match_pcre
    local sig=2
    if [[ "$1" =~ '-\d+' ]] ; then
        sig="$1"
        shift
    fi
    local pids=("$@") pid

    for pid in "$pids[@]" ; do
        local children=("${(@f)$(ps-grandchildren "$pid")}")
        kill -$sig "$pid" "$children[@]"
    done
}

.

Ответ или решение

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

Фон и проблема

Ваша задача заключается в разработке процессного контейнера, который запускает другие программы, например, bash-скрипты с фоновыми задачами, используя символ &. Основная цель — гарантировать, что прекращение работы контейнера завершит все порожденные процессы, включая их потомков, аналогично тому, как это происходит при закрытии окна терминала.

Разработка стратегии завершения процессов

  1. Идентификация потомков:
    Для выявления всех потомков процесса можно использовать команду pstree <pid>. Эта команда строит дерево процессов, делая наглядным представление о том, какие процессы зависят от исходного.

  2. Использование групп процессов:
    Каждый процесс может принадлежать к определенной группе процессов, что упрощает управление ими. Вы можете отправлять сигналы всем процессам внутри одной группы, используя kill -<pgrp>.

  3. Терминальные сессии:
    Когда вы закрываете терминал, ОС посылает сигнал SIGHUP всем процессам, которые связаны с этим терминалом как управляющие. Однако реализация такой схемы потребует создания собственного терминала, например, через screen или script, и затем управления его жизненным циклом.

  4. Управление оболочкой:
    Открытие новой оболочки с использованием команды bash и выполнение всех процессов внутри нее позволяет контролировать процессы через стандарт команды exit.

  5. Использование возможностей утилит:

    • unshare -fp --kill-child -- ваш_скрипт: эта команда гарантирует, что завершение родительского процесса будет автоматически приводить к завершению всех потомков.
    • rkill: эта утилита из пакета pslist может отправить сигнал всем потомкам определенного процесса.
    • pgrep: позволяет находить процессы по идентификатору родительского процесса и управлять ими.

Исполнение стратегии в bash-скрипте

#!/bin/bash -m
set +m
trap 'kill -- -$$' EXIT
process1 &
process2 &
wait

Этот скрипт использует возможность управления работами оболочки для создания группы процессов и их последующего завершения.

Учитывайте различия в правах доступа

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

Заключение

Запуская процессы в изолированном окружении, таком как контейнеры или cgroups, вы добиваетесь более точного контроля за их жизненным циклом. Использование уникальных идентификаторов или переменных окружения поможет в дополнительной идентификации и управлении, но для полной безопасности следует избегать их удаления внутри процессов.

Внедрение всех этих подходов позволит вам создать стабильную среду для запуска и завершения зависимых процессов в соответствии с вашими требованиями.

Оцените материал
Добавить комментарий

Капча загружается...