Библиотека — FreeBSD — rc-система и управление сервисами [X]
Шифр
freebsd-rc-system
Разделы
freebsd tooling
Сверено
2 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
17 826 знаков · 16 разделов · чтения около 20 минут

FreeBSD — rc-система и управление сервисами


Обзор

Rc-система — это init-подсистема FreeBSD, которая управляет всем жизненным циклом системы от момента, когда ядро передаёт управление в userland, до корректного завершения работы. Она определяет, какие сервисы запускаются, в каком порядке, с какими зависимостями, и предоставляет единый интерфейс для управления ими в runtime.

В отличие от systemd в Linux, rc — это набор shell-скриптов с детерминированным порядком исполнения. Это делает систему полностью прозрачной и отлаживаемой: любой аспект поведения можно проследить, прочитав конкретный sh-скрипт. Trade-off — отсутствие параллельного запуска, сокет-активации и встроенной интеграции с cgroups/namespaces (хотя service jails частично закрывают последний пункт).

Историческая эволюция

Утилита rc(8) впервые появилась в 4.0BSD в 1980 году. Классический процесс загрузки был предельно прост: ядро BSD запускало init(8), который передавал управление скрипту /etc/rc, а тот вызывал /etc/netstart для настройки сети и /etc/rc.local для запуска пользовательских демонов.

Исторический BSD имел монолитный стартовый скрипт /etc/rc, который выполнял все задачи перехода в многопользовательский режим. Проблема монолитного подхода была в отсутствии контроля над отдельными компонентами: чтобы перезапустить один демон, администратору приходилось вручную находить процесс, убивать его и восстанавливать параметры запуска из скрипта.

В 2000 году NetBSD начала модернизацию этой подсистемы: сначала появился каталог /etc/rc.d для отдельных скриптов каждого сервиса, затем команда rcorder(8) для определения порядка запуска. FreeBSD интегрировала систему rc.d из NetBSD в 2002 году.

Процесс загрузки

При загрузке ядро запускает init(8) (PID 1), который выполняет /etc/rc. Далее rc действует по следующей логике:

При автозагрузке устанавливается флаг rc_fast=yes, который предотвращает проверку уже запущенных процессов для ускорения загрузки. Затем rc загружает /etc/rc.subr — библиотеку shell-функций, используемую всеми скриптами. После этого загружаются конфигурационные файлы и определяется, выполняется ли загрузка в jail (в этом случае скрипты с ключевым словом nojail пропускаются).

Далее вызывается rcorder(8) для упорядочивания файлов в /etc/rc.d/, не имеющих ключевого слова nostart. Скрипты выполняются последовательно до early_late_divider (по умолчанию это виртуальная точка FILESYSTEMS, то есть момент, когда смонтированы корень и остальные критичные файловые системы). После этого rcorder(8) вызывается повторно, уже включая скрипты из каталогов local_startup (по умолчанию /usr/local/etc/rc.d/), и выполнение продолжается.

Двухфазная загрузка (early/late) — архитектурно важный момент. early_late_divider определяет границу между стадиями: ранняя стадия содержит все сервисы, необходимые для монтирования дисков, чтобы поздняя стадия могла включать скрипты из local_startup. Это решает проблему курицы и яйца: скрипты из /usr/local/etc/rc.d/ не могут быть прочитаны, пока /usr/local не смонтирован.

При завершении работы /etc/rc.shutdown отбирает через rcorder(8) только скрипты с ключевым словом shutdown, разворачивает порядок и вызывает их с аргументом faststop — предварительные проверки вроде чтения PID-файла при этом пропускаются ради скорости выключения.

rcorder: механизм разрешения зависимостей

rcorder(8) — ключевой компонент системы. Он разбирает метаданные зависимостей в заголовках скриптов и выстраивает их в топологически корректном порядке. Программа rcorder(8) анализирует комментарии в начале каждого стартового скрипта для определения порядка вызова сервисов с учётом зависимостей.

Доступные директивы:

PROVIDE — объявляет, какой сервис (или виртуальную «возможность») предоставляет скрипт. Имя в PROVIDE — это то, на что могут ссылаться REQUIRE и BEFORE других скриптов. Рекомендуется совпадение имени файла, значения PROVIDE и переменной $name — это упрощает отладку.

REQUIRE — перечисляет сервисы, которые должны быть запущены до данного скрипта. Если скрипт A содержит REQUIRE: B, то A будет запущен после B.

BEFORE — обратная логика: скрипт будет запущен до перечисленных сервисов. Используется реже и с осторожностью.

KEYWORD — теги, влияющие на поведение скрипта в определённых контекстах: shutdown (запускать скрипт при выключении), nojail (не запускать в jail-окружении), nojailvnet (не запускать в jail без VNET), firstboot (запускать только при первой загрузке), nostart (не запускать автоматически).

Существуют также виртуальные PROVIDE-точки (ALLUPPERCASE), которые обозначают этапы загрузки, а не конкретные сервисы; в порядке запуска это FILESYSTEMS, NETWORKING, SERVERS, DAEMON, LOGIN. Они работают как барьеры — группируют скрипты по стадиям. Например, REQUIRE: LOGIN означает «запустить после того, как система полностью готова к пользовательским сервисам», и это обязательное требование для скриптов, запускающих процессы от имени непривилегированных пользователей.

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

rcorder /etc/rc.d/* /usr/local/etc/rc.d/* 2>/dev/null

Это одна из сильных сторон init-системы rc(8) — просто определить и настроить порядок запуска сервисов, тогда как более сложный systemd(1) может загружаться быстрее, но значительно сложнее в отладке.

rc.conf: центральная конфигурация

/etc/rc.conf — единственный файл, который определяет, что и как запускается в системе. Файл rc.conf содержит описательную информацию о локальном имени хоста, параметрах конфигурации сетевых интерфейсов и сервисах, которые должны быть запущены при начальной загрузке.

Архитектура конфигурации трёхуровневая:

/etc/defaults/rc.conf — дефолтные значения всех переменных. Этот файл никогда не редактируется — он перезаписывается при обновлении системы.

/etc/rc.conf — переопределения дефолтов. Именно здесь администратор включает сервисы, задаёт сетевые настройки, определяет hostname.

/etc/rc.conf.local — дополнительные переопределения для конкретной машины. Рекомендуемый подход для кластерных конфигураций: общие настройки в /etc/rc.conf, машино-специфичные в /etc/rc.conf.local. Файл rc.conf распространяется через rsync/puppet, rc.conf.local уникален для каждого хоста.

Помимо этих трёх файлов load_rc_config подхватывает пофайловые фрагменты из /etc/rc.conf.d/<имя сервиса> (и из каталогов rc.conf.d, соседних с каждым каталогом из local_startup); если по такому пути лежит не файл, а каталог, загружается всё его содержимое.

Все эти файлы — это обычный sh(1)-код, который sourced последовательно: переменные, определённые позже, переопределяют более ранние. Типичные записи:

hostname="myserver.example.org"
ifconfig_vtnet0="inet 10.0.0.1 netmask 255.255.255.0"
defaultrouter="10.0.0.254"
sshd_enable="YES"
nginx_enable="YES"

Принцип прост: для сервиса с именем foo переменная foo_enable="YES" включает его при загрузке. Если переменная не установлена или установлена в "NO" — скрипт при загрузке пропустит запуск.

Дополнительные переменные для сервиса foo: foo_flags (аргументы командной строки), foo_user (пользователь), foo_chroot (chroot-директория), foo_cpuset (привязка к CPU), foo_fib (FIB для setfib). Полный список возможностей документирован в rc.conf(5).

rc.subr: фреймворк для скриптов

/etc/rc.subr — библиотека shell-функций, которая реализует всю логику управления сервисами. Именно она обеспечивает единообразное поведение команд start/stop/restart/status для всех скриптов. Благодаря ей типичный скрипт может состоять всего из нескольких строк sh(1)-кода.

Ключевая функция — run_rc_command "$1". Она принимает аргумент (start, stop, restart, status и др.) и выполняет соответствующее действие, включая: проверку rcvar в rc.conf, проверку required_files/required_dirs/required_vars, выполнение *_precmd перед основным действием, запуск/остановку процесса, работу с PID-файлами.

Стандартные аргументы, которые должен поддерживать каждый скрипт: start, stop, restart, status (проверяет, запущен ли сервис), rcvar (показывает, какие переменные rc.conf управляют сервисом), enabled (возвращает 0 если сервис включён).

Дополнительные аргументы: reload (отправляет SIGHUP для перечитывания конфига), poll (ожидает завершения процесса), а также enable, disable и delete, которые правят переменную _enable прямо в конфиге — в том файле, где она уже определена (это может быть и /etc/rc.conf.d/<имя сервиса>), иначе дописывают её в /etc/rc.conf. Скрипты могут определять собственные команды через extra_commands.

Модификаторы поведения:

onestart/onestop — запустить/остановить сервис однократно, игнорируя значение _enable в rc.conf. Безопасный способ запустить сервис, не включённый при загрузке.

forcestart/forcestop — как one*, но дополнительно пропускает все prerequisite-проверки и игнорирует ошибки. Использовать с осторожностью.

faststart — пропускает проверку на уже запущенный процесс.

Анатомия rc.d-скрипта

Минимальный рабочий скрипт для пользовательского сервиса:

#!/bin/sh

# PROVIDE: myapp
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="myapp"
rcvar=myapp_enable

command="/usr/local/sbin/myapp"
pidfile="/var/run/${name}.pid"

load_rc_config $name

: ${myapp_enable:="NO"}

run_rc_command "$1"

Разберём ключевые элементы:

Заголовок с PROVIDE/REQUIRE/KEYWORD — метаданные для rcorder(8). REQUIRE: LOGIN — стандартный выбор для пользовательских сервисов; означает запуск после того, как базовая система полностью инициализирована. KEYWORD: shutdown — означает, что сервис нужно корректно останавливать при выключении (без этого тега rc.shutdown проигнорирует скрипт).

. /etc/rc.subr — подключение фреймворка. Без этой строки ничего не работает.

name="myapp" — идентификатор сервиса. Определяет имя для service(8), переменные в rc.conf (myapp_enable, myapp_flags), и PID-файл по умолчанию.

rcvar=myapp_enable — связывает скрипт с переменной включения в rc.conf.

command — путь к бинарнику. rc.subr автоматически реализует start (запуск command), stop (kill по PID), status (проверка PID).

load_rc_config $name — загружает переменные из rc.conf, относящиеся к данному сервису.

: ${myapp_enable:="NO"} — дефолтное значение, если не задано в rc.conf. Bourne shell-синтаксис: установить переменную, только если она не определена или пуста.

run_rc_command "$1" — передать управление фреймворку.

Для сервисов, которые не демонизируются сами, используется daemon(8):

command="/usr/sbin/daemon"
command_args="-f -p ${pidfile} /usr/local/bin/myapp --some-flags"

Хуки для кастомизации: start_precmd, start_postcmd, stop_precmd, stop_postcmd — функции, вызываемые до/после соответствующих действий. Полезны для проверки конфигурации, создания директорий, ротации логов.

Управление сервисами в runtime

service(8)

Основной инструмент для взаимодействия с rc-скриптами:

service sshd status        # проверить состояние
service sshd start         # запустить (если включён в rc.conf)
service sshd stop          # остановить
service sshd restart       # перезапустить
service sshd reload        # отправить SIGHUP (если поддерживается)
service sshd onestart      # запустить однократно (даже если не включён)
service -e                 # список включённых сервисов
service -l                 # список всех доступных скриптов
service -r                 # полный rcorder-список скриптов, включённых и нет

service(8) — это обёртка, которая находит нужный скрипт в /etc/rc.d/ или /usr/local/etc/rc.d/ и вызывает его с переданным аргументом.

sysrc(8)

Безопасный инструмент для редактирования rc.conf из командной строки:

sysrc nginx_enable=YES           # включить сервис
sysrc nginx_enable=NO            # выключить
sysrc -n nginx_enable            # показать текущее значение
sysrc -x nginx_enable            # удалить переменную из rc.conf
sysrc -a                         # показать все установленные переменные
sysrc -f /etc/rc.conf.local hostname="node1"  # записать в конкретный файл

sysrc предпочтительнее ручного редактирования — он корректно обрабатывает кавычки, проверяет синтаксис и работает атомарно.

Расположение скриптов

/etc/rc.d/ — скрипты base system. Сюда попадают rc-скрипты для sshd, cron, syslogd, pf, devd и других системных сервисов. Редактировать их не нужно и не рекомендуется — при обновлении системы они перезаписываются.

/usr/local/etc/rc.d/ — скрипты из пакетов и портов. nginx, postgresql, redis и прочий сторонний софт устанавливает свои скрипты сюда. Это следствие архитектурного разделения base/local, описанного в статье о файловой структуре.

Начиная с FreeBSD 6.1-RELEASE, локальные rc.d-скрипты (включая установленные из портов) включаются в общий rcorder(8) базовой системы. Это значит, что пользовательский скрипт в /usr/local/etc/rc.d/ может объявлять зависимости от системных сервисов — и rcorder корректно разрешит порядок запуска.

Service Jails

Механизм появился в FreeBSD 15.0; в ветке 14.x его нет — ни rc.subr, ни rc.conf(5) там про svcj ничего не знают. Service Jails позволяют автоматически запускать rc.d-сервис внутри jail(8) с минимальной конфигурацией — не более двух строк.

В отличие от ручной настройки jail с отдельной файловой системой и сетью, service jail наследует файловую систему хоста, но применяет jail-ограничения: изоляция процессов, ограничение сетевого доступа, запрет монтирования файловых систем.

Включение для конкретного сервиса:

nginx_svcj_options="net_basic"
nginx_svcj="YES"

Это не имеет тех же преимуществ безопасности, что и ручная настройка jail с отдельной файловой системой и IP/VNET, но значительно проще в настройке, обеспечивая при этом часть преимуществ jail — например, сокрытие процессов других пользователей.

Все базовые системные сервисы уже содержат конфигурацию для service jails (переменные *_svcj_options), хотя не все протестированы. Подробности — в rc.conf(5), раздел "SERVICE JAILS".

Отладка

Если сервис не запускается, последовательность диагностики:

service foo rcvar — показывает, какие переменные rc.conf управляют сервисом и их текущие значения. Первое, что нужно проверить.

service foo onestart — попытка запуска вне зависимости от rc.conf. Если работает — проблема в конфигурации, не в скрипте.

Чтение скрипта: less /usr/local/etc/rc.d/foo — скрипт целиком доступен для анализа, включая все зависимости, переменные и хуки.

sh -x /etc/rc.d/foo start — запуск с трассировкой shell. Показывает каждую исполняемую строку — наиболее мощный инструмент отладки.

Для диагностики порядка загрузки: rcorder /etc/rc.d/* /usr/local/etc/rc.d/* 2>/dev/null | grep foo — показывает позицию скрипта в последовательности.

Сравнение с systemd

Rc и systemd решают одну задачу, но с фундаментально разными подходами.

Rc — это shell-скрипты с последовательным исполнением. Полная прозрачность: любой администратор, знающий sh(1), может прочитать и понять любой аспект поведения системы. Порядок загрузки детерминирован — одинаков при каждой загрузке. Расширение — написание shell-скрипта. Недостатки: последовательный запуск (медленнее на системах с большим количеством сервисов), нет сокет-активации, нет встроенного мониторинга процессов (restart-on-failure требует daemon -r), нет нативной интеграции с cgroups.

Systemd — единый бинарный PID 1 с декларативными unit-файлами. Параллельный запуск, сокет-активация, встроенные cgroups, journald, таймеры, мониторинг состояния. Значительно сложнее внутри, отладка менее прозрачна, единая точка отказа.

Для серверных задач с десятками сервисов разница во времени загрузки обычно незначительна. Преимущество rc — в предсказуемости и простоте отладки, что на production-серверах часто важнее скорости загрузки.

Источники


Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.