Автозапуск сервиса через systemd

Новые версии дистрибутивов Linux постепенно переходят на systemd. Так, Debian GNU/Linux, начиная с версии 8, начал использовать systemd по умолчанию. Systemd — это демон инициализации других демонов, пришедший на замену SysV. Одним из преимуществ systemd является интенсивное распараллеливание запуска служб в процессе загрузки операционной системы.
И хотя systemd поддерживает скрипты инициализации SysV, соответствующие Linux Standard Base (LSB), в этой статье мы попробуем настроить автозагрузку тестового сервиса именно средствами systemd.

Сперва убедимся, что операционная система действительно использует systemd:

ps -p 1 -o comm=

Если команда вернула строку «systemd», значит наши предположения верны.

Systemd оперирует специальными файлами конфигурации, которые называются юнитами. Каждый юнит отвечает за одну службу либо точку монтирования, либо подключаемое устройство, либо файл подкачки, и т.п. Обычно (если systemd запущен в режиме —system), юниты расположены по следующим адресам:

  • /etc/systemd/system/ — юниты, созданные системным администратором
  • /run/systemd/system/ — юниты, созданные в рантайме
  • /lib/systemd/system/ – юниты от установленных пакетов

Обрабатываются они в таком же порядке. Если юнит с одним и тем же названием присутствует в двух разных директориях, то будет использоваться тот, который встретился первым.

Юнит представляет собой текстовый файл, который может иметь несколько секций. Секции [Unit] и [Install] являются общими для всех типов юнитов. Кроме того, в зависимости от типа юнита, его файл конфигурации может дополнительно содержать секции [Service], [Socket], [Mount], [Automount], [Swap], [Path], [Timer], [Slice]. Пример:

[Unit]
Description=My Service

[Install]
WantedBy=multi-user.target

[Service]
ExecStart=/opt/myservice/myservice-daemon

Если через systemd идёт обращение к некой службе, но файл юнита для неё не найден, то systemd ищет сценарий инициализации SysV с тем же именем, но без суффикса .service, и динамически создаёт юнит службы на основе этого сценария. Это удобно для совместимости с SysV. Но нужно учитывать, что хотя совместимость и довольно полная, но не 100%. Подробнее см. здесь.

 

Cекция [Unit]

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

Параметры секции [Unit]

Description=

Текстовая строка в свободной форме, описывающая юнит.

Documentation=

URI-ссылки на документацию для юнита, разделенные пробелом.

Requires=

Задаёт зависимости от других юнитов, разделенные пробелом. Если хотя бы один из юнитов, описанных в этом параметре неактивен, то будет предпринята попытка его запуска, если же он деактивирован или не может быть запущен, то наш юнит тоже не будет активирован. Нужно иметь в виду, что если дополнительно не указать очередность запуска и остановки (см. параметры After и Before), то эти юниты будут запущены параллельно с нашим.

Requisite=

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

Wants=

Более слабая версия Requires. Отличие в том, что провал запуска зависимостей будет проигнорирован и не приведёт к провалу запуска нашего сервиса.

BindsTo=

Настраивает зависимости по аналогии с Requires, но предъявляет более жесткие требования к их состоянию. Если хотя бы один из юнитов, описанных в этом параметре, находится или перешёл в состояние неактивности, то наш сервис тоже будет остановлен.

PartOf=

Аналогичен Requires, но касается остановки и перезапуска юнитов. Когда systemd останавливает или перезапускает юниты, перечисленные в этом параметре, то действие распространяется и на наш сервис.

Conflicts=

Список юнитов, разделенных пробелом, который задаёт отрицательные зависимости. Запуск нашего сервиса будет останавливать все юниты, описанные в данном параметре, и наоборот.

Before=, After=

Список юнитов, разделенных пробелом. Задаёт порядок запуска/останова юнитов. Если мы укажем параметр After=mysql.service, то запуск нашего сервиса будет задержан до момента запуска mysql.service. Останов юнитов будет производиться в порядке, обратном запуску.

