Ускоряем запуск BeagleBone или runit не для чайников

от автора

В устройствах, которые мы разрабатываем и производим, требуется быстрый запуск после холодного старта. Для приборов без полноценной операционной системы (в них мы используем NutOS, он же EtherNut) такой проблемы нет — они готовы к работе через пару секунд после включения. Зато в более сложных и продвинутых, с linux внутри, и особенно в портативных измерительных системах, вопрос ускорения алгоритмов инициализации более чем актуален.
В пилотной версии своего коммутатора 10G ethernet мы использовали хорошо известную плату Beaglebone и процесс загрузки, если не считать qemu-эмулятор, с удовольствием отлаживали на ней. Кстати, эта пилотная версия 10-гигабитного свича с управляющей beaglebone-платой (на фотографии к статье) стоит у нас в серверной и пару лет успешно работает,
Сразу скажу, что переход на runit дал ускорение запуска системы на 500MHz процессоре с полуминуты до шести с копейками секунд.

Disclaimer: эта заметка была написана для внутреннего wiki нашей компании, и, поскольку далеко не все разработчики ПО системные администраторы, я посчитал нужным объяснить некоторые моменты максимально простым и понятным языком.

Введение: как есть и что можно изменить

Попробую коротко описать проблему.
Обычно система linux загружается следующим образом:

  1. начальный загрузчик (lilo, grub, u-boot, …)
  2. ядро
  3. программа init
  4. всё остальное (то, что описано в /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-скриптов:

  1. /etc/runit/1 — начальный старт
  2. /etc/runit/2 — multi-user mode (аналогично runlevel 2)
  3. /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-скриптов

/etc/runit/1

#!/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=$(</sys/class/tty/console/active)   tty=/dev/${tty##* }   exec setsid sh -c "exec bash /etc/runit/1 <$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 

/etc/runit/2

#!/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: ...........................................................................................................................................................................................................................................................................................................................................................................................................' 

/etc/runit/3

#!/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

ссылка на оригинал статьи http://habrahabr.ru/post/264263/


Комментарии

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

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