Как создать zombie процесс

Обновлено: 06.07.2024

Примерно год назад, когда Докер был в версии 0.6, мы первыми представили Baseimage-docker. Это минимальный образ Ubuntu, модифицированный специально для Докера. Люди могут пуллить этот базовый образ из Docker Registry и использовать его как основу для своих образов.

Однако, мы выяснили, что многие люди не понимают проблем, с которыми мы столкнулись. Да, это довольно низкоуровневые системные механизмы Unix, которые понятны далеко не всем. Поэтому в этом посте мы опишем самую главную проблему, которую мы решаем – PID 1 zombie reaping problem.

  1. Проблемы, которые мы решаем, актуальны для многих людей.
  2. Многие люди не знают об их существовании, поэтому в какой-то момент обязательно начинаются неожиданные неполадки (закон Мерфи).
  3. Будет очень неэффективно если каждый будет решать проблемы самостоятельно.

Сообществу нравится что мы делаем: наш образ третий по популярности в Docker Registry после официальных образов Ubuntu и CentOS.


The PID 1 problem: сбор зомби

Все процессы в Unix представлены в виде дерева. Каждый процесс порождает дочерние процессы, и каждый процесс имеет родителя кроме самого верхнего (или корневого).

Корневой процесс это init. Он запускается ядром при загрузке системы. init отвечает за старт остальных частей системы, например, демона SSH, демона Докера, запуск Apache/Nginx, запуск графического интерфейса и так далее. Каждый из них в свою очередь запускает свои дочерние процессы.


Ничего необычного. Но что происходит когда процесс завершается? Допустим, процесс bash (PID 5) был завершен. Он превращается в так называемый “defunct process”, также известный как “процесс зомби”.


Почему это происходит? Unix сделан таким образом, что родительский процесс ждет завершения дочернего чтобы получить код завершения (exit status). Зомби процесс существует до тех пор, пока родительский процесс не закончит это действие, используя семейство системных вызовов waitpid(). Вот цитата из man:

A child that terminates, but has not been waited for becomes a “zombie”. The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child.

Обычно люди считают зомби процессы какими-то сбежавшими процессами, вызывающими беспорядок. Но формально, с точки зрения операционной системы Unix, зомби процессы имеют четкое определение. Это процессы, которые завершились, но их родительские процессы еще ждут их завершения.

В большинстве случаев это не проблема. Системный вызов waitpid() для обработки зомби называют “reaping” (сбор, обработка). Многие приложения обрабатывают свои дочерние процессы корректно. В примере с sshd выше если bash завершается, то ОС пошлет сигнал SIGCHLD процессу sshd чтобы разбудить его. Sshd заметит это и обработает (“reaps”) дочерний процесс.


Но есть особый случай. Представьте себе, что родительский процесс завершился, намеренно или из-за действия пользователя. Что происходит с его дочерними процессами? У них больше нет родителя, поэтому они становятся “сиротами” (это технический термин).

Тут в игру вступает процесс init. У процесса init – PID 1 – есть специальная задача: “усыновлять” осиротевшие процессы (это снова настоящий технический термин). Это означает, что init становится родителем таких процессов, не смотря на то, что они в реальности не были порождены init’ом.

Рассмотрим пример с Nginx, который демонизируется по-умолчанию. Он работает следующим образом: сначала Nginx создает дочерний процесс. Потом основной процесс Nginx завершается. Теперь дочерний процесс Nginx усыновлен init’ом.


Ядро ОС ожидает от init специального поведения: ядро считает, что init должен обрабатывать (собирать, “reap”) усыновленные процессы тоже.

Это очень важная функция в Unix. Она настолько фундаментальна, что многие программы рассчитаны на ее корректную работу. Большинство демонов рассчитано на то, что демонизированные процессы будут усыновлены и обработаны (то есть корректно завершены после превращения в зомби) init’ом.

Я использую демоны в качестве примера, но этот механизм распространяется не только на них. Каждый раз когда процесс, имеющий детей, завершается, он ожидает, что init подчистит все за ним. Это описано детально в двух очень хороших книгах: Operating System Concepts и Advanced Programming in the UNIX Environment.

Почему процессы зомби вредны

Почему зомби-процессы вредны, не смотря на то, что они всего лишь завершенные процессы? Ведь наверняка память, выделенная процессу уже освобождена, и зомби это всего лишь строка в ps?

Да, память этого процесса уже освобождена. Но тот факт, что процесс еще виден в ps означает, что он использует ресурсы ядра. Вот цитата из man по waitpid:

As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes.

До тех пор пока zombie не удален из системы с помощью wait, он будет использовать слот в таблице процессов ядра, и если эта таблица заполнится, создание новых процессов будет невозможно

Причем тут Докер

Причем же тут Докер? Многие люди запускают только один процесс в своем контейнере. Но скорее всего этот процесс не ведет себя как правильный init. То есть вместо корректной обработки усыновленных процессов, он считает, что другой init процесс должен делать это. И считает так совершенно справедливо.

Давайте рассмотрим конкретный пример. Допустим, ваш контейнер содержит веб-сервер, в котором крутится CGI-скрипт, написанный на bash. Скрипт вызывает grep. Потом веб-сервер решает, что скрипт обрабатывается слишком долго и убивает его. Но grep остается запущенным. Когда он заканчивает свою работу, он превращается в зомби и усыновляется процессом PID 1 (веб-сервером). Веб-сервер не знает ничего про grep, поэтому не обрабатывает его завершение и зомби-grep остается в системе.

Проблема применима и к другим ситуациям. Многие создают контейнеры для сторонних приложений, например, PostgreSQL, и запускают эти приложения как единственный процесс внутри контейнера. Когда вы запускаете чужой код, уверены ли вы что он не порождает дочерние процессы, которые потом превратятся в зомби? Если вы запускаете свой код и точно знаете, что он и используемые им библиотеки делают, то все хорошо. Но в общем случае необходимо запускать правильный init для решения проблем.

Но разве запуск полного системного init не превращает контейнер в тяжелую штуку вроде виртуальной машины?

Система init не обязательно тяжелая. Возможно, вы думаете про Upstart, Systemd, SysV и так далее. Возможно, вам кажется, что внутри контейнера нужно запустить целую систему. Это не так. “Полная система init” не обязательна и не нужна.

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

Простая init система

Возможно, есть готовые решения? Почти. Старый добрый bash. Bash обрабатывает усыновленные процессы. Bash может запустить что угодно. Так что вместо такой строчки в Dockerfile…

(директива -e запрещает bash’у распознавать скрипт как простую команду и exec()’ать его напрямую).

В итоге получится такая иерархия процессов:


Но, к сожалению, у этого подхода есть проблема. Он не обрабатывает сигналы! Допустим, вы используете kill чтобы послать сигнал SIGTERM процессу bash. Bash завершается, но не посылает SIGTERM своим дочерним процессам!


Когда bash завершается, ядро завершает весь контейнер со всеми процессами внутри. Эти процессы завершаются с помощью SIGKILL. Поэтому нет способа завершить эти процессы чисто. Допустим, ваше приложение пишет что-то в файл. Файл может быть поврежден если приложение завершилось таким образом во время записи. Нечистое завершение процессов это плохо. Это почти как выдернуть шнур питания у сервера.

Но почему нас должно волновать, что процесс init завершается сигналом SIGTERM? Потому что docker stop посылает SIGTERM процессу init. “docker stop” должен остановить контейнер правильно, чтобы его можно было потом запустить с помощью “docker start”.

Эксперты по bash наверняка захотят написать нормальный обработчик EXIT, который посылает сигналы своим детям, вроде такого:


К сожалению, это не решает проблемы. Посылать сигналы дочерним процессам недостаточно. init также должен ожидать завершения дочерних процессов перед тем, как завершаться самому. Если init завершится раньше, то все дочерние процессы будут убиты (не чисто) ядром.

Очевидно, требуется чуть более сложное решение, но полная система init с Upstart, Systemd и SysV это слишком жирно для легковесного докер-контейнера. К счастью, Baseimage-docker содержит решение. Мы написали свою, легкую систему init специально для использования внутри докер-контейнера. Не придумав ничего лучше, мы назвали ее my_init. Это программа на Питоне в 350 строк.

  • Обработает (reap) дочерние процессов
  • Запускает подпроцессы
  • Ожидает завершения всех подпроцессов перед собственным завершением, с максимальным таймаутом
  • Записывает активность в “docker logs”
Решит ли Докер эту проблему сам?

В идеале, проблема с PID 1 должна решаться нативно самим Докером. Было бы здорово, но пока, в январе 2015 года, мы не слышали ничего подобного от команды Докера. Это не критика – Докер очень амбициозен, и я уверен, что у их команды есть проблемы поважнее. Проблема PID 1 легко решается на пользовательском уровне. Так что пока Докер не решит эту проблему официально, мы рекомендуем людям решать ее самим, используя систему вроде той, что описана выше.

Проблема ли это вообще?

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

Не забывайте про закон Мерфи.

Кроме того, что зомби забивают таблицу ресурсов ядра, они также могут мешать корректной работе программ, которые проверяют наличие процессов. Например, Phusion Passenger управляет процессами. Он перезапускает процессы при их падении. Он парсит вывод ps и отправляет сигнал 0 процессу. Зомби виден в ps и реагирует на сигнал 0, так что Phusion Passenger думает, что процесс все еще жив.

Все, что нужно чтобы обезопасить себя от проблемы с зомби, это потратить 5 минут на подключение Baseimage-docker или на импорт 350 строк my_init. Дополнительные затраты на диск и память минимальны: в память добавляется лишь пара мегабайт.

Заключение

Проблема PID 1 – реальна. Один из способов ее решения – использовать Baseimage-docker. Единственный ли это путь? Конечно, нет. Цели Baseimage-docker это:

  1. Рассказать людям о нескольких важных моментах при работы с Докер-контейнерами.
  2. Предоставить готовое решение чтобы люди не изобретали велосипед.

При этом возможны несколько решений, главное чтобы они справлялись с описанной задачей. Можете написать свой вариант на C, Go, Ruby или чем-то еще.

Возможно, вы не хотите использовать базовый образ Ubuntu. Может, вы используете CentOS. Но Baseimage-docker все равно может быть вам полезен. Например, проект ourpassenger_rpm_automation использует контейнеры CentOS. Мы просто извлекли my_init и вставили его туда.

Create zombie process

I am interested in creating a zombie process. To my understanding, zombie process happens when the parent process exits before the children process. However, I tried to recreate the zombie process using the following code:

However, this code exits right after execute which is expected. However, as I do

I found a.out is just running as a normal process, rather than a zombie process as I expected.

The OS I am using is ubuntu 14.04 64 bit


3,619 2 2 gold badges 35 35 silver badges 43 43 bronze badges 575 3 3 gold badges 5 5 silver badges 12 12 bronze badges

Что означает "There is 1 zombie process" и что с ним делать?

Насколько я понял, это означает, что какой-то процесс завершил работу, но не закрылся и не освободил ресурсы. Хотелось бы лучше понять проблему:

  • Как можно его обнаружить? Сейчас я угадал, но нужен стабильный метод.
  • Как можно узнать причины его появления? Есть какие-нибудь логи? Может, по имеющемуся процессу можно что-либо понять?

Если это важно, ubuntu 14.04, sudo есть.

Дополнение: меня не устраивает метод «просто подождать», т.к. есть сервис, который должен работать всегда — и подвис он или кто-то из его детей. Он подстрахован monit'ом, но тот не распознал зомбификацию и не перезагрузил.


31.8k 21 21 золотой знак 118 118 серебряных знаков 205 205 бронзовых знаков Как можно узнать причины его появления? - родитель его (еще) не рипнул, возможно, рипнет позднее. При завершении родителя все дочерние процессы будут усыновлены родительским процессом родителя, где, скорее всего, всё и разрулится. Сам зомби процесс занимает, насколько помню, всего ничего (буквально саму запись в таблице), поэтому пока нет прогрессии, все должно быть в порядке. всё и разрулится — все возникавшие в моей практике zombie-процессы (а случались они очень редко и было это очень давно) продолжали существовать до перезагрузки системы. Замби - плод ошибки в родителе, он просто забыл про ребенка. Если родитель сильно занят и не реагирует на сигналы, то он может просто вовремя не забрать код завершения. так что на очень короткое время ps может и в полностью исправной системе. Но выход убрать зомби только один - убить родителя. Так что придется учить монит понимать зомби, узнавать родителей . @alexanderbarakin родительский процесс-то прибивался?

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

Данная команда покажет все зомби процессы и их родителей (тестировалась под linux, под другими *nix возможны другие ключи у команды ps).

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

Что бы понять почему именно появился зомби надо смотреть исходники породившего его процесса и возможно самого зомби. Часто это одна и та же программа. Любой процесс, выполняющий fork , т.е. запускающий дочерние процессы должен уметь забирать код их завершения, делается это вызовом wait и/или waitpid . Для начала надо просмотреть исходники на наличие функций группы wait, а так же наличие функции-обработчика сигнала SIGCHLD (обработчик устанавливается вызовом signal и функций для работы с сигналами потоков). Если родитель не использует функции группы wait, то возможно при его нормальной работе завершение дочерних процессов разработчик вообще не ожидал. Если это так - то причина образования зомби - незапланированное завершение процесса потомка в следствие какой либо ошибки. Дать рекомендаций как это искать и лечить невозможно. Можно только посоветовать добавить в родительский поток обработку CHLD, забор кода завершения с помощью wait и логирование факта завершения потомка. И дальше запуск потомка под отладчик и т.п. .

