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

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

Комментарии (7)


  1. esinev
    06.08.2015 13:43

    Спасибо за статью, не знал что есть замена daemontools. Хотя мы сейчас переходим с нее на systemd и upstart.
    У daemontools нельзя указывать зависимости.

    Для чего сначала монтируете корневой раздел для чтения, а потом на запись?

    mount -o remount,ro /
    
    ip link set up dev lo
    echo $HOSTNAME > /proc/sys/kernel/hostname
    
    mount -o remount,rw /
    


    И почему нельзя оставить только на чтение?


    1. crazybrake
      06.08.2015 13:46

      конечно, можно смело оставлять только на чтение. видимо, на момент правки скрипта rw нужно было для какого-то из сервисов или для экспериментов.


    1. semlanik
      06.08.2015 21:02

      Сильно заметна разница в upstart и аналогичной загрузке sysvinit + rc? Я тоже ищу альтернативу sysvinit, до проверки upstart еще не дополз.


      1. esinev
        07.08.2015 01:33

        upstart мы не использовали, но как уже написали ниже,
        если выкинуть все ненужное из стандартного sysvinit, то разницы в загрузке быть не должно.

        Другое дело systemd, который может не запускать сервисы, пока они не будут использованы.


  1. semlanik
    06.08.2015 17:26

    Вы немного слукавили когда приписали rc скрипт к sysvinit. sysvinit сам по себе занимается чтением как раз /etc/inittab. А как вы сконфигурируете initab это уже ваше личное дело. Очень хотелось бы увидеть сравнение runit и sysvinit c /etc/inittab примерно следующего содержания:

    0:0:wait:/etc/runit/3
    1:1:wait:/etc/runit/1
    3:3:wait:/etc/runit/2
    5:5:wait:/etc/runit/2
    6:6:wait:/etc/runit/3

    Тогда сравнение будет честным


    1. crazybrake
      06.08.2015 19:06
      +1

      само-собой, никто не мешает прописать в inittab всё, что угодно, сохранив механизм переключения runlevel'ов. мне кажется, что в большинстве современных применений оно не особенно нужно. single, multi-user и halt вполне достаточно.
      а так — да, можно сказать, что если rc-скрипт не использовать, разница на глаз будет незаметна. но идеология чуть другая.
      и исполняемый файл runit-init где-то в шесть раз меньше, чем init ;)


      1. semlanik
        06.08.2015 20:56

        Ну учитывая то что 2 из 6 ранлевелов не используются вовсе(я не видел применения), не могу не согласится с вами. Просто реально большую часть времени съедает сама процедура загрузки приложений и обработки sh скриптов нежелени init. Я и сам смотрел в сторону различных init, но в итоге пришел к выводу, что sysvinit на самом деле не так уж и плох с точки зрения fastboot.