Основы написания rc.d скриптов в BSD

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

1 Вступление
2 Обозначение задачи
3 Простейший скрипт
4 Настраиваемый простейший скрипт
5 Запуск и остановка простого демона
6 Запуск более сложного демона
7 Присоединение rc.d скрипта к инфраструктуре
8 Создание более гибких rc.d скриптов

1 Вступление

Когда-то BSD имела монолитный скрипт запуска /etc/rc. Его вызывал init(8) во время загрузки системы, и этот скрипт выполнял все задачи в пространстве пользователя (userspace), необходимые для многопользовательских операций: проверку и монтирование файловых систем, настройку сети, запуск демонов и тому подобное. Но список таких задач не одинаков в разных системах, администраторам необходима более тонкая настройка. За исключением отдельных моментов, /etc/rc легко поддавался модификации, и по-настоящему увлеченные люди часто и с удовольствием его модифицировали.

Основная проблема подобного монолитного решения заключалась в том, что оно не предоставляло механизма контроля над отдельными компонентами, запускаемыми из скрипта /etc/rc. К примеру, /etc/rc не мог перезапустить отдельный сервис. Системному администратору приходилось находить идентификатор процесса вручную, убивать этот процесс, дожидаться, фактического завершения процесса, находить в файле /etc/rc параметры, с которыми этот процесс был запущен, вручную запускать процесс из командной строки. Эта последовательность действий становилась еще более запутанной и сложной, если перезапускаемый сервис порождал несколько процессов или требовал дополнительных действий при запуске. В двух словах, монолитный скрипт запуска не мог выполнить основную задачу всех скриптов — сделать жизнь системного администратора проще.

Позже были предприняты попытки разделить /etc/rc на части, целью вынести некоторые наиболее важные элементы. Замечательный пример этого — скрипт /etc/netstart, инициализирующий работу сети. Этот скрипт позволял запустить сеть из однопользовательского режима, но его невозможно было полноценно интегрировать в систему автозапуска, поскольку части кода, имеющие отношение к сети нужно было чередовать с частями, к сети не относящимися. По этой причине скрипт /etc/netstart со временем мутировал в /etc/rc.network. Последний уже не является совершенно независимым скриптом, он состоит из больших, взаимозависимых функций, написанных на sh(1), которые, в свою очередь, вызываются из /etc/rc на разных этапах запуска системы. Тем временем, поскольку скрипты запуска стали больше, между ними появились сложные зависимости, «квази-модульная» система запуска стала еще запутаннее, чем монолитный скрипт /etc/rc.

Без простой и прозрачной подсистемы стартовые скрипты стали еще меньше удовлетворять потребности быстро развивающихся BSD-систем. Стало очевидно, что необходимы принципиальные шаги к достижению модульности и гибкости rc системы запуска. Исходя из этих соображений и была создана система запуска BSD rc.d. Ее признанными родителями стали Люк Мьюберн (Luke Mewburn) и сообщество NetBSD. Позже эта система была импортирована в FreeBSD. Ее название ссылается на расположение системных скриптов для отдельных сервисов, которые располагаются в /etc/rc.d. Немного позже мы рассмотрим отдельные компоненты системы rc.d и увидим, как запускаются отдельные скрипты.

Общая идея системы запуска BSD rc.d заключается в высокой степени модульности и повторном использовании кода. Высокая степень модульности значит, что каждый отдельный «сервис», такой как системный демон или простейшая задача, используют свой собственный скрипт, написанный на sh(1), который может запускать, останавливать, перезапускать этот сервис и проверять его статус. Стандартное действие выбирается из командной строки скрипта. /etc/rc так же как и раньше является основным загрузочным скриптом, но теперь он не выполняет весь процесс загрузки, а вызывает отдельные скрипты с параметром start. Так же легко выполняется остановка системы — для этого запускается этот же набор скриптов, но с аргументом stop. Это действие выполняется управляющим скриптом /etc/rc.shutdown. Обратите внимание, насколько точно эта система запуска соответствует традиционному в Unix принципу — использовать набор маленьких специализированных инструментов, каждый из которых призван решать конкретную задачу. Повторное использование кода это подход, который значит, что часто используемые операции реализованы в виде функций на языке sh(1) и объединены в файл/etc/rc.subr. Теперь типичный скрипт может состоять всего из нескольких строк кода на языке sh(1). И наконец, очень важная часть подсистемы rc.d — программа rcorder(8), которая которая помогает скрипту /etc/rc запускать отдельные скрипты в определенном порядке с учетом зависимостей между ними. В том числе, она так же помогает скрипту /etc/rc.shutdown, поскольку правильный порядок завершения работы системы противоположен порядку при запуске.