OnFailure=

Список юнитов, разделенных пробелом, которые активируются в случае, если наш сервис входит в состояние «failed».

PropagatesReloadTo=

Задаёт список юнитов, разделенных пробелом, которым, в случае получения запроса на перезагрузку конфигурации (reload) нашего сервиса, также будет передан запрос на перезагрузку их конфигураций (reload).

ReloadPropagatedFrom=

При перезагрузке конфигурации (reload) любого из юнитов в этом списке, наш сервис тоже получит аналогичный запрос.

JoinsNamespaceOf=

Позволяет поместить несколько юнитов, запускающих процессы (напр., службы), в одно пространство имён («песочницу»), что даст им возможность взаимодействовать между собой, как через сетевой интерфейс обратной петли, так и через файлы в каталогах /tmp и /var/tmp, при этом оставаясь изолированными от остальной системы.

RequiresMountsFor=

Список абсолютных путей, разделённых пробелом. Автоматически добавляет зависимости Requires и After для всех юнитов типа .mount, необходимых для доступа к указанным путям.
Точки монтирования, отмеченные noauto, не монтируются автоматически через local-fs.target, но они будут задействованы этим юнитом.

OnFailureJobMode=

Принимает одно из следующих значений: «fail», «replace», «replace-unversible», «isolate», «flush», «ignore-dependencies» или «ignore-requirements». По умолчанию «replace». Указывает каким образом юниты, перечисленные в OnFailure, будут поставлены в очередь на запуск. Если для этого параметра установлено значение «isolate», в OnFailure можно указать только один юнит.

IgnoreOnIsolate=

Принимает значение true или false. По умолчанию false. Если true, то наш юнит не будет остановлен в случае изоляции другого юнита.

StopWhenUnneeded=

Булевский тип. По умолчанию false. Если true, то юнит будет остановлен, когда больше никакие активные юниты не используют его.

RefuseManualStart=, RefuseManualStop=

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

AllowIsolate=

Булевский тип. По умолчанию false. Если true, то данный юнит можно использовать с командой systemctl isolate.

DefaultDependencies=

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

JobTimeoutSec=

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

JobTimeoutAction=

Опционально задаёт дополнительное действие, которое нужно выполнить при истечении таймаута JobTimeoutSec. Он принимает те же значения, что и StartLimitAction. По умолчанию «none».

JobTimeoutRebootArgument=

Здесь можно указать дополнительные аргументы, которые будут переданы при вызове функции reboot(2) по истечении таймаута.

StartLimitIntervalSec=, StartLimitBurst=

Настройки частоты запуска юнитов. По умолчанию, не разрешается запускать юнит чаще, чем 5 раз за 10 секунд. StartLimitIntervalSec задаёт интервал времени в секундах, а StartLimitBurst — максимально допустимое количество попыток запуска юнита в течение этого интервала.

StartLimitAction=

Задаёт действие, которое нужно выполнить в случае достижения лимита на запуск (см. StartLimitBurst). Принимает одно из следующих значений: none, reboot, reboot-force, reboot-immediate, poweroff, poweroff-force или poweroff-immediate. Если действие не задано, то достижение лимита на запуск не вызовет никаких действий, кроме того, что запуск юнита не будет разрешён.

RebootArgument=

Определяет необязательный аргумент для вызова reboot(2) в случае, если StartLimitAction или FailureAction имеют значение «reboot».

ConditionArchitecture=

Этот параметр используется для проверки архитектуры ОС. Он может принимать одно из значений: x86, x86-64, ppc, ppc-le, ppc64, ppc64-le, ia64, parisc, parisc64, s390, s390x, sparc, sparc64, mips, mips-le, mips64, mips64-le, alpha, arm, arm-be, arm64, arm64-be, sh, sh64, m86k, tilegx, cris. Архитектура определяется из информации, возвращаемой uname(2).

