Ускоряем запуск BeagleBone или runit не для чайников
В устройствах, которые мы разрабатываем и производим, требуется быстрый запуск после холодного старта. Для приборов без полноценной операционной системы (в них мы используем NutOS, он же EtherNut) такой проблемы нет — они готовы к работе через пару секунд после включения. Зато в более сложных и продвинутых, с linux внутри, и особенно в портативных измерительных системах, вопрос ускорения алгоритмов инициализации более чем актуален.
В пилотной версии своего коммутатора 10G ethernet мы использовали хорошо известную плату Beaglebone и процесс загрузки, если не считать qemu-эмулятор, с удовольствием отлаживали на ней. Кстати, эта пилотная версия 10-гигабитного свича с управляющей beaglebone-платой (на фотографии к статье) стоит у нас в серверной и пару лет успешно работает,
Сразу скажу, что переход на runit дал ускорение запуска системы на 500MHz arm-процессоре с полминуты до шести с копейками секунд.
Disclaimer: эта заметка была написана для внутреннего wiki нашей компании, и, поскольку далеко не все разработчики ПО системные администраторы, я посчитал нужным объяснить некоторые моменты максимально простым и понятным языком.
Введение: как есть и что можно изменить
Попробую коротко описать проблему.
Обычно система linux загружается следующим образом:
- начальный загрузчик (lilo, grub, u-boot, …)
- ядро
- программа init
- всё остальное (то, что описано в /etc/inittab) запускается этой самой программой init
Не буду рассказывать про runlevel’ы. Для этого есть man: man runlevel, man init, man inittab.
init — основной процесс системы и он, так или иначе, управляет всеми остальными программами, и даже номер процесса у него — 1. Если совсем упрощённо, то его задача — запускать и перезапускать программы, перечисленные в файле /etc/inittab.
Но вернёмся к процессу загрузки.
Авторы дистрибутивов, придерживающихся так называемого System V (sysv) порядка загрузки, почему-то решили, что init должен вызывать один и тот же скрипт rc с параметром, определяющим уровень выполнения (runlevel). Это можно расценивать так же, как, например, если бы все дети ходили в школу с одним учебником, только открывали его на разных страницах, в зависимости от того, в каком они классе учатся. Можно себе представить, какой толщины должен был быть такой учебник!
С последовательностью старта системы linux практически такая же ситуация.
Вот, к примеру, кусочек из /etc/inittab:
id:2:initdefault: # Boot-time system configuration/initialization script. # This is run first except when booting in emergency (-b) mode. si::sysinit:/etc/init.d/rcS # What to do in single-user mode. ~~:S:wait:/sbin/sulogin # Runlevel 0 is halt. # Runlevel 1 is single-user. # Runlevels 2-5 are multi-user. # Runlevel 6 is reboot. l0:0:wait:/etc/init.d/rc 0 l1:1:wait:/etc/init.d/rc 1 l2:2:wait:/etc/init.d/rc 2 l3:3:wait:/etc/init.d/rc 3 l4:4:wait:/etc/init.d/rc 4 l5:5:wait:/etc/init.d/rc 5 l6:6:wait:/etc/init.d/rc 6
Скрипт /etc/init.d/rc содержит больше 300 (!) строк кода для shell’а. А делает, в общем-то, всего ничего: последовательно запускает программы из /etc/rc?.d в зависимости от этапа выполнения (runlevel’а).
Едем дальше. На начальном этапе (runlevel S, см. выше или внутрь /etc/inittab) выполняется всё, что находится в каталоге /etc/rcS.d/. это 20 скриптов от hostname до монтирования nfs и иницализации random-генератора.
После выполнения этой части загрузочного процесса init переходит в multi-user режим и выполняет скрипты из /etc/rc2.d/. В самом простом случае их около десяти штук.
При добавлении новых программ (сервисов, демонов etc) установочные пакеты обычно добавляют к процессу загрузки ещё один-два скрипта и время запуска системы опять увеличивается.
Если настольный компьютер или ноутбук или даже сервер могут не торопиться с загрузкой, то приборы, которые мы выпускаем, должны быть готовы к работе после включения как можно быстрее. для таких устройств, как Беркут-ЕТ, проблем нет — там мы полностью контролируем процесс, а для Беркут-ММТ вопрос быстрого старта до недавнего времени оставался открытым.
Так вот, набор программ runit даёт возможность максимально просто и полностью управлять процедурой старта системы.
Фактически, старт linux (не только linux, можно и bsd-like загружать, в общем случае) при использовании runit сводится к запуску ядром программы runit-init, которая управляет работой трёх shell-скриптов:
- /etc/runit/1 — начальный старт
- /etc/runit/2 — multi-user mode (аналогично runlevel 2)
- /etc/runit/3 — выключение/перезагрузка (poweroff, shutdown, halt)
Каждый скрипт обычно занимает один-два экрана команд и делает только то, что необходимо. Благодаря простоте, прозрачности и, самое главное, небольшому объёму сценариев администратор или разработчик установочного образа системы легко и полностью может управлять процессом.
Остальные программы и сервисы обычно стартуют под управлением программы runsvdir, а зависимости, приоритеты и порядок запуска определяются администратором. Пример работы с runsvdir уже есть на Хабре, поэтому детально это описывать здесь не буду.
В логе загрузки Bercut.BeagleBone можно обратить внимание на время, которое система затратила на выполнение начального скрипта (/etc/runit/1) и время до появления приглашения «login:»:
20:06:22.33157 Uncompressing Linux... done, booting the kernel. 20:06:22.99351 - runit: $!Id: 25da3b86f7bed4038b8a039d2f8e8c9bbcf0822b $: booting. 20:06:22.99351 - runit: enter stage: /etc/runit/1 20:06:24.07955 - runit: leave stage: /etc/runit/1 20:06:24.07955 - runit: enter stage: /etc/runit/2 20:06:25.41956 20:06:25.41957 Debian GNU/Linux 7.0 x10-02 ttyO0 20:06:25.41957 20:06:25.41957 x10-02 login:
Как видим, процесс загрузки системы (процессор arm 500 MHz) занял около трёх секунд. и это с учётом старта ядра!
Процесс перехода на: шаг за шагом
Процесс специально описан в общих чертах, чтобы заставить задуматься и понять, как оно устроено, а не тупо бездумно скопировать скрипты в систему и удивляться, почему не работает.
Шаг 1
Установить в систему runit. Как угодно, в виде чучела или тушки пакета или собрать из исходников.
Шаг 2
Убедиться, что сервисы под управлением runsvdir стартуют, работают и для них ведётся лог. Если вы никогда не пользовались программами из серии daemontools или runit (без замены init), то дальше можно не читать.
Шаг 3
Настроить сервис для консольного логина. Если вы никогда не пользовались программами из серии daemontools или runit (без замены init), то вам будет тяжело и дальше можете не читать. Sorry.
Скрипт run для пятой консоли (Alt-F5) может, например, выглядеть так:
#!/bin/sh
exec 2>&1
exec setsid /sbin/agetty --nohostname tty5 38400 linux
Шаг 4
В каталог /etc/runit установить шелл-скрипты с запоминающимися названиями 1, 2, 3 (раз, два, три). В примерах это bash-скрипты, но, в принципе, никто не мешает им быть sh-скриптами или даже perl-программами. Впрочем, а зачем?
Скрипт 1 выполняется на первом этапе загрузки (аналог rcS, single-user mode), скрипт 2 — основной режим работы системы, 3 — shutdown, reboot и poweroff.
Шаг 5
Перезагрузить систему, передав ядру параметр init=/sbin/runit-init.
Шаг 6
Смотреть на результат и пытаться понять, что не так;)
Шаг 7
Если удалось разобраться, что не так и добиться появления приглашения к вводу логина на 5-й консоли, то я вас поздравляю: БОльшая часть работы сдалана! Теперь можно прикручивать остальные сервисы (udev etc.)
Скрипты 1, 2, 3 — внизу этой страници под спойлерами (см. ниже). Обратите внимание на объём кода. удивительно, что этого достаточно для полноценной загрузки операционной системы!
лог загрузки платы Bercut.BeagleBone
С момента «холодного» старта до появления приглашения login: проходит всего 6.1019 секунд. По-моему, весьма неплохо.
20:06:19.31769 U-Boot SPL 2011.09-00000-gf63b270-dirty (Apr 24 2012 - 09:51:01) 20:06:19.52354 Texas Instruments Revision detection unimplemented 20:06:19.94867 No AC power, disabling frequency switch 20:06:19.94868 OMAP SD/MMC: 0 20:06:19.94868 reading u-boot.img 20:06:19.94868 reading u-boot.img 20:06:19.94868 20:06:19.94868 20:06:19.94869 U-Boot 2011.09-00000-gf63b270-dirty (Apr 24 2012 - 09:51:01) 20:06:20.20756 20:06:20.20756 I2C: ready 20:06:20.20756 DRAM: 256 MiB 20:06:20.39067 No daughter card present 20:06:20.39067 NAND: HW ECC Hamming Code selected 20:06:20.39067 nand_get_flash_type: unknown NAND device: Manufacturer ID: 0x10, Chip ID: 0x10 20:06:20.39970 No NAND device found!!! 20:06:20.39970 0 MiB 20:06:20.39970 MMC: OMAP SD/MMC: 0 20:06:20.39970 *** Warning - readenv() failed, using default environment 20:06:20.65954 20:06:20.65954 Net: cpsw 20:06:20.65955 Hit any key to stop autoboot: 0 20:06:21.54266 SD/MMC found on device 0 20:06:21.54266 reading uEnv.txt 20:06:21.54266 20:06:21.54266 202 bytes read 20:06:21.54266 Loaded environment from uEnv.txt 20:06:21.54266 Importing environment from mmc ... 20:06:21.74756 reading uimage 20:06:21.87867 20:06:21.87868 3083432 bytes read 20:06:21.87869 ## Booting kernel from Legacy Image at 80007fc0 ... 20:06:21.87870 Image Name: Angstrom/3.2.18/beaglebone 20:06:21.88761 Image Type: ARM Linux Kernel Image (uncompressed) 20:06:21.88761 Data Size: 3083368 Bytes = 2.9 MiB 20:06:21.94465 Load Address: 80008000 20:06:21.94465 Entry Point: 80008000 20:06:21.94465 Verifying Checksum ... OK 20:06:21.94465 XIP Kernel Image ... OK 20:06:22.33156 OK 20:06:22.33156 20:06:22.33156 Starting kernel ... 20:06:22.33156 20:06:22.33157 Uncompressing Linux... done, booting the kernel. 20:06:22.99351 - runit: $!Id: 25da3b86f7bed4038b8a039d2f8e8c9bbcf0822b $: booting. 20:06:22.99351 - runit: enter stage: /etc/runit/1 20:06:24.07955 - runit: leave stage: /etc/runit/1 20:06:24.07955 - runit: enter stage: /etc/runit/2 20:06:25.41956 20:06:25.41957 Debian GNU/Linux 7.0 x10-02 ttyO0 20:06:25.41957 20:06:25.41957 x10-02 login:
Примеры runit-скриптов
#!/bin/bash
# system one time tasks
PATH=/sbin:/bin:/usr/sbin:/usr/bin
# re-exec this script with a controlling tty
if [ "$(tty)" = "/dev/console" ]; then
mountpoint -q /sys || \
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mountpoint -q /proc || \
mount -t proc proc /proc -o nosuid,noexec,nodev
tty=$($tty 2>&1"
fi
trap : INT TSTP QUIT
trap 'sulogin -p <>$(tty) 2>&1' ERR
mountpoint -q /proc || \
mount -t proc proc /proc -o nosuid,noexec,nodev
mountpoint -q /sys || \
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mountpoint -q /dev || \
mount -t devtmpfs dev /dev -o mode=0755,nosuid
test -e /dev/fd || ln -s /proc/self/fd /dev/fd
exec 2> >(sed 's/^/:: /')
# set -v
. /etc/rc.conf
mountpoint -q /run || \
mount -t tmpfs run /run -o mode=0755,nosuid,nodev
mountpoint -q /dev || \
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mkdir -p -m0755 /run/runit /run/lock /run/user /dev/pts /dev/shm \
/run/network /run/runit
mountpoint -q /dev/pts ||
mount -n -t devpts devpts /dev/pts -o mode=0620,gid=5,nosuid,noexec
mountpoint -q /dev/shm || \
mount -n -t tmpfs shm /dev/shm -o mode=1777,nosuid,nodev
mkdir -p -m 0755 /var/lib/supervise
mountpoint -q /var/lib/supervise || \
mount -n -t tmpfs tmpfs /var/lib/supervise -o mode=1777,nosuid,nodev
#mkdir -p -m 0755 /var/log
#mountpoint -q /var/log || \
# mount -n -t tmpfs tmpfs /var/log -o mode=1777,nosuid,nodev
mount -o remount,ro /
ip link set up dev lo
echo $HOSTNAME > /proc/sys/kernel/hostname
mount -o remount,rw /
mount -a -t "nosysfs,nonfs,nonfs4,nosmbfs,nocifs" -O no_netdev
#cp /var/lib/random-seed /dev/urandom &>/dev/null || true
#( umask 077; dd if=/dev/urandom of=/var/lib/random-seed count=1
#bs=512 &>/dev/n ull )
install -m0664 -o root -g utmp /dev/null /var/run/utmp
install -m0 /dev/null /run/runit/stopit
install -m0 /dev/null /run/runit/reboot
install -m0644 -o root -g utmp /dev/null /var/log/lastlog
dmesg > /var/log/dmesg
#!/bin/bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin
exec 2> >(sed 's/^/:: /')
# set -v
. /etc/rc.conf
# sysctl --system
for i in ${DAEMONS[@]}; do
d=/var/lib/supervise/${i}
mkdir -p $d ${d}.log
l=/var/log/${i}
mkdir -p $l
done
exec env - PATH=$PATH \
runsvdir -P /service 'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................'
#!/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
exec 2> >(sed 's/^/:: /')
# set -v
LAST=0
test -x /etc/runit/reboot && LAST=6
echo 'Waiting for services to stop...'
sv -w196 force-stop /etc/service/*
sv exit /etc/service/*
stty onlcr
echo Shutdown...
udevadm control --exit
killall5 -15
i=10; while killall5 -18 && (( i-- )) ; do echo -n .; sleep 0.5; done; echo
killall5 -9
umount /tmp
mount -o remount,ro /
sleep 1
sync
Ссылки
cr.yp.to/daemontools.html — предок runit (от D.J. Bernstein’а)
smarden.org/runit — runit собственной персоной
smarden.org/socklog — альтернатива syslog’у
github.com/chneukirchen/ignite — набор init-скриптов для runit
lwn.net/Articles/331818 — про то, как можно жить без udevd
metrotek.spb.ru/x10–24.html — коммутатор 10G, в котором используется описанный механизм
habrahabr.ru/post/21205 — Как загружается Linux