Система запуска BSD rc.d более подробно описана в статье Люка Мьюберна, а устройство отдельных компонентов — в документации к компонентам на соответствующих страницах справочника. Тем не менее, эта документация не дает однозначного описания того, как новичок может используя небольшое количество простых элементов создать полноценный rc.d для специфической задачи. Поэтому, основная цель этой статьи — показать несколько примеров использования системы запуска rc.d. Мы разберем несколько наиболее часто встречающихся случаев и покажем как можно в этих случаях использовать rc.d. Обратите внимание, что этот документ не является пошаговым описанием («HOWTO»), поскольку мы не ставим задачу дать готовые рецепты на все случаи жизни. Нашей задачей является дать вам точку входа для самостоятельного творчества в системе запуска rc.d. Так же, эта статья не в коей мере не является заменой страницам справочника. Не ленитесь заглядывать в них для более точной и полной информации во время чтения этой статьи.

Есть несколько требований для полного понимания изложенного материала. Во первых, вы должны быть знакомы с программированием на скриптовом языке sh(1). Во вторых, вы должны понимать как система осуществляет стартовые и завершающие действия пространства пользователя, которые описаны в rc(8).

Эта статья делает основной упор на FreeBSD версию rc.d. Тем не менее, она так же может быть полезна разработчикам NetBSD. Не смотря на то, что системы запуска FreeBSD и NetBSD реализованы по разному, пользовательские интерфейсы предоставляемые ими практически идентичны.

2 Обозначение задачи

Маленькое замечание перед началом. Не бойтесь переменной окружения $EDITOR.

Для того, чтобы писать качественные rc.d скрипты для системных сервисов нам нужно ответить на следующие вопросы:

Этот сервис обязательный или нет?

Будет скрипт обслуживать только одну программу (т.е. демон) или будет выполнять более комплексные задачи?

От каких сервисов будет зависеть этот сервис и какие будут зависеть от него?

Из следующих примеров мы можем увидеть, насколько важно знать ответы на эти вопросы перед написанием rc.d скриптов.

3 Простейший скрипт

Этот скрипт просто выдает сообщение при каждом старте системы:

#!/bin/sh(1)

. /etc/rc.subr(2)

name="dummy"(3)
start_cmd="${name}_start"(4)
stop_cmd=":"(5)

dummy_start()(6)
{
    echo "Nothing started."
}

load_rc_config $name(7)
run_rc_command "$1"(8)

Здесь:

(1)
Скрипт должен начинаться с магической строки "#!/bin/sh" («shebang»). Эта строка указывает командный интерпретатор для скрипта. Благодаря этой строке скрипт может быть вызван как любой другой исполняемый файл, если у него установлен бит исполнения (см. chmod(1)). Например, системный администратор может запустить такой скрипт прямо из командной строки:

# /etc/rc.d/dummy start

Note: Для того, чтобы подсистема rc.d могла правильно управлять этим скриптом, он должен быть написан на языке sh(1). Если ваш сервис или порт использует бинарное приложение для контроля запуска и остановки, установите это бинарное приложение в /usr/sbin для системного сервиса или в/usr/local/sbin для портов, и вызывайте его из скрипта на sh(1) в соответствующем каталоге.

Tip: Если вы хотите подробнее узнать почему rc.d скрипты должны быть написаны на языке sh(1), посмотрите как /etc/rc вызывает их с помощью функции run_rc_script, а так же посмотрите, как устроенна эта функция в файле /etc/rc.subr.

(2)
В файле /etc/rc.subr определено множество функций на языке sh(1), которые могут быть использованы в rc.d скриптах. Эти функции задокументированы в rc.subr(8). И хотя теоретически возможно написать rc.d скрипт вообще не пользуясь этими функциями, rc.subr(8) функции настолько удобны и функциональны, что нет смысла осложнять себе жизнь не пользуясь ими. Поэтому, совершенно не удивительно, что все rc.d скрипты обращаются к rc.subr(8). И наш скрипт — не исключение.

Каждый rc.d должен «включать» в себя файл /etc/rc.subr (здесь для этого используется команда «.») перед тем как он вызовет функцию rc.subr(8). Наиболее предпочтительный вариант — сделать это перед любыми другими действиями.

Note: Некоторые полезные функции, имеющие отношение к сети, находятся в еще одном важном файле /etc/network.subr.

(3)
Обязательная переменная name определяет имя нашего скрипта, функции в rc.subr(8) требуют наличия этой переменной. Поэтому каждый rc.d скрипт должен иметь установленную переменную name до того, как он вызовет какую-либо из rc.subr(8) функций.

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

Note: В настоящее время при написании rc.d значения переменных заключаются в двойные кавычки. Помните, что это только стилистическая особенность и она не всегда применима. Вы можете опускать кавычки вокруг простых слов, не содержащих специальных символов sh(1) или заключать значения в одиночные кавычки чтобы предотвратить преобразование значений интерпретатором sh(1). Программист должен отличать стилистические условности от потребностей языка и с пониманием использовать разные виды синтаксиса.

(4)
Главная идея rc.subr(8) заключается в предоставлении rc.d методов вызова различных типичных действий. В частности, таким образом вызываются методы start и stop передаваемые скрипту как аргументы командной строки. Метод это выражение на языке sh(1) записанное как argument_cmd, где argument является аргументом командной строки. Далее мы увидим как rc.subr(8) предоставляет стандартные методы для типичных аргументов.