ConditionVirtualization=

Используется для проверки того, работает ли система в виртуализованной среде и, при необходимости, проверяет какая именно эта среда. Принимает либо логическое значение, если достаточно знать только виртуализована среда или нет; либо одно из следующих значений: qemu, kvm, zvm, vmware, microsoft, oracle, xen, bochs, uml, openvz, lxc, lxc-libvirt, systemd-nspawn, docker, rkt; либо значение private-users, соответствующее работе в пространстве имён пользователя. Если имеет место вложенная виртуализация, то определяется только самая внутренняя.

ConditionHost=

Проверяет имя хоста (hostname) либо идентификатор машины (/etc/machine-id).

ConditionKernelCommandLine=

Можно проверить установлена ли определённая опция командной строки ядра. Значение должно быть либо одним словом, либо двумя словами, разделёнными знаком «=».

ConditionSecurity=

Проверяет включен ли в системе указанный модуль безопасности. Принимает значения: selinux, apparmor, ima, smack, audit.

ConditionACPower=

Используется для проверки подключения системы к сети переменного тока. Если true, то условию выполнится, если хотя бы один разъём блока питания подключен к сети, либо не найдено ни одного разъёма питания; если false, то условие выполнится, если все разъёмы питания отсоединены от сети, и при этом в системе имеется хотя бы один разъём питания.

ConditionNeedsUpdate=

Принимает значение /var или /etc (можно с префиксом «!» для инвертирования условия). Этот параметр позволяет проверить требуется ли указанному каталогу обновление.

ConditionFirstBoot=

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

ConditionPathExists=

Условие существования файла. Проверяется перед запуском юнита. Если указанного абсолютного пути не существует, то условие не будет выполнено. Для инвертирования условия может быть использован префикс «!».

ConditionPathExistsGlob=

Аналогичен ConditionPathExists, но проверяет наличие хотя бы одного файла или каталога, соответствующего шаблону.

ConditionPathIsDirectory=

Проверяет существует ли указанный путь и является ли он каталогом.

ConditionPathIsSymbolicLink=

Проверяет существует ли указанный путь и является ли он символической ссылкой.

ConditionPathIsMountPoint=

Проверяет существует ли указанный путь и является ли он точкой монтирования.

ConditionPathIsReadWrite=

Проверяет существует ли указанный файл и доступен ли он для записи и чтения.

ConditionDirectoryNotEmpty=

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

ConditionFileNotEmpty=

Проверяет, что указанный файл существует и его размер больше нуля.

ConditionFileIsExecutable=

Проверяет, что указанный файл существует и является исполняемым.

AssertArchitecture=, AssertVirtualization=, AssertHost=, …

Эти параметры аналогичны ConditionArchitecture, ConditionVirtualization, ConditionHost, …, описанным выше, но имеют одно отличие — любой параметр, для которого не выполнится условие, приведёт к сбою запуска юнита. Используйте эти параметры для юнитов, которые не могут работать, когда определённые требования не выполняются.

[свернуть]

 

Cекция [Install]

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

Параметры секции [Install]

Alias=

Список псевдонимов юнита, разделенных пробелом. Имена, перечисленные здесь, должны иметь тот же суффикс (тип), что и имя файла юнита. Эта опция может быть указана более одного раза, в таком случае используются все перечисленные имена. Во время установки, systemctl enable создаст символические ссылки из этих имен на файл юнита. Обратите внимание, что не все типы юнитов могут иметь псевдонимы. Например, для типов .mount, .slice, .swap и .automount этот параметр не поддерживается.

WantedBy=, RequiredBy=

Каждый из этих параметров может использоваться более одного раза, или содержать список имён юнитов, разделенных пробелом. Символическая ссылка создается в каталоге .wants/ или .requires/ каждого из перечисленных юнитов, когда это устройство устанавливается с помощью systemctl enable. Это приводит к тому, что зависимость типа Wants или Requires добавляется от перечисленных юнитов к нашему. Основной результат состоит в том, что наш юнит будет запущен при запуске юнита из списка (см. описание Wants и Requires в разделе [Unit]). Например:

WantedBy=multi-user.target

означает, что наш сервис должен быть запущен, когда будет активирована цель multi-user.target, что эквивалентно уровню выполнения (runlevel) 3.

В systemd концепция уровней выполнения заменена так называемыми целями (targets). Цели – это точки синхронизации, при помощи которых сервер может переключать состояния. Сервисы и другие юниты можно привязывать к целям. Кроме того, система может использовать несколько целей одновременно.
Чтобы просмотреть доступные цели, введите:

systemctl list-unit-files --type=target

Чтобы просмотреть цель по умолчанию, которую systemd использует сразу после запуска (она в свою очередь запускает все юниты, которые являются частью её дерева зависимостей), введите:

systemctl get-default

Чтобы изменить цель по умолчанию, используйте опцию set-default:

sudo systemctl set-default multi-user.target

Чтобы посмотреть дерево зависимостей юнитов, например, от цели multi-user.target, введите:

systemctl list-dependencies multi-user.target

С помощью опции isolate можно переключать цели:

sudo systemctl isolate multi-user.target

Однако прежде чем это делать, убедитесь, что новая цель не отключит важнейшие сервисы.

Цели, как и юниты, могут зависеть друг от друга. Например, graphical.target будет ожидать запуска multi-user.target, прежде чем запустится логин-менеджер.

Ниже приведена таблица целей systemd, соответствующих уровням выполнения Linux:

Уровнень выполнения Цель systemd Описание
0 runlevel0.target, poweroff.target выполняются действия по выключению системы.
1 runlevel1.target, rescue.target однопользовательский режим (single user mode). Предназначен для различных административных действий по восстановлению системы. На этом уровне выполнения система полностью сконфигурирована, но не запущен ни один сервис, а из пользователей может работать только один root.
2 runlevel2.target не используется, но сконфигурирован как уровень выполнения 3.
3 runlevel3.target, multi-user.target многопользовательский режим (multiuser mode), нормальный режим работы сервера.
4 runlevel4.target в Slackware Linux используется для графического входа в систему, в RedHat и SuSE Linux не сконфигурирован.
5 runlevel5.target, graphical.target в Debian, RedHat и SuSE Linux используется для графического входа в систему, в Slackware Linux не сконфигурирован.
6 runlevel6.target, reboot.target выполняются действия по перезагрузке системы.

Also=

Дополнительные юниты для установки/деинсталляции при установке/деинсталляции нашего юнита, соответственно. Если пользователь запрашивает установку/деинсталляцию юнита с заданным параметром Also, то systemctl enable и systemctl disable автоматически установят/удалят юниты, перечисленные в этом списке.
Данный параметр может использоваться более одного раза, или может быть указан список юнитов, разделенных пробелом.

DefaultInstance=

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

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

<имя_службы>@<аргумент>.service

[свернуть]

 

Секция [Service]

Предназначена для юнитов типа .service. Количество различных параметров, которые может содержать данная секция, исчисляется десятками, поэтому здесь я опишу только некоторые из них. За подробностями прошу в документацию:

man systemd.service
man systemd.exec
man systemd.kill
man systemd.resource-control
Параметры секции [Service]

Type=

Тип запуска сервиса: simple, forking, oneshot, dbus, notify или idle.
Если simple (по умолчанию, если не указан ни Type, ни BusName, ни ExecStart), то предполагается, что процесс, указанный в ExecStart, является основным процессом данного сервиса.
Если forking, то предполагается, что процесс, указанный в ExecStart, вызовет fork() во время своего запуска. Ожидается, что родительский процесс завершится после окончания запуска и настройки каналов коммуникации, а дочерний процесс останется работать в качестве основного демона. Такое поведение характерно для традиционных демонов UNIX. Если используется данный тип запуска, то рекомендуется также использовать параметр PIDFile, чтобы systemd мог идентифицировать основной процесс демона. Systemd продолжит запуск следующих по очереди юнитов по завершении родительского процесса.
Поведение oneshot аналогично simple, но ожидается, что процесс должен завершиться до того, как systemd продолжит запуск следующих юнитов. RemainAfterExit особенно полезен для этого типа сервиса.
Описание остальных типов запуска см. в документации.

RemainAfterExit=

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

PIDFile=

Принимает полный путь к PID-файлу данного сервиса. Systemd будет считывать PID основного процесса демона после запуска сервиса и удалит этот файл после остановки сервиса.

ExecStart=

Команды с аргументами, которые выполняются при запуске сервиса. Если Type=forking не установлен, процесс, запущенный с помощью этой опции, будет считаться основным процессом демона.

ExecReload=

Команды, которые выполняются при перезагрузке конфигурации сервиса. Добавлена одна дополнительная переменная среды: $MAINPID. Если она установлена, то указывает на основной процесс демона и может использоваться в команде вида:

/bin/kill -HUP $MAINPID

Стоит иметь в виду, однако, что перезагрузка демона путём отправки сигнала (как в примере выше) обычно не является хорошим выбором, потому что это асинхронная операция и, следовательно, не подходит для упорядоченной перезагрузки нескольких сервисов. Рекомендуется указывать в ExecReload команду, которая не только запускает перезагрузку конфигурации демона, но и синхронно ждёт её завершения.

ExecStop=

Команды, выполняемые для остановки сервиса, запущенного через ExecStart. после выполнения команд, указанных в этой опции, все оставшиеся процессы сервиса завершаются в соответствии с настройкой KillMode. Если этот параметр не указан, то при запросе остановки сервиса, он останавливается путём отправки сигнала, указанного в KillSignal. Поддерживается подстановка переменных среды (включая $MAINPID, см. выше).
Обратите внимание, что обычно недостаточно указывать команду для этого параметра, которая только запрашивает завершение службы (например, путем отправки ей сигнала завершения), но не ждет действительного завершения. Поскольку оставшиеся процессы службы будут убиты в соответствии с KillMode и KillSignal, как описано выше, сразу после выполнения команды, что может не привести к чистой остановке. Следовательно, указываемая команда должна быть синхронной, а не асинхронной.
Следует знать, что команды, указанные в ExecStop, выполняются только если сервис был запущен. Они не вызывается, если служба никогда не запускалась вообще, или в случае неудачи ее запуска. То есть команды, указанные в ExecStop, выполняются с тем предположением, что сервис всё ещё полностью работоспособен и может корректно реагировать на все команды. Для случаев, когда сервис уже неработоспособен, используйте опцию ExecStopPost.

ExecStopPost=

Дополнительные команды, которые выполняются после остановки сервиса. Сюда относятся случаи, когда были выполнены команды, указанные в параметре ExecStop, когда у сервиса не указан ExecStop, или при внезапной остановке сервиса. В отличие от ExecStop, команды, указанные в данной опции, вызываются когда сервис не запускается корректно и снова останавливается.
Рекомендуется использовать данный параметр для операций очистки, которые должны выполняться даже если сервис не запускался корректно.
Обратите внимание, что все команды, указанные в этой опции, вызываются с кодом завершения службы, а также с кодом завершения и статусом основного процесса, установленными в переменных среды $SERVICE_RESULT, $EXIT_CODE и $EXIT_STATUS.

KillMode=

