Сегодняшний пост доставлен вам письмом Z
.
Этот пост посвящен поджатникам Linux, почему они существуют, каково их поведение и как вы можете их использовать, на практическом примере в Go. Если вас устраивает, как работает таблица процессов и состояния процессов, вы можете пропустить раздел предварительного чтения, в противном случае поехали!
Предварительное чтение состояний процессов
В Linux процесс может проходить через различные состояния, однако обычно, когда вы смотрите на вывод ps
, вы видите только R
(для работы) и S
(для прерываемого сна). Ниже вы можете увидеть некоторый вывод ps
для родительской и дочерней связи, они оба находятся в состоянии S
с некоторой дополнительной информацией, о которой вы можете прочитать на странице man7 ps
.
Однако реже вы можете видеть процессы в состоянии Z
(для зомби); зомби-процессы — это те, которые завершились, но не были получены предком через системный вызов из семейства wait
. Они больше не работают, но оставили какое-то состояние в таблице процессов ядра. Их часто можно увидеть со связанной строкой <defunct>
string, как показано ниже:
Итак, почему мы видим здесь зомби-процесс? Почему мы вообще видим их в выводе ps
? Что ж, вывод ps
создается путем просмотра содержимого файлов, найденных в каталоге /proc/<pid>/
. Файловая система procfs
— это особый зверь, виртуальная файловая система, которая может читать таблицу процессов — вы можете прочитать о ней больше на странице man7 proc
. После завершения процесса и перехода в состояние Z
он все еще имеет запись в таблице процессов до тех пор, пока он не будет собран, и поэтому он появляется в выводе ps
. Так что в этом случае мы можем указать пальцем на процесс ./main
за неправильную жатву!
По умолчанию, когда дочерние процессы становятся осиротевшими (из-за того, что их родительский процесс умирает :(как печально), они переназначаются процессу инициализации (pid 1
) в своем пространстве имен pid. Процесс инициализации отвечает за очистку от любого мусора, и попытается пожинать всех зомби, которые были перевоспитаны под ним.
Иногда переназначение init является нежелательным поведением; Иногда нам нужно, чтобы процесс отвечал за всех потомков, даже если промежуточные процессы в дереве завершились. Каноническим примером здесь является поддержка супервизорных процессов пользовательского пространства, таких как systemd
, где сервисы часто дважды разветвляются для демонизации — на основе описанного выше поведения это приведет к тому, что сервисы переназначаются непосредственно для инициализации, минуя супервизорный процесс.
Так какой же механизм позволяет супервизорным процессам работать так, как задумано?
Не бойтесь субжнеца
Начиная с версии 3.4, в Linux появилась концепция поджатки. Ни один ресурс не может сказать это лучше, чем страница man7 на prctl:
Поджнец выполняет роль init(1) для своих дочерних процессов. Когда процесс становится осиротевшим (т. е. его непосредственный родитель завершается), тогда этот процесс будет переназначен ближайшему еще живому предку-поджнецу. Впоследствии вызовы getppid() в осиротевшем процессе теперь будут возвращать PID подчиненного процесса, а когда потерянный процесс завершится, именно этот вспомогательный процесс получит SIGCHLDсигнал и сможет ждать(2) процесса, чтобы обнаружить его статус завершения.
Итак, давайте перейдем к самой интересной части и практически поиграем с субжнецами. Я создал небольшую библиотеку для работы на https://github.com/williammartin/subreaper, которая предоставляет три функции:
Функция Prctl
из библиотеки unix
позволяет взаимодействовать с различными атрибутами процесса. В зависимости от значения, переданного в первом аргументе, следующие аргументы могут иметь разное значение. Для атрибутов subreaper нам нужны значения PR_SET_CHILD_SUBREAPER
и PR_GET_CHILD_SUBREAPER
.
При установке процесса в качестве поджатки ненулевой второй аргумент устанавливает атрибут, а если он равен нулю, атрибут не устанавливается. При извлечении настройки поджатника процесса второе значение является указателем, в котором установлено значение атрибута поджатника (0
или 1
).
Итак, теперь у нас есть несколько утилит, которые играют с настройкой subreaper для процесса, и мы можем увидеть, как это влияет на дерево процессов и сбор урожая!
Это простая демонстрационная программа, использующая предыдущую библиотеку. Давайте разберем структуру времени выполнения; Эта программа выполняет двойное ответвление, как обсуждалось ранее, когда выполняется с run
в качестве первого аргумента.
- Родитель порождает и ждет ребенка (через
cmd.Run()
), затем спит - Ребенок порождает внука, но не ждет (через
cmd.Start()
) - Внучатый процесс переходит в спящий режим
Следует отметить, что /proc/self/exe
— это специальный файл, который ссылается на исполняемый в данный момент двоичный файл. Мы можем использовать ps
для сравнения таблицы процессов с поведением повторного родителя по умолчанию (закомментировав строку subreaper.Set()
) с установкой родителя в качестве поджатки:
Поведение повторного родителя по умолчанию
Здесь мы видим, что процесс /proc/self/exe grandchild
был преобразован в процесс init. Если мы еще раз проверим позже, он был собран и больше не появляется в выводе ps
.
Поведение вспомогательного жнеца при перевоспитании:
Вау! На этот раз, когда ребенок вышел, внук был переназначен поджнецу, а не инициализирован! Если мы вернемся через 20 секунд, процесс станет зомби…
Теперь все, что нужно сделать нашему поджнецу, — это wait
для любых потомков, и он может действовать как настоящий супервизор!
Единственное изменение, которое мы здесь внесли, — прослушивание сигналов SIGCHLD
и вызов функции Wait4
с -1
в первом аргументе, чтобы пожинать плоды любого завершившегося дочернего процесса. Вы можете прочитать о различных аргументах на странице man7 wait
. Теперь наш внук успешно пожинается!
Обратите внимание, что если бы вы написали это нетривиальным образом, вы, вероятно, хотели бы, чтобы цикл управлял SIGCHLD
сигналами от многих потомков, а не завершался после одного. Вероятно, вы также захотите поймать выход дочернего процесса в вашем цикле, а не через cmd.Run()
.
Заключение
Надеюсь, теперь вы хорошо понимаете, как работает модель Linux, когда речь идет о состояниях процессов, дереве процессов и сборе урожая. Если у вас есть какие-либо вопросы, дайте мне знать!