Note: Для стандартизации rc.d скриптов рекомендуется использовать переменную ${name} где это возможно. Благодаря этому, зачастую, основной работой в написании нового скрипта является копирование нескольких строк из уже существующего.

(5)
Нужно помнить, что rc.subr(8) предоставляет стандартные методы для типичных аргументов. Поэтому, мы должны переопределить стандартный метод пустой операцией языка sh(1) в случае, если мы хотим, чтобы наш скрипт ничего не делал.
(6)
Тело более сложного метода должно быть реализовано в виде функции. Хорошей практикой является осмысленное название таких функций.

Important: Настоятельно рекомендуется добавлять префикс ${name} к именам всех функций определенных в нашем скрипте чтобы избежать переопределения функций из rc.subr(8) или других включаемых файлов.

(7)
Эта команда rc.subr(8) загружает переменные из файла rc.conf(5). Наш скрипт их пока не использует, но тем не менее, рекомендуется выполнять этот вызов в скрипте, так как в rc.conf(5) могут содержаться переменные, управляющие непосредственно функциями rc.subr(8).
(8)
Обычно, это самая последняя команда в rc.d скрипте. Она непосредственно вызывает действия из rc.subr(8) подготовленные методами и переменными, определенными в нашем скрипте.

4 Настраиваемый простейший скрипт

Теперь давайте добавим несколько элементов управления в наш скрипт. Как вы знаете, rc.d скрипты управляются из файла rc.conf(5). К счастью, rc.subr(8) скрывает от нас все сложные элементы разбора и анализа. Приведенный далее скрипт проверяет, было ли разрешено его выполнение в фале rc.conf(5), и если оно было разрешено, выводит сообщение при загрузке. Фактически, две эти задачи совершенно независимы. С одной стороны, rc.d скрипт поддерживает разрешение и запрещение его исполнения через rc.conf(5), с другой стороны rc.d может иметь конфигурационные переменные. Мы реализуем обе эти возможности в нашем скрипте:

#!/bin/sh

. /etc/rc.subr

name="dummy"
rcvar=`set_rcvar`(1)
start_cmd="${name}_start"
stop_cmd=":"

load_rc_config $name(2)
eval "${rcvar}=\${${rcvar}:-'NO'}"(3)
dummy_msg=${dummy_msg:-"Nothing started."}(4)

dummy_start()
{
    echo "$dummy_msg"(5)
}

run_rc_command "$1"

Что изменилось в этом примере?

(1)
Переменная rcvar определяет значение переменной, которая должна разрешать или запрещать запуск скрипта. Необходимость получать имя переменной из rc.subr(8) вызовом функции set_rcvar обусловлена тем, что разные операционные системы используют разные соглашения в именовании переменных. Например, FreeBSD использует переменные вида ${name}_enable, а NetBSD использует ${name} в файле rc.conf(5). Таким образом, наш скрипт будет руководствоваться значением переменной dummy_enable под FreeBSD и значением переменной dummy под NetBSD.
(2)
Теперь load_rc_config вызывается до загрузки переменных из rc.conf(5)

Note: При изучении rc.d скриптов, помните, что sh(1) откладывает интерпретацию переменных до момента, когда они будут фактически вызваны, то есть, не является ошибкой вызвать load_rc_config позже, но до вызова run_rc_command.

(3)
Вызов функции run_rc_command выдаст предупреждение, если переменная rcvar определена, но для нее не выставлено значение. Если ваш rc.d скрипт предназначен для базовой системы, вы должны добавить значения по-умолчанию для него в файл /etc/defaults/rc.conf и задокументировать их в rc.conf(5). В противном случае, вы должны определить значения по-умолчанию в самом скрипте. Переносимый вариант последнего случая показан в нашем примере.

Note: Вы можете заставить rc.subr(8) действовать так, как будто значение управляющей переменной выставлено в ON независимо от реального значения добавляя к аргументу скрипта префиксы one или force, таким образом запуская скрипт с опциями onestart или forcestop. Помните, что force имеет ряд опасных побочных эффектов, которые мы обсудим позже, в то время как one просто переопределяет значение ON/OFF переменной. Другими словами, если предположить, что значение переменной dummy_enable установлено в OFF, следующая команда выполнит метод start вопреки этому значению:

# /etc/rc.d/dummy onestart

(4)
Теперь сообщение, выводимое скриптом при старте, не является жестко прописанным в скрипте и неизменным. Оно может быть определено как переменная dummy_msg в rc.conf(5). Это простейший пример того, как переменные rc.conf(5) могут управлять поведением rc.d скрипта.

Important: Все rc.conf(5) переменные, используемые только нашим скриптом, должны иметь один и тот же префикс: ${name}. Например, dummy_mode, dummy_state_file, и так далее.

Note: Хотя вполне возможно использовать внутри скрипта короткие имена переменных, то есть просто msg, тем не менее добавление уникального префикса ${name} ко всем глобальным переменным, используемым нашим скриптом предохранит нас от потенциальных переопределений переменных из пространства имен rc.subr(8).