Вариант 2: родительский процесс использует wait , но зомби все равно появляются. Копать в сторону того, какая разновидность wait используется, если waitpid, которая проверяет завершение конкретных потомков, то смотреть откуда она берет проверяемые pid, возможно в процессе работы какие то pid потомков теряются и программа про них забывает. Может программа в какой то момент запрещает обработку сигналов и забывает восстановить обработку, после прохождения критического участка. Опять же - вариантов очень много, но сосредоточены они вокруг обработчика SIGCHLD и функций wait .

Вариант 3: родительский процесс умеет обрабатывать и готов правильно обработать завершение своих потомков. Но зацикливается в другом месте программы или засыпает на системном вызове, например чтения с сетевого диска, который стал недоступен и при этом прерывание по SIGCHLD запрещено. В этом случае надо разбираться с причинами его зависания. Кстати, отсутствие доступа к каким либо ресурсам, типа сетевых дисков (или при выходе из строя физического диска) - довольно частая причина массового появления зомби.

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

3.4.3. Процессы-зомби

Если дочерний процесс завершается в то время, когда родительский процесс заблокирован функцией wait(), он успешно удаляется и его код завершения передается предку через функцию wait(). Но что произойдет, если потомок завершился, а родительский процесс так и не вызвал функцию wait()? Дочерний процесс просто исчезнет? Нет, ведь в этом случае информация о его завершении (было ли оно аварийным или нет и каков код завершения) пропадет. Вместо этого дочерний процесс становится процессом-зомби.

Зомби — это процесс, который завершился, но не был удален. Удаление зомби возлагается на родительский процесс. Функция wait() тоже это делает, поэтому перед ее вызовом не нужно проверять, продолжает ли выполняться требуемый дочерний процесс. Предположим, к примеру, что программа создает дочерний процесс, выполняет нужные вычисления и затем вызывает функцию wait(). Если к тому времени дочерний процесс еще не завершился, функция wait() заблокирует программу. В противном случае процесс на некоторое время превратится в зомби. Тогда функция wait() извлечет код его завершения, система удалит процесс и функция немедленно завершится.

Что же всё-таки случится, если родительский процесс не удалит своих потомков? Они останутся в системе в виде зомби. Программа, показанная в листинге 3.6, порождает дочерний процесс, который немедленно завершается, тогда как родительский процесс берет минутную паузу, после чего тоже заканчивает работу, так и не позаботившись об удалении потомка.

Листинг 3.6. (zombie.c) Создание процесса-зомби

/* Создание дочернего процесса. */

if (child_pid > 0)

/* Это родительский процесс — делаем минутную паузу. */

/* Это дочерний процесс — немедленно завершаем работу. */

Скомпилируйте этот файл и запустите программу. Пока программа работает, перейдите в другое окно и просмотрите список процессов с помощью следующей команды:

% ps -е -o pid,ppid,stat,cmd

Эта команда отображает идентификатор самого процесса и его предка, а также статус процесса и его командную строку. Обратите внимание на присутствие двух процессов с именем zombie. Один из них — предок, другой — потомок. У последнего идентификатор родительского процесса равен идентификатору основного процесса zombie, при этом потомок обозначен как <defunct> (несуществующий), а его код состояния равен Z (т.е. zombie — зомби).

Итак, мы хотим узнать, что будет, когда программа zombie завершится, не вызвав функцию wait(). Останется ли процесс-зомби? Нет — выполните команду ps и убедитесь в этом: оба процесса zombie исчезли. Дело в том, что после завершения программы управление ее дочерними процессами принимает на себя специальный процесс — демон init, который всегда работает, имея идентификатор 1 (это первый процесс, запускаемый при загрузке Linux). Демон init автоматически удаляет все унаследованные им дочерние процессы-зомби.

Процессы

Процессы Создание параллельных процессов настолько полно описано в литературе по UNIX, что здесь мы приведем лишь минимально необходимый беглый обзор, останавливаясь только на отличительных особенностях ОС QNX.Всякое рассмотрение предполагает наличие системы понятий.

Процессы в MI

Процессы в MI Процесс в MI — это системный объект, называемый пространством управления процессом. Обратите внимание, что эквивалентного объекта OS/400 нет. (Мы еще поговорим об этом в разделах, посвященных управлению работами). Задача процесса в MI — связать воедино ресурсы,

Процессы

Процессы Процессы в операционной системе UNIX играют ключевую роль. От оптимальной настройки подсистемы управления процессами и числа одновременно выполняющихся процессов зависит загрузка ресурсов процессора, что в свою очередь имеет непосредственное влияние на

Процессы

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

1.6 Процессы

1.6 Процессы Наряду с файлом, понятие процесса является важнейшим в концепции открытых операционных систем.Процесс – это обладающая уникальным идентификатором единица исполняемого кода35 в памяти.Подавая простую команду из оболочки, оператор дает ОС указание запустить

12.3.3.4 Фиктивные процессы

12.3.3.4 Фиктивные процессы Когда ядро выполняет переключение контекста в однопроцессорной системе, оно функционирует в контексте процесса, уступающего управление (см. главу 6). Если в системе нет процессов, готовых к запуску, ядро переходит в состояние простоя в контексте

3.4. Процессы

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

5 Процессы

5 Процессы 5.1. Системные вызовы fork() и ехес() Процесс в Linux (как и в UNIX) — это программа, которая выполняется в отдельном виртуальном адресном пространстве. Когда пользователь регистрируется в системе, под него автоматически создается процесс, в котором выполняется оболочка

Зомби? Нет – перерождение

Зомби? Нет – перерождение После того как домен удален из реестра доменных имен, регистрация такого же домена оказывается доступна любому другому лицу; в стандартных случаях у прежнего администратора нет никаких приоритетов. Правда, подобные приоритеты могут возникнуть

1.6 Процессы

1.6 Процессы Наряду с файлом, понятие процесса является важнейшим в концепции открытых операционных систем.Процесс — это обладающая уникальным идентификатором единица исполняемого кода[35] в памяти.Подавая простую команду из оболочки, оператор дает ОС указание запустить

3.1.3. Взаимодействующие процессы

3.1.3. Взаимодействующие процессы В случае Unix малозатратное создание дочерних процессов (Process-Spawning) и простое межпроцессное взаимодействие (Inter-Process Communication — IPC) делают возможным использование целой системы небольших инструментов, каналов и фильтров. Данная система будет

7.2.5. Подчиненные процессы

7.2.5. Подчиненные процессы Иногда дочерние программы интерактивно принимают и возвращают данные вызвавшим их программам через каналы, связанные со стандартным выводом и вводом. В отличие от простых вызовов с созданием подоболочки и конструкций, которые выше были названы

Глава 3 Процессы

Глава 3 Процессы Выполняющийся экземпляр программы называется процессом. Если на экране отображаются два терминальных окна, то, скорее всего, одна и та же терминальная программа запущена дважды — ей просто соответствуют два процесса. В каждом окне, очевидно, работает

2 Answers 2

To my understanding, zombie process happens when the parent process exits before the children process.

This is wrong. According to man 2 wait (see NOTES) :

A child that terminates, but has not been waited for becomes a "zombie".

So, if you want to create a zombie process, after the fork(2) , the child-process should exit() , and the parent-process should sleep() before exiting, giving you time to observe the output of ps(1) .

For instance, you can use the code below instead of yours, and use ps(1) while sleep() ing:


3,619 2 2 gold badges 35 35 silver badges 43 43 bronze badges Thank you so much chrk! I tried your code, and I saw some zombie processes. But I still have one more concern. Why my code does not create a zombie process? According to the definition in your post, does the child process exited without being waited for by the parent (The parent had been exited without wait command). @chrk

A zombie or a "defunct process" in Linux is a process that has been completed, but its entry still remains in the process table due to lack of correspondence between the parent and child processes. Usually, a parent process keeps a check on the status of its child processes through the wait() function. When the child process has finished, the wait function signals the parent to completely exit the process from the memory. However, if the parent fails to call the wait function for any of its children, the child process remains alive in the system as a dead or zombie process. These zombie processes might accumulate, in large numbers, on your system and affect its performance.

Below is a c program to creating a Zombie-Process on our system Save this file as zombie.c:

The zombie process created through this code will run for 60 seconds. You can increase the time duration by specifying a time(in seconds) in the sleep() function.

Compile this program

Now run the zombie program:

The ps command will now also show this defunct process, open a new terminal and use the below command to check the defunct process:

Читайте также: