Как найти zombie процесс

Обновлено: 05.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 и вставили его туда.

Apr 27, 2017 16:22 · 405 words · 2 minute read zombie defunct

Дочерний процесс в Unix-системе, завершивший своё выполнение, но ещё присутствующий в списке процессов операционной системы, чтобы дать родительскому процессу считать код завершения называют зомби (англ. zombie process, defunct process) — говорит нам Википедия.

Подробнее ознакомиться с возникновением зомби-процессов и связанными с ними проблемами можно на той же Wiki, а мы разберемся как вычислить такие процессы и “убить” их!

Найти zombie -процесс можно несколькими способами, например:

Итак, находим zombie -процессы в нашей системе:

“Убить” найденные zombie-процессы просто так не получится. Самый правильный вариант — найти родительский процесс и убить или перезапустить его. (Еще может помочь перезагрузка сервера, но это точно не наш путь). Находим родительские процессы (их PID’ы в третьей колонке):

При желании можно узнать больше подробностей о родительском процессе, например так:

Ищем и убиваем zombie процессы

Ищем и убиваем zombie процессы

Что же это такое?
Это дочерний процесс в Unix-системе, завершивший своё выполнение, но ещё присутствующий в списке процессов операционной системы, чтобы дать родительскому процессу считать код завершения. Процесс при завершении освобождает все свои ресурсы (за исключением PID — идентификатора процесса) и становится «зомби» — пустой записью в таблице процессов, хранящей код завершения для родительского процесса.
Система уведомляет родительский процесс о завершении дочернего с помощью сигнала SIGCHLD. Предполагается, что после получения SIGCHLD он считает код возврата с помощью системного вызова wait(), после чего запись зомби будет удалена из списка процессов. Если родительский процесс игнорирует SIGCHLD (а он игнорируется по умолчанию), то зомби остаются до его завершения.

А теперь возникают вопросы: как же всё-таки их найти и убить? Найти их очень просто. Вот несколько вариантов:

1)
top | grep zombie
225 processes: 1 running, 222 sleeping, 2 zombie

2)
ps aux | grep -w Z
root 3994 0,0 0,0 0 0 ?? Z 13июн11 16:23,02 <defunct>
root 3995 0,0 0,0 0 0 ?? Z 13июн11 13:43,28 <defunct>

3)
ps -alx | awk '$10

Находим родительский процесс:

ps ajx | grep -w Z
root 3994 3992 3992 3992 0 Z ?? 16:23,02 <defunct>
root 3995 3992 3992 3992 0 Z ?? 13:43,28 <defunct>

3-я колонка как раз и показывает pid родительского процесса. Смотрим, что это за процесс:

ps auxww | grep 3992
root 3992 0,0 0,2 30664 9872 ?? Ss 13июн11 0:08,21 [exilog_agent] (perl5.12.3)

Как посмотреть зомби в операционной системе Linux

Это можно сделать при помощи утилиты ps:

$ ps aux | grep defunct

ps

Как обнаружить в системе «родителя»:

$ ps -xal | grep defunct

Ищем родителя

После того, как нашли родителя можно удалить зомби. Это единственный способ уничтожения зомби процесса. Прямые команды не удалят их. Найдите в колонке идентификатор «родителя». В приведенном примере он находится в 4-м столбце под номером 761.

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

В вашем случае вместо 761 будет стоять другое значение. Его нужно правильно обнаружить.

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

Зомби процессы в Linux

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

Функция fork запускает дочерний процесс, который попадает под контроль родительского процесса. Если родительский процесс получает сигнал о завершении дочернего процесса, но при этом игнорирует его (в возможные причины сбоя вдаваться не станем), то этот процесс становится зомби.

Стоит ли обращать на них своё внимание? Ответ: делать это совершенно не обязательно. Так как процесс уже был завершен, он перестал использовать системные ресурсы, но остался в системе. Самое разумное в этом случае – сделать вид, что его не существует, и продолжать работать с устройством, как ни в чем не бывало. Если пользователь попытается «уничтожить» зомби при помощи специальной команды «kill», у него ничего не выйдет, ведь этот процесс не выполняется. Это действие является бессмысленным, а потому процесс продолжит отображаться в системе.

Чтобы полностью убрать зомби, необходимо уничтожить или перезапустить его родительский процесс. Если есть такое желание, обратитесь к инструкции ниже.

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