До тех пор, пока переменные из rc.conf(5) и их используемые в скрипте эквиваленты одинаковы, мы можем использовать более компактную запись для установки значения по-умолчанию:

: ${dummy_msg:="Nothing started."}

Хотя, в текущем соглашении о стиле все же рекомендуется использовать более развернутую форму записи.

Как правило, для rc.d скриптов базовой системы не устанавливают значения по-умолчанию в файле rc.conf(5), поскольку эти значения должны находиться в файле /etc/defaults/rc.conf. С другой стороны, скрипты из системы портов должны устанавливать значения по-умолчанию сами, как это было сделано в нашем скрипте.

(5)
Здесь мы используем переменную dummy_msg непосредственно для действия, то есть чтобы отобразить сообщение.

5 Запуск и остановка простого демона

Как уже упоминалось ранее, rc.subr(8) может предоставить некоторые методы по-умолчанию. Очевидно, что такие стандартные методы не могут быть универсальными. Они предназначены в общем случае для запуска и остановки простого приложения-демона. Давайте предположим, что нам нужно написать rc.d скрипт для такого демона. Пусть демон называется mumbled. Вот этот скрипт:

#!/bin/sh

. /etc/rc.subr

name="mumbled"
rcvar=`set_rcvar`
command="/usr/sbin/${name}"(1)

load_rc_config $name
run_rc_command "$1"

Необычайно просто, не так ли? Давайте посмотрим на него немного внимательнее. Отметить нужно следующее:

(1)
Переменная command очень важна для rc.subr(8). Если она установлена, то rc.subr(8) будет действовать согласно сценарию обслуживания стандартного демона. В частности, стандартные методы предоставляются для аргументов: start, stop, restart, poll и status.

Демон будет запущен командой $command с ключами определенными в переменной $mumbled_flags. Таким образом, все данные, необходимые для стандартного метода start определены в переменных на момент запуска скрипта. В отличие от метода start, другим методам может потребоваться дополнительная информация о запущенном процессе. Например, stop должен знать об идентификаторе запущенного процесса (PID) чтобы завершить его. В нашем случае, rc.subr(8) найдет среди запущенных процессов процесс с именем $procname. Эта переменная выставляется rc.subr(8) автоматически и по-умолчанию ее значение соответствует переменной command. Другими словами, когда мы указываем значение command, мы тем самым указываем и procname. Это позволяет нашему скрипту проверить, запущен ли демон или завершить его работу.

Note: Некоторые программы на самом деле — запускаемые скрипты. Система запускает такие скрипты в командном интерпретаторе, передавая ему имя скрипта в качестве аргумента. Это отражается и на списке процессов, в том числе сбивает с толку rc.subr(8). Если вы запускаете такой скрипт, то вы должны указать интерпретатор как значение переменной command_interpreter, чтобы rc.subr(8) мог правильно найти его в списке процессов.

Для каждого rc.d скрипта есть необязательная переменная rc.conf(5), имеющая приоритет над переменной command. Она называется ${name}_program, где name — обязательная переменная, которая обсуждалась ранее. Другими словами, в нашем случае переменная будет выглядеть как mumbled_program. Эта переменная фактически переопределяет переменную rc.subr(8) command.

Особенности языка sh(1) позволяют указать значение переменной ${name}_program в файле rc.conf(5) или даже в самом скрипте и даже в том случае, если переменная command не установлена. В этом случае специальные свойства переменной ${name}_program теряются и она становится обыкновенной переменной внутри скрипта и вы можете использовать ее в своих целях. При таком использовании теряется смысл переменной ${name}_program, так как в rc.d скрипте переменные ${name}_program и command при фактическом выполнении будут иметь одно значение.

Для более подробной информации смотрите страницу справочника rc.subr(8).

6 Запуск более сложного демона

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

#!/bin/sh

. /etc/rc.subr

name="mumbled"
rcvar=`set_rcvar`

command="/usr/sbin/${name}"
command_args="mock arguments > /dev/null 2>&1"(1)

pidfile="/var/run/${name}.pid"(2)

required_files="/etc/${name}.conf /usr/share/misc/${name}.rules"(3)

sig_reload="USR1"(4)

start_precmd="${name}_prestart"(5)
stop_postcmd="echo Bye-bye"(6)

extra_commands="reload plugh xyzzy"(7)

plugh_cmd="mumbled_plugh"(8)
xyzzy_cmd="echo 'Nothing happens.'"

mumbled_prestart()
{
    if checkyesno mumbled_smart; then(9)
        rc_flags="-o smart ${rc_flags}"(10)
    fi
    case "$mumbled_mode" in
    foo)
        rc_flags="-frotz ${rc_flags}"
        ;;
    bar)
        rc_flags="-baz ${rc_flags}"
        ;;
    *)
        warn "Invalid value for mumbled_mode"(11)
        return 1(12)
        ;;
    esac
    run_rc_command xyzzy(13)
    return 0
}

mumbled_plugh()(14)
{
    echo 'A hollow voice says "plugh".'
}

load_rc_config $name
run_rc_command "$1"

(1)
Дополнительные аргументы команде $command можно передать в переменной command_args. Эти аргументы будут добавлены в командную строку после значения переменной $mumbled_flags. Поскольку командная строка, получившаяся в результате этих преобразований передается команде eval, то перенаправления ввода-вывода могут быть указаны в переменной command_args.

Note: Никогда не указывайте опций, начинающихся со знака -, таких как -X или —foo в переменной command_args. Значение переменной command_args добавляется к концу формируемой командной строки, поэтому они будут добавлены после значений из ${name}_flags. Но многие приложения не могут интерпретировать флаги после обычных аргументов. Лучшим вариантом будет добавить эти аргументы к переменной $command, таким образом в конечной командной строке они будут добавлены перед значением переменной ${name}_flags. Другой вариант — переопределить переменную rc_flags, как это будет показано ниже.

(2)
Хорошо воспитанный демон должен создавать файл с идентификатором процесса (pidfile), чтобы в дальнейшем можно было с легкостью найти запущенный процесс. Если переменная pidfile установлена, она указывает rc.subr(8) где искать файл с идентификатором процесса для использования внутри своих методов.

Note: Фактически, rc.subr(8) будет использовать эту переменную, и для того, чтобы проверить, запущен ли демон, перед тем как запустить его. Эта проверка может быть пропущена при использовании аргумента faststart.

(3)
Если демону для запуска требуются определенные файлы, просто перечислите их в переменной required_files, и rc.subr(8) будет проверять наличие этих файлов при старте. В том числе, существуют специальные переменные required_dirs и required_vars для каталогов и переменных окружения соответственно. Все эти переменные перечислены на странице справочника rc.subr(8).

Note: Стандартные методы rc.subr(8) можно заставить не проводить такие проверки, для этого нужно использовать аргумент forcestart для скрипта.

(4)
В случае, если демон использует нестандартные сигналы, они так же могут быть определены в скрипте. В нашем случае переменная sig_reload определяет сигнал, который заставит демона перечитать свою конфигурацию. По умолчанию, таким сигналом является SIGHUP. Другим сигналом, который можно переопределить, является SIGTERM, который используется для остановки демона, и этот сигнал определяется переменной sig_stop.

Note: Названия сигналов должны быть переданы rc.subr(8) без префикса SIG, как показано в примере. FreeBSD версия программы kill(1) умеет распознавать префикс SIG, но версии других операционных систем могут этого не уметь.

(5)(6)
Дополнительные действия, которые нужно выполнить до или после стандартных процедур можно определить с помощью переменных argument_precmd и argument_postcmd. Команды на языке sh(1), определенные в этих переменных будут выполнены до или после соответствующих методов, в зависимости от названия переменной.

Note: Переопределение стандартных методов пользовательскими параметрами argument_cmd не мешает использованию argument_precmd или argument_postcmd там, где они необходимы. В частности, в случаях, когда необходимы дополнительные проверки перед выполнением команды, использование argument_precmd вместе с argument_cmd позволяет логически разделить проверку и команду.

Не забывайте, что эти переменные должны содержать правильные выражения на языке sh(1). Зачастую, наиболее удобным способом является указание в этих переменных пользовательской функции, выполняющей всю работу. Это соответствует общей стилистике написания скриптов запуска, но не позволяйте ограничениям стиля ограничивать ваше понимания процесса в целом.

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

Note: Аргумент reload и его метод особые. С одной стороны, этот метод описан в rc.subr(8), с другой стороны, сам метод reload по-умолчанию не добавляется к списку методов скрипта. Причина этого кроется в том, что разные демоны используют различные механизмы перезагрузки, а некоторые вообще не имеют таких механизмов. Поэтому, если мы хотим выполнить определенные действия для перезагрузки демона, мы должны сделать это через переменную extra_commands.

Так что же делает стандартный метод reload? Очень часто демоны используют сигнал SIGHUP для перечитывания своей конфигурации, поэтому rc.subr(8) пытается перезагрузить демон посылая ему этот сигнал. Этот сигнал может быть переопределен в переменной sig_reload если это необходимо.

(8)(14)
Наш скрипт имеет два нестандартных аргумента plugh и xyzzy. Мы можем видеть их в extra_commands, и здесь мы предоставляем методы для этих аргументов. Метод xyzzy определен прямо как значение переменной, а метод plugh реализован в виде функции mumbled_plugh.

Это нестандартные аргументы, и они не будут вызываться при запуске и остановке системы. Обычно, эти команды нужны для выполнения их администратором. В том числе, они могут использоваться сторонними подсистемами, например, devd(8), если они указаны в файле devd.conf(5).

Полный список доступных команд можно посмотреть, если вызвать наш скрипт без аргументов. Этот список будет автоматически сформирован rc.subr(8). Например, такой вывод мы увидим, вызвав наш скрипт без аргументов:

# /etc/rc.d/mumbled
Usage: /etc/rc.d/mumbled [fast|force|one](start|stop|restart|rcvar|reload|plugh|xyzzy|status|poll)

(13)
Скрипт может вызывать свои собственные стандартные или нестандартные команды, если это необходимо. Это может выглядеть как вызов функций в теле скрипта, но нужно помнить, что вызов функции командной оболочки и вызов скрипта — это не одно и то же. Например, команда xyzzy в нашем случае не реализована в виде функции. Кроме того, могут быть команда с префиксами pre- и post- необходимые для выполнения функции. Поэтому, правильный путь — запускать команды так, как они представляются rc.subr(8), как это и показано в примере.
(9)
rc.subr(8) предоставляет еще одну очень полезную функциюcheckyesno. Она принимает в качестве аргумента имя переменной и возвращает нулевой код возврата, если значение переменной установлено в YES, TRUE, ON или 1 не зависимо от регистра. В противном случае, функция возвращает ненулевой код возврата. В последнем случае, функция так же проверяет, имеет ли переменная значение NO, FALSE, OFF или 0 не зависимо от регистра, и если переменная содержит что-то кроме этих значений (мусор), выводит предупреждение.

Помните, что в языке sh(1) нулевой код возврата соответствует значению истина, а ненулевой код соответствует значению ложь.

Important: Функция checkyesno принимает в качестве значения имя переменной. Не передавайте ей развернутое значение переменной; это приведет к совершенно неожиданным результатам.

Пример правильного использования функции checkyesno:

if checkyesno mumbled_enable; then
    foo
fi

И наоборот, вызов функции checkyesno как показано ниже не будет работать, или, по крайней мере, будет работать не так, как вы этого ожидаете:

if checkyesno "${mumbled_enable}"; then
    foo
fi

(10)
Мы можем поменять флаги, передаваемые $command изменяя переменную rc_flags в $start_precmd.
(11)
В некоторых случаях бывает необходимо отобразить важные сообщения и в то же время передать их в syslog. Это с легкостью можно проделать с помощью функций debug, info, warn, и err, определенных в rc.subr(8). Функция err после своего вызова завершает скрипт с указанным кодом возврата.
(12)
Коды возврата методов и их pre- команд по умолчанию не игнорируются. Если argument_precmd возвращает ненулевой код, главный метод не будет выполнен. Так же и argument_postcmd не будет выполнен, если главный метод вернет ненулевой код.

Note: Тем не менее, в командной строке можно дать указание rc.subr(8) игнорировать коды ошибок всех функций и выполнять все команды. Для этого нужно добавить к аргументу скрипта в качестве префикса force, например forcestart.

7 Присоединение rc.d скрипта к инфраструктуре

После того, как скрипт был написан, его необходимо присоединить к rc.d инфраструктуре. Решающим шагом является установка скрипта в /etc/rc.d (для базовой системы) или в /usr/local/etc/rc.d (для портов). В файлах и существуют специальные процедуры для этого, и вам не придется заботиться о правильных правах доступа и владельце скриптов. Системные скрипты должны устанавливаться из директории src/etc/rc.d и должны быть указаны для этого в Makefile, находящемся в этой директории. Скрипты из портов должны устанавливаться с помощью переменной USE_RC_SUBR, как это описано в в руководстве по созданию портов.

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

Мы уже упоминали об rcorder(8). Сейчас настал момент, взглянуть на эту программу поближе. В двух словах, rcorder(8) принимает в качестве аргументов список файлов, проверяет их содержимое и выводит упорядоченный список в stdout. Идея держать информацию о зависимостях внутри заключается в том, чтобы каждый файл имел структурную зависимость только от себя. Внутри файла можно указать следующую информацию:

названия «условий» (в нашем случае — сервисов), которые наш скрипт предоставляет;

названия «условий», которые необходимы скрипту для запуска;

названия «условий», которые должны быть выполнены до запуска нашего сервиса;

дополнительные ключевые слова (keywords), чтобы выделить группу файлов из всего набора. (можно дать команду rcorder(8) игнорировать или использовать только те файлы, в которых есть определенное ключевое слово.)

В нормальном случае rcorder(8) может оперировать только с текстовыми файлами, синтаксис которых похож на синтаксис sh(1). Поэтому, специальные строки, обрабатываемые rcorder(8) выглядят как комментарии в языке sh(1). Синтаксис этих строк очень жесткий, чтобы упростить их обработку. Смотрите rcorder(8) для более детальной информации.

Кроме того, использование специальных строк позволяет скрипту настаивать на зависимости от другого сервиса с принудительным стартом последнего. Это может быть необходимым в случае, когда сервис-зависимость является необязательным или не запускается, потому что администратор по ошибке отключил его в rc.conf(5).

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

#!/bin/sh

# PROVIDE: mumbled oldmumble (1)
# REQUIRE: DAEMON cleanvar frotz(2)
# BEFORE: LOGIN(3)
# KEYWORD: nojail shutdown(4)

. /etc/rc.subr

name="mumbled"
rcvar=`set_rcvar`
command="/usr/sbin/${name}"
start_precmd="${name}_prestart"

mumbled_prestart()
{
    if ! checkyesno frotz_enable && \
      ! /etc/rc.d/frotz forcestatus 1>/dev/null 2>&1; then
      force_depend frotz || return 1(5)
    fi
    return 0
}

load_rc_config $name
run_rc_command "$1"

А теперь, как и прежде, проведем детальный анализ изменений:

(1)
Эта строка описывает «условия», предоставляемые нашим скриптом. Теперь, другие скрипты могут перечислять в списке «условий» «условия», предоставляемые нашим скриптом, тем самым они станут зависимы от нашего скрипта.

Note: Обычно, скрипт может предоставлять только одно условие, тем не менее, ничто не мешает нам указать несколько условий. Например, для совместимости с предыдущими версиями.

В любом случае, название первого из этих условий (или единственного) должно быть таким же, как значение переменной ${name}.

(2)(3)
Кроме всего прочего, наш скрипт определяет «условия», от которых он зависит. Следуя этим строкам rcorder(8) запустит наш скрипт после скриптов, предоставляющих условия DAEMON и cleanvar, но до скрипта предоставляющего условие LOGIN.

Note: Строка BEFORE: не формирует полный список зависимостей. Правильный способ ее использования — если другой скрипт может корректно запуститься без нашего скрипта, но наш скрипт может помочь ему сделать это лучше. Типичный пример из жизни — сетевые интерфейсы и файерволы. Интерфейс на самом деле не зависит от фаервола, но из соображений безопасности системы иногда необходимо, чтобы фаервол был инициализирован до появления какого бы то ни было сетевого трафика.

Кроме условий, соответствующих отдельному сервису, существуют мета-условия и их «контейнеры». Они описывают группы операций. Их можно отличить по написанию их названий в верхнем регистре. Список контейнеров и их назначение можно найти на странице справочника rc(8).

Помните, что размещение сервиса в строке REQUIRE: не гарантирует, что этот сервис будет действительно запущен до запуска нашего сервиса. Требуемый сервис может быть отключен в rc.conf(5) или просто не сумеет запуститься из-за ошибки. Очевидно, что rcorder(8) не способен отследить такие тонкости, так же этого не может проконтролировать и rcorder(8). Тем не менее, наш скрипт должен уметь проконтролировать подобную ситуацию. Как это можно сделать в некоторых случаях описано ниже.

(4)
Как мы помним, ключевые слова rcorder(8) могут использоваться для запуска групп скриптов или исключения таких групп из запуска. А именно, используя опции rcorder(8) -k и -s мы можем указывать список ключевых слов для групп, которые нужно выполнить (keep list) или которые нужно пропустить (skip list) соответственно. Из всех файлов, которые необходимо отсортировать в порядке исполнения rcorder(8) выберет только те, в которых указано ключевое слово из списка выполнения и не указано ключевое слово из списка пропуска.

В FreeBSD, rcorder(8) используется в скрипте /etc/rc и скрипте /etc/rc.shutdown. Эти два скрипта определяют стандартный для FreeBSD список ключевых слов и их значений. Например:

nojail
Сервис не для запуска в jail(8). Автоматические процедуры запуска и остановки системы будут игнорировать такие скрипты, если они запускаются изнутри jail(8).

nostart
Сервис не должен запускаться вручную или не должен запускаться вообще. Автоматическая процедура запуска будет игнорировать такие скрипты. В сочетании с ключевым словом shutdown это может использоваться для скриптов, которые должны делать что-либо только при завершении системы.

shutdown
Это ключевое слово должно использоваться только для сервисов, которые нужно остановить перед остановкой всей системы.

Note: Когда система завершает работу, запускается скрипт /etc/rc.shutdown. Этот скрипт подразумевает, что большая часть rc.d скриптов ничего не делает в этот момент. Поэтому, /etc/rc.shutdown не пытается запустить rc.d скрипты, а запускает только те из них, в которых содержится ключевое слово shutdown и полностью игнорирует все остальные. Для еще более быстрого завершения работы системы /etc/rc.shutdown завершает все скрипты с аргументом faststop, то есть, скрипты пропускают некоторые стандартные проверки, такие как проверку на наличие файла с идентификатором процесса (pidfile). Так как зависимые сервисы должны быть завершены раньше, чем те от которых они зависят, то /etc/rc.shutdown запускает скрипты в обратном порядке.

Если вы пишете rc.d скрипт, вы должны знать как он будет вести себя в момент завершения работы системы. Другими словами, если ваш скрипт выполняет свои действия по команде start, то вам не нужно указывать в нем ключевое слово shutdown. Если же ваш скрипт управляет работой сервиса, хорошей идеей будет остановить его перед фактическим завершением работы системы, описанным в halt(8). В частности такой подход необходим к сервисам, которым необходимо время для корректного завершения. Типичный пример такого сервиса — база данных.

(5)
Для начала, помните, что функция force_depend должна использоваться с большой осторожностью. В общем случае, лучше будет изменить иерархию конфигурационных переменных в ваших rc.d скриптах, если они независимы.

