LINUX.ORG.RU

Отчего всё так плохо с отношением родитель-ребенок?

 ,


1

3

Вот представьте, есть у вас некое приложение, если придумывать какую-то аналогию, не связанную с оригинальной задачей, то давайте представим что у нас есть некий 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 - делай роскомнадзор.

★★★★★

гуй не юниксвеен, сри текстом в пайпы и не задавай глупых вопросов

anonymous
()

А ты можешь сформулировать решаемую задачу без всего этого потока сознания о том как не совершенен мир? В конечном итоге существуют программы типа redis, которые не портированы на оффтопик по техническим причинам

cobold ★★★★★
()
Ответ на: комментарий от cobold

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

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)

Ну так то всё логично, а вобще лучше не использовать сигналы в многопоточных программах. Сигналы - это из тех времен когда тредов не было.

sergej ★★★★★
()
Ответ на: комментарий от sergej

Кстати, а от того, что посылая сигнал процессу, ты не можешь знать какой тред его обработает у тебя не подгорает? :)

sergej ★★★★★
()
Ответ на: комментарий от sergej

Подгорает, но это можно 100% контролировать флагами.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А причем тут systemd если речь не про какой-то системный запуск программы, а про то что вы в своей программе временно запускаете другую.

PPP328 ★★★★★
() автор топика

Отчего всё так плохо с отношением родитель-ребенок?

Классиков читай. «Отцы и дети». До просветления. И не байти левыми заголовками :)

slackwarrior ★★★★★
()
Ответ на: комментарий от anonymous

Вы предлагаете каждую гуёвую пользовательскую программу добавлять в systemd? Мсье полон извращений

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от slackwarrior

Ну дак я ж не в толксы закинул. Контекст.

PPP328 ★★★★★
() автор топика

Проблема имеется, но вот твой пример никуда не годится.

Ну то есть решили вы архитектурно организовать запуск рендеринга в одном треде, запуск приложений в другом (вы ж не бобо блокировать главный тред для этого), запустили отдельным тредом приложеньку, чекнули, что она форкнулась, всё там хорошо, внутри форка проставилм PDEATHSIG, завершили тред-запускалку… И получили прибитый ffmpeg

Что это за бредятина?! Создавать тред чтобы вызвать из него форк? Причём тут какое-то блокирование? Форк никого не блокирует.

И вроде бы по логике 3й пункт и ничего так,

Нет, третий пункт это дальнейшее усугубление бреда. Из твоих вариантов самый хороший - первый, и минусов у него почти нет.

Какие еще есть варианты умирания без модификации child-программы?

Пустой unshare(), хотя это конечно тоже костыль.

А та штука с deathsig работать будет не всегда корректно, даже если не не будешь устраивать этот идиотизм с тредами.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

В чём проблема с вторым вариантом? Вполне адекватная практика — иметь некий поток, который выполняет инструкции из очереди. Закодить 10 минут.

  1. У вас выполнение операций из очереди не зависит от работы основного потока
  2. У вас всего один поток, который отвечает за все открытия ffmpeg. Получаете больше контроля над работой программы. Ещё немного подпилите — и сможете, например, контролировать количество запущенных дочек.
  3. Умрёт процесс — умрёт тред-обработчик очереди и все дочерние процессы также отвалятся.

Можете ещё эту логику инкапсулировать в какую-нибудь красивую обёртку, чтобы глаз не мозолило.

witaway
()
Последнее исправление: witaway (всего исправлений: 1)
Ответ на: комментарий от dataman

Дисклеймер не читай
@
Сразу отвечай

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от firkax

Причём тут какое-то блокирование? Форк никого не блокирует.

Форк - нет. Вот только если требование о создании форка возникло где-то в другом потоке (button pressed), то вам весь свой рабочий луп по обработке пула задач надо переносить в main. Из-за чего даже банальное закрытие окна будет работать не сразу, а пока main не прокрутит свой цикл внутри while (!terminated). Блокировка.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от witaway

В чём проблема с вторым вариантом? Вполне адекватная практика — иметь некий поток, который выполняет инструкции из очереди. Закодить 10 минут.

То что логика обработки в этом треде должна быть оверинжинеренная - он должен уметь всё следующее:

  • Иметь цикл забирания команд на запуск из очереди (то есть уже надо городить какой-то пул задач с механизмом «положил»\«забрал» и реализацией блокировок)
  • Форкаться и выставлять там все флаги
  • Иметь цикл слежения за всеми запущенными процессами (wait)
  • И всё это обернутое в цикл проверки на завершение процесса (пресловутый while !terminated)

При этом по terminated он должен уметь разослать всем запущенным процессам SIGTERM и присесть в ожидании пока все завершатся.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

Хватит писать ерунду. Ты походу кардинально не понимаешь идеологию запуска новых процессов в юниксах. Я даж не знаю что тебе объяснять, в нормальном случае просто никаких проблем с форками не возникает, ты их выдумал на ровном месте какими-то странными построениями.

firkax ★★★★★
()
Ответ на: комментарий от PPP328

ничего не нужно никуда добавлять, systemd сам это отслеживает

anonymous
()
Ответ на: комментарий от PPP328

У вас ведь в любом случае эта логика будет где-то рамазана по коды программы. Главное сделать как-нибудь грамотно и по SRP.

  • Цикл забирания команд из очереди получается достаточно простым. Оформите очередь как отдельную структуру и сделайте пару методов для работы с ними. В эти методы инкапсулируйте блокировку на основе мьютексов при добавлении/удалении/чтении. Задача вне зависимости от языка несложная.
  • Форк и выставление всех флагов тоже инкапсулируйте в какую-нибудь функцию. Тогда проблем с замусориванием кода у вас не будет.
  • Раз вам нужен ещё и кикл слежения за потомками… Скорее всего, нужен ещё один поток. Он будет ещё проще.
  • И зачем вам вообще нужен цикл проверки на завершение процесса? Добавьте в while своих циклов какой-нибудь && !terminated. Так, после завершения работы программы вы точно не создадите новых детей.
  • Чтобы дети не появились, если программа получила сигнал завершения, но уже после начала нового цикла — добавьте условие ещё и непосредственно перед вызовом форка.
  • Зачем детям рассылать SIGTERM? Их за вас убьёт ядро. Вы как раз для этого вызывали prctl.
  • Но если же вам при завершении работы нужно их не просто убить, но и выполнить с ними какую-то ещё работу — просто заведите некую функцию grateful_kill_children(...) и вызовите в обработчике сигнала. Она пусть уже проходится по списку детей и делает что хочет. В таком случае, правда, придётся этот список завести и поддерживать.

Поведение ядра, конечно, не слишком удобное и интуитивное, в чем я вас понимаю. Но нерешаемых проблем не вижу. Как по мне, это в любом случае самый простой и адекватный подход.

witaway
()
Ответ на: комментарий от PPP328

Видимо. Хотя у тебя и там были косяки, хоть и не сильно существенные, о которых я в первом же комменте написал.

Вобщем, fork ни с какими блокировками не связан, откуда ты их выдумал я не знаю. Если хочешь использовать deathsig - запускай прогу из того треда, которому она нужна, не вижу проблем.

firkax ★★★★★
()
Ответ на: комментарий от witaway

Но нерешаемых проблем не вижу

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

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

В кутях как раз легко все эти запуски дочерних процессов делать из главного потока, в нём же event loop крутится

Begemoth ★★★★★
()
Ответ на: комментарий от PPP328

Тут мне уже нечего сказать. Таков путь.

Честно, я пока не вижу, где тут нужда перекраивать всю программу. Для меня это пока выглядит как реализация +1 дополнительного модуля, который легко встраивается в любой код.

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

Может я не совсем понимаю всю ситуацию - но пока что не вижу необходимости всю программу перестраивать. Как я понимаю, у вас просто наболело

witaway
()
Последнее исправление: witaway (всего исправлений: 2)
Ответ на: комментарий от LongLiveUbuntu

А не на кутях - если есть event loop встраиваться в него, если нет - сделать отдельный поток для выполнения задач из очереди.

Begemoth ★★★★★
()
Ответ на: комментарий от Begemoth

А не работает оно. Поэкспериментировали с setsid, -1 не возвращает, но ничего не работает. getsid выдает левую фигню. И если глянуть в /proc//sessionid то во всех процессах (ubuntu 20.04) там стоит 4 294 967 295‬, в том числе у тех, у кого вызвали setsid

PPP328 ★★★★★
() автор топика

Все процессы, которые мы хотим запустить форкать из main-треда (ЩИТО?)

А в чем проблема? Тебе твои треды(«архитектурно организовать запуск рендеринга в одном треде, запуск приложений в другом») шлют в main-тред сигнал «запусти то-то и то-то», main-тред запускает и может быть отдает запрашиваеющему треду какую нибудь информацию вроде pid запущенного и в итоге main является родителем всех, да. Проблема по мне так, высосана из пальца.

Loki13 ★★★★★
()
Ответ на: комментарий от Loki13

А ничего, что майн тред во фреймворках часто отдан под гуй? Ну и да, портировать виндовую программу с запуском чего-то в фоне под линь уже нельзя - надо переписывать логику работы

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

А ничего, что майн тред во фреймворках часто отдан под гуй?

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

Loki13 ★★★★★
()

Все процессы, которые мы хотим запустить форкать из main-треда (ЩИТО?)

По-моему под linux так всегда и делалось. В том числе и в гуйне.

Почему в windows сделано нормально, а тут вот такой цирк? (Про который еще и никто из индусов на SO не упоминает)

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

vtVitus ★★★★★
()

Расширьте обзор. Можно запускать задачу в рамках отдельного systemd scope. Тогда при падении родителя все потомки будут убиты.

anonymous2 ★★★★★
()
Последнее исправление: anonymous2 (всего исправлений: 2)
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.