Вот представьте, есть у вас некое приложение, если придумывать какую-то аналогию, не связанную с оригинальной задачей, то давайте представим что у нас есть некий GUI для конвертации видео. Пусть будет конвертер видео-DVD дисков в нормальный формат.
Дисклеймер: этот пример придуман из головы, но суть та же, триггернуло меня на рабочем проекте.
Ну так как мы люди умные, а есть люди ещё умнее, то мы будем пользовать блага что они сделали. GUI нарисуем сами, а конвертировать будем через ffmpeg. Т.е. пользователь выбирает диск, дорожку, сабы, звук, а мы вызываем под капотом бинарь ffmpeg с нужными параметрами.
И вот казалось бы, все рады, всё работает. Но представим, что по какой-то причине наш GUI упал. Плевать как - кривой программист написал всё на си, нас пристрелил ООМ, нам прислали kill -9
. Это всё не важно, нас пристрелили принудительно, не дав нам вызвать нужные деструкторы.
Что тогда происходит по умолчанию? Если мы успели запустить какой-нибудь рендеринг-конверт отдельным процессом - то ВНЕЗАПНО, этот процесс не умрёт. Его усыновит ближайший по проходу по дереву запусков процесс, у которого установлен флаг SUBREAPER. Обычно проверка доходит до init, если мы не запущены в каком-нибудь специфичном контейнере.
Ну нам же такое поведение не нужно. А если там рендеринг на 4 часа? Сидеть ждать пока дочешет? И вот выхожу я такой в интернет с этим вопросом и получаю два ответа, для windows и для linux. Для оффтопика существует специальный механизм, который пошлёт «смерть» дочерним процессам в случае смерти родителя:
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (см. подробности на MSDN)
Независимо ни от чего, все запущенные нами побочные процессы (ffmpeg, probe, и так далее) - грохнутся вместе с нами. У виндов, правда, особое отношение к терминации процесса, если у него нет главного окна (чтобы система виртуально нажала там «крестик») или если оно не запущено в терминале (чтобы система виртуально послала там «ctrl+c») - то процесс просто будет убит.
А вот под linux… Под linux все на stackoverflow наперебой орут, что такой механизм есть. И показывают:
if (prctl(PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) == -1) {
// ашипка
} // там еще проверка ppid, но для контекста это не важно
И вот тут начинаются приключения жопогорения. Потому что есть нюанс, про который никто не говорит. И который (судя по старым манам и всем русским манам, которые рисуются со старых) раньше не указывался:
PR_SET_PDEATHSIG (since Linux 2.1.57)
Set the parent-death signal of the calling process to arg2 (ei‐
ther a signal value in the range 1..maxsig, or 0 to clear).
This is the signal that the calling process will get when its
parent dies.
Warning: the "parent" in this case is considered to be the
thread that created this process. In other words, the signal
will be sent when that thread terminates (via, for example,
pthread_exit(3)), rather than after all of the threads in the
parent process terminate.
Ну вы поняли, да? Вы должны запускать все субпроцессы из главного треда приложения. Вы не можете создать какой-нибудь отдельный тред и там выполнить запустить что-то и забыть. Потому что ведро не может определить, вы в субтреде запустили процесс или нет. И по цепочке дойти до основного пида процесса и установить его там. Фигушки. Как только тред помрёт - все процессы, запущенные вами из этого треда будут прибиты. Ну то есть решили вы архитектурно организовать запуск рендеринга в одном треде, запуск приложений в другом (вы ж не бобо блокировать главный тред для этого), запустили отдельным тредом приложеньку, чекнули, что она форкнулась, всё там хорошо, внутри форка проставилм PDEATHSIG, завершили тред-запускалку… И получили прибитый ffmpeg. Ну разве это не прекрасно? И варианта у нас три:
- Все процессы, которые мы хотим запустить форкать из main-треда (ЩИТО?)
- Иметь какой-то отдельный тред, который будет получать какие-то инструкции для запуска и каждый раз запускать новый экземпляр ffmpeg через себя И НЕ УМИРАТЬ! ПОЖАЛУЙСТА, НЕ УМИРАЙ! #ТРЕДЖИВИ
- Не иметь отдельный тред для всех, но каждый раз для запуска создавать отдельный тред и держать его пока форкнутое приложение 100% не завершило работу.
И вроде бы по логике 3й пункт и ничего так, но он подходит далеко не всегда. Фиг с ним, что если мы запускаем 50 субсервисов - то нам надо будет держать 50 тредов, плевать. Иногда нам нафиг не нужно сидеть и ждать (waitpid), чо там с процессом. Ну для нашей DVD-риделки это еще может быть критично (ну там прогресс-бар нарисовать, постоянно читая ffmpeg или сразу сказать, что процесс сдох), а вот для некоторых других запусков - нет. Ну вот у меня в рабочем проекте я вообще не чекаю статус запущенного сервиса. Я с ним иногда по сокету общаюсь и если он подох туда ему и дорога - то я просто его заново запущу (когда он понадобится) и в логи стрельну, что такое было. Мне вообще плевать на его состояние после транзакции.
А разрабы ядра мне выбора не оставили. Или сиди и смотри на процесс в отдельном треде (и запускай сто тредов если запустил сто приложений. Как пример - какой-нибудь thumbnailer для файлового менеджера) или запускай процессы форкая main.
Горит. Немыслимо горит. Почему в windows сделано нормально, а тут вот такой цирк? (Про который еще и никто из индусов на SO не упоминает)
Выдохнул. Сабж. Какие еще есть варианты умирания без модификации child-программы? С модификацией любой дурак сможет - создал пайп в паренте, передал в child и сиди в child’e пырь в read. Пришёл 0 - делай роскомнадзор.