Если же вы никак не можете обойтись без функции force_depend, в нашем примере представлен типичный пример, как это сделать корректным образом. Нашему демону mumbled дополнительно нужен для работы сервис frotz. Тем не менее сервис frotz — вспомогательный. К счастью, наш скрипт имеет доступ ко всем переменным rc.conf(5). Если переменная frotz_enable установлена и ее значение — истина, мы надеемся на лучшее и полностью полагаемся на rc.d в запуске сервиса frotz. В противном случае, мы форсированно проверяем статус сервиса frotz, и если сервис не запущен, мы запускаем сервис frotz функцией force_depend. Предупреждающее сообщение будет показано при вызове этой функции так как этот вызов произойдет только в случае ошибки конфигурации.

8 Создание более гибких rc.d скриптов

Во время запуска и остановки системы rc.d скрипт предположительно оперирует над той или иной подсистемой целиком. К примеру, скрипт /etc/rc.d/netif должен запускать и останавливать все сетевые интерфейсы, описанные в rc.conf(5). Так же каждая задача должна быть целиком описана одной командой — start или stop. Но между запуском системы и ее остановкой rc.d должны помогать системному администратору управлять запущенными процессами, и зачастую для этого необходимо больше гибкости. Например, администратор может сконфигурировать новый интерфейс в rc.conf(5) и ему понадобиться запустить только этот интерфейс, не затрагивая все остальные. В следующий раз, администратору может понадобиться остановить этот единственный интерфейс. Для этого можно передавать дополнительные опции для скрипта, такие как название интерфейса, в виде дополнительных аргументов командной строки.

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

Как rc.subr(8) может получить доступ к аргументам командной строки? Передать их непосредственно внутри скрипта функции run_rc_command невозможно, так как функции sh(1) не имеют доступа к позиционным параметрам вызывающего их объекта. Кроме того, в rc.d скриптах считается хорошим тоном, когда скрипт сам определяет какие аргументы должны быть переданы его методам.

В rc.subr(8) используется следующее решение: функции run_rc_command передаются все аргументы командной строки, при этом подразумевается, что первым из этих аргументов является имя запускаемого метода — start, stop и т.д. Эти аргументы сдвигаются внутри функции run_rc_command c помощью оператора sh(1) shift. Таким образом, переменная $2 в изначальной командной строке передается методу как $1, и так далее.

So the approach adopted by rc.subr(8) is as follows:

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

#!/bin/sh

. /etc/rc.subr

name="dummy"
start_cmd="${name}_start"
stop_cmd=":"
kiss_cmd="${name}_kiss"
extra_commands="kiss"

dummy_start()
{
    if [ $# -gt 0 ]; then(1)
        echo "Greeting message: $*"
    else
        echo "Nothing started."
    fi
}

dummy_kiss()
{
    echo -n "A ghost gives you a kiss"
    if [ $# -gt 0 ]; then(2)
        echo -n " and whispers: $*"
    fi
    case "$*" in
    *[.!?])
        echo
        ;;
    *)
        echo .
        ;;
    esac
}

load_rc_config $name
run_rc_command "$@"(3)

Какие значимые изменения были сделаны в этом файле?

(1)
Все аргументы, переданные скрипту после аргумента start будут переданы как аргументы соответствующему методу. Мы можем использовать их любым способом в соответствии с поставленной задачей и в меру наших навыков и фантазии. В настоящем примере мы просто передаем их все команде echo(1) как одну строку в переменной $* внутри двойных кавычек. Вот пример как может быть вызван такой скрипт:

# /etc/rc.d/dummy start
Nothing started.
# /etc/rc.d/dummy start Hello world!
Greeting message: Hello world!

(2)
Такой же трюк можно проделать с любыми методом нашего скрипта, а не только со стандартными. Добавим нестандартный метод kiss, и он будет иметь те же возможности, что и метод start:

# /etc/rc.d/dummy kiss
A ghost gives you a kiss.
# /etc/rc.d/dummy kiss Once I was Etaoin Shrdlu…
A ghost gives you a kiss and whispers: Once I was Etaoin Shrdlu…

(3)
Если мы хотим просто передать все дополнительные аргументы любому методу, мы можем просто заменить «$1» на «$@» в последней строке нашего скрипта, в которой мы вызываем run_rc_command.

Important: Программисты на языке sh(1) обязаны понимать тонкую разницу между специальными переменными $* и $@ и тем, как они передают позиционные параметры. Для более тщательного изучения этого вопроса ознакомьтесь с очень подробной страница справочника sh(1). Не используйте эти переменные, если вы не понимаете их действие, поскольку их неправильное использование может сделать ваш скрипт неправильно работающим и небезопасным.

Note: В настоящее время функция run_rc_command имеет ошибку, которая может привести терять к потере разделителей между аргументами. Поэтому, аргументы, в которых присутствует пробел могут обрабатываться некорректно. Появление этой ошибки может быть спровоцировано неправильным использованием переменной $*.

Источник

Запись опубликована в рубрике Записки админа. Добавьте в закладки постоянную ссылку.

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *