После переезда на openSUSE 15.5 я столкнулся с некой странностью. Автозапускаемые после старта приложения стали падать при попытке перезапуска.

Тут надо сказать, что сама по себе идея само-перезапуска (по крайней мере, в мире POSIX) достаточно простая и примитивная. Стартуем новый процесс, копируя ему всё своё окружение (не забывая про открытые файлы) и завершаемся. PPid при этом сбрасывается в наш бывший родительский, и вот. Имеем новый процесс, выглядящий (за исключением pid, конечно) ровно так же как и предыдущий.

Подробнее в дебри я тут лезть не буду. Кому интересно глубже — гуглите реализацию "fork-exec". В linux "fork()" обозвали "clone()", но суть от этого не изменилась. Вот тут — побольше философии.

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

На этой технике ("respawn") работает в т.ч. обновление всякого десктопного софта. (Если вы конечно ему доверите такое вытворять у себя в системе.) Тот же клиент telegram. Ну и в своём xswitcher я тоже вкрутил подобную возможность.

И вот она-то наглухо "отвяла" после обновления ОС. Везде где была. Попытки дебага показывали что процесс запускается, начинает работать и затем молча дохнет. (Получая то ли sigterm, то ли sigkill. Я поленился встроить в xswitcher обработчик, за что и поплатился непонятками.)

Можно было бы ещё долго удивляться новой странной "магии", если бы не помог случай. Выпуская один хитрый сценарий для контроля топологии сети, решил "быть модным" и вместо всяких cron повесить его на systemd.timer. (Штука там достаточно автономная, так что текущий дизайн не ломает.)

И вот, "на голубом глазу" пишу в юнит-файле:

ExecStart=/bin/bash -c 'ERR=`/usr/local/sbin/xxx-check-topology.sh 2>&1` || (echo "$ERR" | /usr/bin/mail -s "xxx topology error(s) found" root)'

Раскидываю по местам. …И удивляюсь, что почты как-то меньше ожидаемого.

Как так? "Руками" запускаю — вот она, ругань. В зачищенном окружении (обычная проблема для башатины) — тоже. Начинаю копать и выясняю, что этот самый "mail" (который пакуют во всякие "mailutils", "mailx" и т.п.) работает, оказывается, в асинхронном режиме! (И имеет спец. ключ "-Ssendwait" на случай когда так не надо.)

Применительно к процессам "асинхронный режим" -- это, на мой взгляд, неправильная терминология. mail форкается и выходит, не дожидаясь завершения дочернего процесса.

Начинаю гуглить и вижу, что народ на это натыкается достаточно массово. По причине systemd.kill. Начиная с некоторой версии, systemd (безопасно/отказоустойчиво/инклюзивно/wtfElse) расстреливает всё содержимое cgroup при завершении запущенного им процесса. Однако, если процесс изначально стартовал как-то ещё и потом был "сброшен" на systemd-user, этот фокус не работает.

Поправьте, если я ошибаюсь. На примере KDE.
  • Вариант "systemd". Запустившийся "systemd --user" дёргает условный "kstart". Не так, там генератор от systemd. "Вон по тому списку." Тот, в свою очередь, форкает то что заказано. И самоустраняется. Запущенная софтина (kwin и что там поназаказывали в автозапуск) в итоге "повисает на шее" systemd, и тот регистрирует процесс у себя. А увидев, что упало — экологично киляет всех потомков.

  • Вариант "запустил из KDE". Когда я командую "запусти мне bash", происходит примерно так. Некий условный "kded" (не знаю, кто на самом деле в KDE запуском заведует) дёргает "kstart". Запускается окошко "konsole" (или ещё какой-нибудь терминал). Тот, в свою очередь, стартует bash. Из которого можно скомандовать запуск ещё чего-нибудь.

    В этом месте я продолжаю кое-что не понимать.

    # grep PPid /proc/$$/status
    PPid:   15589
    # grep PPid /proc/15589/status
    PPid:   2967

"bash порождён konsole, konsole порождён systemd (2967 — это он самый)."

  1. В моей картине мира, родителем должен был бы быть заказчик всего этого банкета (какой-нибудь резидентный процесс KDE).

  2. Но теперь можно респавниться как угодно. Systemd не обращает на шалости такого процесса никакого внимания.

…Вот такое сегодня получилось эссе. Проблему я подсветил, но пока не знаю, как правильно её купировать.

  • С одной стороны, можно просто "дать по рукам" systemd. Там есть (см. по ссылке выше) крутилка "не убивать". Но, глядя на перспективу, я так делать опасаюсь. Плод трудов известного сотрудника Micro$oft, конечно, не абсолютное зло. Но однозначно приучает к безалаберности. Больше можно не думать обо всех этих "демонизациях", логировании и прочей IT-гигиене. "Сделай тяп-ляп, а systemd за тобой подотрёт". В любой момент может оказаться, что писатель очередного ПО всю зачистку переложил на systemd.

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

