Сегодняшний пост доставлен вам письмом 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 в качестве первого аргумента.

  1. Родитель порождает и ждет ребенка (через cmd.Run()), затем спит
  2. Ребенок порождает внука, но не ждет (через cmd.Start())
  3. Внучатый процесс переходит в спящий режим

Следует отметить, что /proc/self/exe — это специальный файл, который ссылается на исполняемый в данный момент двоичный файл. Мы можем использовать ps для сравнения таблицы процессов с поведением повторного родителя по умолчанию (закомментировав строку subreaper.Set()) с установкой родителя в качестве поджатки:

Поведение повторного родителя по умолчанию

Здесь мы видим, что процесс /proc/self/exe grandchild был преобразован в процесс init. Если мы еще раз проверим позже, он был собран и больше не появляется в выводе ps.

Поведение вспомогательного жнеца при перевоспитании:

Вау! На этот раз, когда ребенок вышел, внук был переназначен поджнецу, а не инициализирован! Если мы вернемся через 20 секунд, процесс станет зомби…

Теперь все, что нужно сделать нашему поджнецу, — это wait для любых потомков, и он может действовать как настоящий супервизор!

Единственное изменение, которое мы здесь внесли, — прослушивание сигналов SIGCHLD и вызов функции Wait4 с -1 в первом аргументе, чтобы пожинать плоды любого завершившегося дочернего процесса. Вы можете прочитать о различных аргументах на странице man7 wait. Теперь наш внук успешно пожинается!

Обратите внимание, что если бы вы написали это нетривиальным образом, вы, вероятно, хотели бы, чтобы цикл управлял SIGCHLD сигналами от многих потомков, а не завершался после одного. Вероятно, вы также захотите поймать выход дочернего процесса в вашем цикле, а не через cmd.Run().

Заключение

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