Определяет как будут уничтожены процессы данного юнита: control-group, process, mixed, none.
Если установлено значение control-group (по умолчанию), то все оставшиеся в контрольной группе процессы данного юнита будут убиты при его остановке (для служб: после выполнения команды останова, в соответствии с ExecStop).
При значении process уничтожается только сам основной процесс.
Если установлено mixed, то сигнал SIGTERM посылается основному процессу, а всем остальным процессам из контрольной группы данного юнита посылается сигнал SIGKILL.
Значение none указывает, что процесс не должен уничтожаться. В этом случае будут выполнены только команды останова, но принудительной терминации процесса не будет. Процессы, оставшиеся в живых после останова, останутся в контрольной группе, которая будет существовать, пока не пуста.

KillSignal=

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

WorkingDirectory=

Путь к рабочему каталогу службы относительно корневого, указанного в RootDirectory. Если значение не задано, используется корневой каталог, когда systemd запущен как —system, либо домашний каталог соответствующего пользователя, если systemd запущен как —user. Если установлено специальное значение «~», то используется домашний каталог пользователя, указанного в User. Если параметр имеет префикс «-», то отсутствующий рабочий каталог не считается фатальной ошибкой.

RootDirectory=

Устанавливает путь (через системный вызов chroot(2)) к корневому каталогу исполняемых процессов юнита относительно корня системы, на которой запущен диспетчер служб. В этом случае необходимо обеспечить доступность всех файлов сервиса в указанном chroot-окружении.

User=, Group=

Устанавливает для выполняемых процессов юнита системного пользователя и группу, соответственно. Принимает одно имя пользователя, группы или числовой идентификатор в качестве аргумента. Для системных служб (сервисов, выполняемых диспетчером системных служб, то есть управляемых PID 1) и для пользовательских сервисов пользователя root по умолчанию используется «root». Для пользовательских сервисов любого другого пользователя не разрешается изменение идентификатора пользователя, поэтому единственным допустимым параметром является тот же пользователь, от которого работает диспетчер служб пользователя. Если группа не установлена, используется группа пользователей по умолчанию.

Environment=

Список переменных, разделенных пробелом, который задаёт переменные среды для выполняемых процессов юнита.
Если для этой опции установлена пустая строка, список переменных среды сбрасывается. Расширение переменной не выполняется внутри строк, однако возможно расширение спецификатора. Символ $ не имеет особого значения. Если вам нужно присвоить значение, содержащее пробелы, используйте кавычки ("). Пример:

Environment="VAR1=word1 word2" VAR2=word3 "VAR3=$word 5 6"

Даёт три переменные «VAR1», «VAR2», «VAR3» со значениями «word1 word2», «word3», «$word 5 6».

OOMScoreAdjust=

Устанавливает уровень для убийцы процессов (Out-Of-Memory, OOM) из-за нехватки памяти. Принимает целое число от -1000 (чтобы отключить убийство OOM для этого процесса) до 1000 (чтобы сделать убийство этого процесса крайне желательным в случае нехватки памяти). Подробнее см. proc.txt.

TimeoutStartSec=

Настраивает время ожидания запуска. Если процесс демона не сообщает о завершении запуска в течение установленного времени, запуск сервиса будет считаться неудачным и снова будет погашен. Принимает значение в секундах без указания единицы измерения, либо значение вида «5min 20s». Установите «infinity» для отключения таймаута. По умолчанию берёт значение DefaultTimeoutStartSec из файла конфигурации менеджера, за исключением случая, когда используется Type=oneshot, тогда таймаут отключен по умолчанию.

TimeoutStopSec=

Таймаут ожидания останова. Если сервис получил запрос на останов, но не завершился за указанное время, он будет принудительно завершён через SIGTERM, а затем, после такого же таймаута, SIGKILL. Принимает значение по аналогии с TimeoutStartSec.

TimeoutSec=

Одновременно присваивает TimeoutStartSec и TimeoutStopSec указанное значение.

Restart=

Настраивает перезапуск сервиса при выходе из процесса сервиса, терминации или по достижении таймаута. Если смерть процесса является результатом работы systemd (например, остановки или перезапуска службы), то сервис не будет перезапущен.
Принимает одно из следующих значений: no, on-success, on-failure, on-abnormal, on-watchdog, on-abort или always. Значение no (по умолчанию) — не перезапускать; on-success — перезапускать только после чистого завершения процесса сервиса, то есть код выхода 0, или одного из сигналов SIGHUP, SIGINT, SIGTERM или SIGPIPE, а также состояний выхода и сигналов, указанных в SuccessExitStatus; on-failure — перезапуск в случае выхода из процесса с ненулевым кодом, по сигналу (кроме четырёх вышеупомянутых) или по таймауту (например, при service reload); on-abnormal — перезапуск в случае завершения процесса по сигналу (кроме четырёх вышеупомянутых) или по таймауту; on-abort — служба будет перезапущена только в том случае, если процесс завершается из-за неперехваченного сигнала, который не указан как статус чистого выхода; on-watchdog — служба будет перезапущена, только если истёк тайм-аут сторожевого таймера; always — сервис будет перезапущен в любом случае.

SuccessExitStatus=

Принимает список статусов выхода, которые при возврате основным сервисом будут считаться успешным завершением, в дополнение к нормальному успешному коду выхода 0 и сигналам SIGHUP, SIGINT, SIGTERM и SIGPIPE.
Определения статуса выхода могут быть либо числовыми кодами, либо именами терминального сигнала, разделенными пробелами. Например:

SuccessExitStatus = 1 2 8 SIGKILL

гарантирует, что коды выхода 1, 2, 8 и сигнал завершения SIGKILL считаются чистым завершением.

[свернуть]

 

Теперь, разобравшись со структурой и параметрами, мы можем дополнить файл конфигурации нашего юнита, который нужно поместить в каталог /etc/systemd/system/ под именем myservice.service:

[Unit]
Description=My Service

[Install]
WantedBy=multi-user.target

[Service]
Type=forking
PIDFile=/var/run/myservice.pid
WorkingDirectory=/opt/myservice
ExecStart=/opt/myservice/myservice-daemon
TimeoutSec=10
Restart=always

Проверим, увидела ли его система:

systemctl status myservice

Правильный ответ команды должен быть такой:

● myservice.service - My Service
   Loaded: loaded (/etc/systemd/system/myservice.service; disabled)
   Active: inactive (dead)

 

Приступаем к созданию демона, точнее его исполняемого файла. Ниже приведен листинг программы на Си, реализующий ключевые элементы демона, который раз в минуту сбрасывает в лог результат выполнения команды who(1).

Листинг демона

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int Daemon(void);
int writeLog(char msg[256]);
char* getCommand(char command[128]);
static char *log_file_name = "/opt/myservice/daemon.log";
static char *pid_file_name = "/var/run/myservice.pid";
static int pid_fd = -1;

char* getCommand(char command[128]) { // функция возвращает результат выполнения команды linux
    FILE *pCom;
    static char comText[256];
    bzero(comText, 256);
    char buf[64];
    pCom = popen(command, "r"); // выполняем команду
    if(pCom == NULL) {
        writeLog("Error Command");
        return "";
    }
    strcpy(comText, "");
    while(fgets(buf, 64, pCom) != NULL) { // читаем результат выполнения команды
        strcat(comText, buf);
    }
    pclose(pCom);
    return comText;
}

int writeLog(char msg[256]) { // функция записи строки в лог
    FILE * pLog;
    pLog = fopen(log_file_name, "a");
    if(pLog == NULL) {
        return 1;
    }
    char str[312];
    bzero(str, 312);
    strcat(str, "==========================\n");
    strcat(str, msg);
    strcat(str, "\n");
    fputs(str, pLog);
    fclose(pLog);
    return 0;
}

int main(int argc, char* argv[]) {
    writeLog("Daemon Start");

    pid_t parpid, sid;

    parpid = fork(); // создаём дочерний процесс
    if(parpid < 0) {
        exit(1);
    } else if(parpid != 0) {
        exit(0);
    }
    umask(0); // даём права на работу с ФС
    sid = setsid(); // генерируем уникальный индекс процесса
    if(sid < 0) {
        exit(1);
    }
    if((chdir("/")) < 0) { // выходим в корень ФС
        exit(1);
    }
    // закрываем доступ к стандартным потокам ввода-вывода
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // пробуем записать PID нашего демона в lock-файл
    if (pid_file_name != NULL) {
        char str[256];
        pid_fd = open(pid_file_name, O_RDWR|O_CREAT, 0644);
        if (pid_fd < 0) {
            exit(EXIT_FAILURE); // ошибка открытия lock-файла
        }
        if (lockf(pid_fd, F_TLOCK, 0) < 0) {
            exit(EXIT_FAILURE); // ошибка блокировки lock-файла
        }
        sprintf(str, "%d\n", getpid()); // получаем PID
        write(pid_fd, str, strlen(str)); // записываем PID в lock-файл
    }

    return Daemon();
}

int Daemon(void) { // бесконечный цикл демона
    char *log;
    while(1) {
        log = getCommand("who");
        if(strlen(log) > 5) { // если в системе есть активные пользователи, то пишем в лог
          writeLog(log);
        }
        sleep(60); // ждём 60 секунд до следующей итерации
    }
    return 0;
}

[свернуть]

Кладём этот код в файл /opt/myservice/myservice-daemon.c, устанавливаем компилятор gcc (если не был установлен) через apt-get install gcc, и компилируем:

gcc /opt/myservice/myservice-daemon.c -o /opt/myservice/myservice-daemon

В каталоге /opt/myservice должен появиться бинарник.

Настало время активировать наш сервис:

sudo systemctl enable myservice
Created symlink from /etc/systemd/system/multi-user.target.wants/myservice.service to /etc/systemd/system/myservice.service.

Проверяем:

systemctl status myservice
● myservice.service - My Service
   Loaded: loaded (/etc/systemd/system/myservice.service; enabled)
   Active: inactive (dead)

Запускаем сервис:

sudo systemctl start myservice

Ещё раз проверяем статус:

systemctl status myservice
● myservice.service - My Service
   Loaded: loaded (/etc/systemd/system/myservice.service; enabled)
   Active: active (running) since Fri 2017-07-07 11:03:19 MSK; 35s ago
  Process: 993 ExecStart=/opt/myservice/myservice-daemon (code=exited, status=0/SUCCESS)
 Main PID: 994 (myservice-daemo)
   CGroup: /system.slice/myservice.service
           └─994 /opt/myservice/myservice-daemon

Пробуем перезапустить сервис:

sudo systemctl restart myservice

Если всё нормально, то должен измениться PID. Смотрим:

systemctl status myservice
● myservice.service - My Service
   Loaded: loaded (/etc/systemd/system/myservice.service; enabled)
   Active: active (running) since Fri 2017-07-07 11:05:07 MSK; 2s ago
  Process: 1005 ExecStart=/opt/myservice/myservice-daemon (code=exited, status=0/SUCCESS)
 Main PID: 1006 (myservice-daemo)
   CGroup: /system.slice/myservice.service
           └─1006 /opt/myservice/myservice-daemon

Действительно изменился.

В файле конфигурации юнита мы указали Restart=always. Попробуем убить нашего демона и посмотрим, перезапустит ли его systemd:

sudo kill 1006

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

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

Если необходимо его отключить, то выполняем:

sudo systemctl stop myservice                 # останавливаем
sudo systemctl disable myservice              # отключаем
sudo rm /etc/systemd/system/myservice.service # удаляем файл юнита

На этом всё. Благодарю за внимание 🙂

Категория: System

Добавить комментарий