На хабре достаточно людей с глубоким пониманием "кухни" ОС. Жду соображений "как правильно" в комментариях.

Всем удачи!

Комментарии (9)


  1. IZh
    02.12.2024 07:15

    А у вас какой тип сервиса указан в unit-файле? Есть разные варианты поведения сервиса, но, да, systemd должен понимать, в какой момент сервис завершился, чтобы подчистить за ним оставшееся окружение.


    1. PnDx Автор
      02.12.2024 07:15

      Вот такой шаблон:

      # systemctl cat user@1000.service
      …
      [Service]
      User=%i
      PAMName=systemd-user
      Type=notify
      ExecStart=/usr/lib/systemd/systemd --user
      Slice=user-%i.slice
      KillMode=mixed
      Delegate=pids memory
      TasksMax=infinity
      TimeoutStopSec=120s
      KeyringMode=inherit

      Type=notify


      1. IZh
        02.12.2024 07:15

        Это для пользовальской сессии. А приложение-то как запускается?


        1. PnDx Автор
          02.12.2024 07:15

          "Ты хитрый."©
          Чтобы более-менее внятно ответить на вопрос, надо размотать пол-системы.
          Вот так "в лоб" мы ничего не видим:

          # find / -type d -wholename '*/systemd/user/*'
          /etc/systemd/user/graphical-session-pre.target.wants
          /etc/systemd/user/sockets.target.wants
          /etc/systemd/user/pipewire.service.wants
          /etc/systemd/user/timers.target.wants
          /etc/systemd/user/basic.target.wants
          
          /usr/lib/systemd/user/sockets.target.wants
          /usr/lib/systemd/user/vte-spawn-.scope.d

          Но применив "магическое заклинание" "systemctl --user status" (из-под активного пользователя), видим куст поназапущенного.

          В частности:

                     │ ├─app-xswitcher@autostart.service
                     │ │ └─ 2108 /usr/local/bin/xswitcher

          Подробности:

          # systemctl --user status app-xswitcher@autostart.service
          ● app-xswitcher@autostart.service - xswitcher
               Loaded: loaded (/home/xxx/.config/autostart/xswitcher.desktop; generated)
               Active: active (running) since Mon 2024-12-02 21:53:04 MSK; 14h ago
                 Docs: man:systemd-xdg-autostart-generator(8)
             Main PID: 2108 (xswitcher)
               CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/app-xswitcher@autostart.service
                       └─ 2108 /usr/local/bin/xswitcher

          Видим, что некий "systemd-xdg-autostart-generator" взял шаблон из "autostart/xswitcher.desktop". И сгенерил по своему усмотрению вот такое:

          cat /run/user/1000/systemd/generator.late/app-xswitcher@autostart.service
          # Automatically generated by systemd-xdg-autostart-generator
          
          [Unit]
          Documentation=man:systemd-xdg-autostart-generator(8)
          SourcePath=/home/xxx/.config/autostart/xswitcher.desktop
          PartOf=graphical-session.target
          
          Description=xswitcher
          After=graphical-session.target
          
          [Service]
          Type=exec
          ExecStart=:/usr/local/bin/xswitcher # (":" suppresses variable expansion)
          Restart=no
          TimeoutSec=5s
          Slice=app.slice
          WorkingDirectory=-/home/xxx

          Исходный шаблон (с ним всё скучно):

          [Desktop Entry]
          Comment[ru_RU]=
          Comment=
          Exec=/usr/local/bin/xswitcher
          GenericName[ru_RU]=
          GenericName=
          Icon=system-run
          MimeType=
          Name[ru_RU]=xswitcher
          Name=xswitcher
          Path=/home/xxx
          StartupNotify=false
          Terminal=false
          TerminalOptions=\s
          Type=Application
          X-KDE-SubstituteUID=false
          X-KDE-Username=

          Таки да. Процесс (теперь) запускает именно systemd. Завернув в cgroup (подробности можно подглядеть в "/sys/fs/cgroup").


          1. IZh
            02.12.2024 07:15

            Просто по вашему исходному сообщению непонятно, вы сами написали unit-файл для вашего скрипта (я думал, что да, его и хотел увидеть) или же у вас какая-то автомагия конкретного дистрибутива Linux. Соответственно, чтобы понять, что и где не так, требовалось полное описание контекста.

            Да, в systemd есть шаблоны сервисов (как в примере с пользовательской сессией), позволяющие инстанциировать сессию, подставив UID пользователя (например, 1000). А есть и генераторы, которые при запуске на основе найденных конфигов (например, тех же старых init-скриптов) создают временные unit-файлы в /run.

            Вам надо поменять поведение системы в целом или для одного конкретного приложения/сервиса? Если для одного конкретного, то можно просто написать нужный unit-файл безо всяких шаблонов. Или пооверрайдить сгенерированный unit-файл своими параметрами. Если же для всех, можно попробовать пооверрайдить шаблон. Но я не советую так делать, ибо может поломаться совсем всё.


  1. shadowjack
    02.12.2024 07:15

    В linux "fork()" обозвали "clone()", но суть от этого не изменилась.

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

    Начинаю копать и выясняю, что этот самый "mail" (который пакуют во всякие "mailutils", "mailx" и т.п.) работает, оказывается, в асинхронном режиме!

    Применительно к процессам "асинхронный режим" -- это, на мой взгляд, неправильная терминология. mail форкается и выходит, не дожидаясь завершения дочернего процесса.

    но пока не знаю, как правильно её купировать.

    Вызывать в вашем юните mail с параметром -S sendwait. В чём проблема с KDE я не понял.

    Больше можно не думать обо всех этих "демонизациях", логировании и прочей IT-гигиене. "Сделай тяп-ляп, а systemd за тобой подотрёт". В любой момент может оказаться, что писатель очередного ПО всю зачистку переложил на systemd.

    Так ведь в этом и смысл systemd. Ваш юнит просто пишет свой вывод в stdout/stderr, это всё логгируется с помощью journald. Можно настроить юнит так, чтобы юнит перезапускался, если процесс завершается с ненулевым статусом. И т.д. и т.п., у systemd масса всякого функционала.

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

    Если вы пишете софт под Devuan или под какие-то очень старые системы на SysVinit -- тогда да, надо самому имплементировать всё это добро.


    1. PnDx Автор
      02.12.2024 07:15

      Спасибо, поправил статью по первым двум пунктам (добавил цитаты).

      Вызывать в вашем юните mail с параметром -S sendwait. В чём проблема с KDE я не понял.

      Чтобы включить "sendwait", нужно осознавать в чём тут фокус. Надеюсь что моя заметка сократит кому-нибудь время на дебаг. Проблема у меня на локалхосте (KDE) просто имеет ту же самую причину.

      Удержать в голове все "нюансы" systemd для меня, например, как для "ассоциатива" — нереально. Пролистываю мануалы по мере возникновения задач, пропуская мимо всё не связанное. Постепенно число связей растёт. Как-то так…
      И вот тут — да, отношусь к systemd с некоторым скепсисом, как неплохо информированный оптимист. Кое-что — вот прямо очень удобно. К примеру, контрольные группы по CPU/blkio или навешивание приоритетов для OOM. С другой стороны, полностью динамический порядок запуска — полный кошмар при отладке. Или вот прохождение квеста "э, а где мой dmesg?". На стыке ядра (буфера 2к бывает маловато) и journald.
      …Что касается "очень старых систем", то они никуда не девались. Просто в новом-модном это называется "initrd"/"initramfs" и туда мало кто лазает. Да, там теперь "из коробки" дирижирует systemd. Но (к счастью) оставлена возможность запуститься в шелл и провести необходимые (ремонтные) мероприятия. Вплоть до "восхода солнца вручную".


      1. shadowjack
        02.12.2024 07:15

        Я согласен с тем, что дебажить такого рода проблему как с форками mail в systemd совсем не тривиально. Я бы попробовал включить LogLevel=debug и посмотреть, есть ли какая-то информация о том, какие процессы systemd убивает при завершении юнита.


  1. Johan_Palych
    02.12.2024 07:15

    Давно не смотрел openSUSE. Поставил на виртуалку openSUSE Leap 15.6 с KDE Plasma(openSUSE-Leap-15.6-NET-x86_64-Media.iso)
    Долго тупил как включить sudo юзверю(помимо группы wheel и правки sudoers)
    Теперь надо ставить пакеты sudo-policy-wheel-auth-self system-group-wheel и возможно system-user-mail
    Еще интересный пакет systemd-status-mail - Send a mail if a systemd.timer fails and/or succeeds
    zypper se wheel mail
    Рекомендую почитать(лучше на English)
    https://wiki.archlinux.org/title/Systemd_(Русский)/Timers_(Русский)
    https://wiki.archlinux.org/title/Systemd/Timers
    https://wiki.archlinux.org/title/Systemd_(Русский)/User_(Русский)
    https://wiki.archlinux.org/title/Systemd/User

    openSUSE Leap 15.6 с KDE Plasma