{{tag>debian remove systemd sysv system init}}

====== Как загружается Linux ======


Когда я осваивал Linux, мне было очень интересно что происходит при загрузке системы. Попытка разобраться в процессе загрузки привела меня в исходники загрузочных скриптов (/etc/inittab, /etc/rc*, /etc/init.d/*, ...) и их конфигов (/etc/sysconfig/*, /etc/cond.f/*, ...). Надо отметить серьёзные размеры и сложность этих скриптов - чтобы в них разобраться потребовалось немало времени. Но я в те времена искренне верил, что загрузка это сложный процесс, и что размеры и сложность загрузочных скриптов вполне оправданы.
Когда меня окончательно достал RedHat, я решил собрать свой дистрибутив на базе LFS. Для своего дистрибутива пришлось самостоятельно разрабатывать загрузочные скрипты, и тут-то выяснилась правда: ничего сложного в процессе загрузки нет!
Проработав 2.5 года на своём дистрибутиве (PoWeR Linux) я мигрировал на Gentoo (на качественную поддержку своего просто не хватало времени). Изучив загрузочные скрипты Gentoo я пришёл в ужас! Их размеры и сложность были ещё больше, чем у старого RedHat. После детального изучения стала ясна причина: один и тот же комплект загрузочных скриптов использовался и для LiveCD и для обычной системы - такой себе универсальный монстрик.\\
<text>   </text>Так что при переходе на Gentoo я решил взять загрузочные скрипты из PoWeR Linux а стандартные Gentoo-шные не использовать (т.е. у меня от Gentoo используется только portage). И с тех пор ещё 4 года эти скрипты работают у меня на домашней рабочей станции и кучке удалённых серверов.

===== Характеристики =====

Размер скриптов (всех вместе) - 316 строк, 7.5KB:
  $ wc 1 3 lib.sh 
   199  703 5152 1
    89  272 1671 3
    28  118  770 lib.sh
   316 1093 7593 итого
==== Минусы: ====

Всё в одном файле - при обновлении приложений практически невозможно автоматически обновить код инициализации этого приложения. Например, когда обновляется ALSA, то пакет может просто заменить файлы /etc/init.d/alsasound, /etc/conf.d/alsasound, /etc/modules.d/alsa. А в моём случае админу нужно будет ручками править /etc/runit/1.
Нет поддержки всего на свете. Например, я не использую RAID и LVM - так что команды для их инициализации вам нужно будет добавлять самостоятельно.
Вам нужно будет самостоятельно поддерживать эти скрипты. Я обычно при обновлении Gentoo поглядываю на изменения в (неиспользуемых мной) /etc/init.d/* скриптах и если меняется что-то важное - обновляю свои скрипты. Но, на практике, необходимость в таких изменениях возникает примерно раз в два года.
==== Плюсы: ====

Всё в одном небольшом файле - нет необходимости разыскивать по куче скриптов и их конфигов где настраивается то, что вам понадобилось; можно быстро и легко увидеть все базовые настройки системы.

Есть поддержка всего, что мне и моим друзьям за 6.5 лет было нужно на домашних компах и серверах.

Идеальны для изучения процесса инициализации Linux. Вы работаете с реальными базовыми командами Linux, которые одинаковы во всех дистрибутивах, а не со скриптами и конфигами специфичными для вашего дистрибутива.

Ускоряют загрузку системы. У меня домашняя машина грузится в single user mode (6 консолек с getty, syslog, klog, gpm) за 16.5 секунд. Я экспериментировал с параллельной загрузкой в стиле initng - эффект скорее отрицательный за счёт усложнения скриптов и порождения лишних процессов. Initng хорош для ускорения загрузки традиционных, раздутых скриптов, выполняющих множество ненужных действий, а в моём случае ускорять просто нечего. :)
Не смотря на малый размер эти скрипты не только надёжно и быстро грузят систему, но и поддерживают несколько фич облегчающих жизнь админу:

Выполняемые команды сгруппированы в блоки по типам. В процессе загрузки выводятся названия блоков, с информацией были ли ошибки при выполнении команд блока.
Если при выполнении блока команд были ошибки - выводится детальная информация по командам этого блока и возникшим ошибкам, после чего система ждёт 5 секунд нажатия любой кнопки для запуска bash и ручного исправления ошибок. После выхода из bash предлагается либо продолжить загрузку либо перегрузиться. Если ничего не нажимать, загрузка продолжится.
Логи всего, что выводится при загрузке и отключении на экран (названия блоков команд и информация по возникающим ошибкам) сохраняются в файлах /var/log/boot, /var/log/shutdown. Благодаря этому можно на удалённых серверах посмотреть, как проходила загрузка/отключение.
Пример сообщений выводимых при загрузке
   + UDEV
   + MODULES
   + SYSCTL
   + MTAB
   - MOUNTALL
  ++ swapon -a
  ++ false
  EXIT CODE: 1
  ++ mount -at nocoda,nonfs,noproc,noncpfs,nosmbfs,noshm
  ... press any key in 5 seconds to open shell ...
   + CLEANTMP
   + RANDOMSEED
   + HWCLOCK
   + SENSORS
   + LOADKEYS
   + SOUND
   + HOST_NAME
   + ENVUPDATE
   + NETWORK
   + RUNIT
   + DMESG
  
====== Runit ======


Для загрузки я вместо SysV init использую Runit. Runit не поддерживает /etc/inittab, вместо этого в нём используется простая схема:
При загрузке запускается скрипт /etc/runit/1. Его задача полностью проинициализировать систему.
По завершению скрипта /etc/runit/1 запускается скрипт /etc/runit/2, который должен запустить все необходимые сервисы (syslog, getty, ssh, apache, ...).
Когда пользователь останавливает/перегружает систему запускается скрипт /etc/runit/3 который должен подготовить систему к отключению (завершить все процессы, отмонтировать диски, etc.).
При желании можно настроить SysVinit для работы в том же стиле:
Запуск /etc/runit/{1,2,3} из SysVinit: /etc/inittab

  id:3:initdefault:
  rc::bootwait:/etc/runit/1
  l0:0:wait:/bin/sh -c '/etc/runit/3; exec /sbin/halt'
  l3:3:once:/etc/runit/2
  l6:6:wait:/bin/sh -c '/etc/runit/3; exec /sbin/reboot'
  ca:12345:ctrlaltdel:/sbin/shutdown -r now

Сервисы
Для запуска всех сервисов (getty, syslog, mysql, etc.) я использую тот же runit (по сути это просто немного улучшенный вариант daemontools). Но это отдельная большая тема, так что просто уточню что в этой статье скриптов для запуска сервисов нет, здесь только инициализация/отключение системы.

Исходники
Вспомогательные функции: **/etc/runit/lib.sh**
  #!/bin/bash
  startlog() { exec 3>&1 4>&2 1> >(tee $1) 2>&1; }
  stoplog() { exec 1>&3- 2>&4-; }
  
  wanna() {
      echo -e "\a... press any key in $2 seconds to $1 ..."
      read -t $2 -n 1 -s </dev/console
  }
  emergency() {
      if wanna "open shell" 5; then
          bash --norc </dev/console &>/dev/console
          if [[ "$0" == "/etc/runit/1" ]] && wanna "reboot now" 3; then
              exit 100
          fi
      fi
  }
  
  trace() { trap 'ERR=$?' ERR; set -Ex; $1 2>&1; set +Ex; trap ERR; } 2>&-
  try() {
      local output=$( trace $1 )
      if [[ "$output" =~ "ERR=" ]]; then
          echo -e "\e[1m\e[31m - \e[37m$1\e[0m"
          echo "$output" | sed $'s/.*ERR=\(.*\)/\a\033[36mEXIT CODE: \\1\033[0m/g'
          emergency
      else
          echo -e "\e[1m\e[32m + \e[37m$1\e[0m"
      fi
  }
Startup: **/etc/runit/1**
  #!/bin/bash
  
  CONSOLE() {
      dmesg -n 1
  }
  
  INIT() {
      mount -n -t proc  none /proc
      mount -n -t sysfs none /sys
      mount -n -t ramfs none /dev
      mknod -m 660 /dev/console c 5 1
      mknod -m 660 /dev/null c 1 3
      ln -snf /proc/self/fd /dev/fd   # needed for startlog in /etc/runit/lib.sh
  }
  
  UDEV() {
      udevstart
  
      ### Standard add-on
      ln -snf /proc/self/fd/0 /dev/stdin
      ln -snf /proc/self/fd/1 /dev/stdout
      ln -snf /proc/self/fd/2 /dev/stderr
      ln -snf /proc/kcore /dev/core
      mkdir /dev/pts
      mkdir /dev/shm
      mount -n -t devpts -o gid=5,mode=0620 none /dev/pts
  
      ### My custom add-on
      ln -s vcs /dev/vcs0
      ln -s dvd2 /dev/dvd
      ln -s cdrom2 /dev/cdrom
      mkdir /dev/lirc
      mknod /dev/lirc/0 c 61 0
      mknod /dev/vmmon c 10 165
      mknod /dev/sdb  b 8  16
      mknod /dev/sdb1 b 8  17
  }
  
  MODULES() {
      update-modules
  
      ### Network
      modprobe -q skge
      modprobe -q 8139too media=56 # 1=10half 17=10full 56=100full
      modprobe -q sky2
  
      ### Lirc
      setserial /dev/ttyS1 uart none
      modprobe -q lirc_serial io=0x2f8 irq=3 # io=0x3f8 irq=4
  
      ### VMware
      modprobe -q vmmon
      modprobe -q vmnet
  }
  
  SYSCTL() {
      sysctl -p /etc/sysctl.conf
  }
  
  MTAB() {
      # Adding already-mounted fs to /etc/mtab
      >/etc/mtab
      mount -f /
      awk '$2 != "/" {print}' /proc/mounts >> /etc/mtab
      for i in $(cut -d ' ' -f 2 </etc/mtab); do mount -f -o remount "$i"; done
  }
  
  MOUNTALL() {
      swapon -a
      mount -at nocoda,nonfs,noproc,noncpfs,nosmbfs,noshm
  }
  
  CLEANTMP() {
      # Cleaning up temporary files and locks, prepare utmp & wtmp
      rm -rf /var/lib/net-scripts/state/*
      rm -rf /var/run/console.lock /var/run/console/* # reset pam_console
      find /var/lock -type f -print0 | xargs -0 rm -f --
      find /var/run ! -type d ! -name utmp ! -name innd.pid \
          ! -name random-seed -exec rm -f -- {} \;
      > /var/lock/.keep
      rm -f /tmp/.X*-lock /tmp/esrv* /tmp/kio* /tmp/jpsock.* /tmp/.fam* /tmp/iceauth.* /tmp/xauth.*
      rm -rf /tmp/.esd* /tmp/orbit-* /tmp/ssh-* /tmp/ksocket-* /tmp/.*-unix
      rm -rf /tmp/.{ICE,X11}-unix
      mkdir -p /tmp/.{ICE,X11}-unix
      chmod 1777 /tmp/.{ICE,X11}-unix
  }
  
  RANDOMSEED() {
      # Restoring random-seed.
      [ -f /var/run/random-seed ] && cat /var/run/random-seed >>/dev/urandom
      rm -f /var/run/random-seed
      ( umask 077 ; dd if=/dev/urandom of=/var/run/random-seed count=1 2>/dev/null )
  }
  
  HWCLOCK() {
      if [ ! -f /etc/adjtime ]; then echo "0.0 0 0.0" > /etc/adjtime ; fi
      hwclock --adjust --localtime
      hwclock --hctosys --localtime
  }
  
  SENSORS() {
      sensors -s
  }
  
  LOADKEYS() {
      # Commands for TTY initialization like 'setfont' and 'echo -ne "\033(K"'
      # shouldn't be executed in /etc/runit/1 because:
      # - which TTYs should be initialized may depend on current runlevel
      # - if TTY state become broken (for ex. after 'cat /dev/urandom'),
      #   then after logout and login TTY state should be reinitialized
      # these commands should be executed before each getty invocation instead.
      loadkeys koi2   # -q windowkeys
  }
  
  SOUND() {
      alsactl -f /etc/asound.state restore
  }
  
  HOST_NAME() {
      hostname home
      echo "HOSTNAME='$(hostname)'" >/etc/env.d/01hostname
  }
  
  ENVUPDATE() {
      env-update.sh -u
  }
  
  NETWORK() {
      ifconfig lo 127.0.0.1 up
      route add -net 127.0.0.0 netmask 255.0.0.0 gw 127.0.0.1 dev lo
      iptables-restore </etc/iptables
      ifconfig eth2 192.168.2.1 broadcast 192.168.2.255 netmask 255.255.255.0
      ifconfig eth2:0 192.168.2.254
      ifconfig eth1 192.168.1.2 broadcast 192.168.1.255 netmask 255.255.255.0
      ifconfig eth0 192.168.10.2 broadcast 192.168.10.255 netmask 255.255.255.0
      #route add default gw 192.168.1.1 dev eth1
  }
  
  RUNIT() {
      # Set default action (shutdown or not) if Ctrl+Alt+Del pressed,
      # but /etc/runit/ctrlaltdel don't setup /etc/runit/stopit.
      touch /etc/runit/stopit
      chmod 100 /etc/runit/stopit
      
      # Set default action on shutdown (halt or reboot) if:
      # - /etc/runit/1 crash or exit 100
      # - /etc/runit/2 exit non 111
      # - Ctrl+Alt+Del pressed, but /etc/runit/ctrlaltdel don't setup /etc/runit/reboot
      touch /etc/runit/reboot
      chmod 100 /etc/runit/reboot
      
      # Set runlevel to:
      # - single      if kernel has param: S
      # - RUNLEVELNAME    if kernel has param: runlevel=RUNLEVELNAME
      # - default     if kernel has no params or unable to set requested runlevel
      grep -q '\(^\| \)S\( \|$\)' /proc/cmdline && runlevel='single'
      runsvchdir ${runlevel:-default} || runsvchdir default
  }
  
  SEND_MAIL() {
      echo -e "To: root\nSubject: reboot at $(date)" | sendmail -t
  }
  
  DMESG() {
      # Create an 'after-boot' dmesg log
      touch /var/log/dmesg
      chmod 640 /var/log/dmesg
      dmesg > /var/log/dmesg
  }
  
  
  PATH=/sbin:/usr/sbin:/bin:/usr/bin
  trap ':' INT QUIT TSTP
  . /etc/runit/lib.sh
  
  try CONSOLE
  try INIT
  startlog /var/log/boot
  try UDEV
  try MODULES
  try SYSCTL
  try MTAB
  try MOUNTALL
  try CLEANTMP
  try RANDOMSEED
  try HWCLOCK
  try SENSORS
  try LOADKEYS
  try SOUND
  try HOST_NAME
  try ENVUPDATE
  try NETWORK
  try RUNIT
  #try SEND_MAIL
  try DMESG
  stoplog
  
  # Select next stage (exit 0 for stage 2, exit 100 for stage 3):
  exit 0
Shutdown: **/etc/runit/3**
  #!/bin/bash
  
  CONSOLE() {
      chvt 1
      # Required in case getty was last process in this console and it leave
      # console in broken state (\n work as <LF> without <CR>).
      { stty sane ; echo ; } >/dev/console
  }
  
  TERM() {
      # Give a chance for all processes for clean exit.
      # This also will kill all 'runsvdir' and signal all 'runsv' to exit.
      killall5 -15
  }
  
  HWCLOCK() {
      hwclock --systohc --localtime
  }
  
  SERVICES() {
      sv force-stop /var/service/* &>/dev/null || :
  }
  
  SOUND() {
      alsactl -f /etc/asound.state store
  }
  
  RANDOMSEED() {
      umask 077
      dd if=/dev/urandom of=/var/run/random-seed count=1 2>/dev/null
  }
  
  NETWORK() {
      for i in $(ifconfig | grep '^[^ ]' | cut -d ' ' -f 1 | tac); do
          ifconfig "$i" down
      done
  }
  
  WTMP() {
      /sbin/halt -w
  }
  
  KILL() {
      # Goodbye to everybody...
      killall5 -9
  }
  
  UMOUNTPSEUDO() {
      # Unmounting memory filesystems:
      umount -a -t tmpfs
      swapoff -a
      # Unmounting loopback devices first:
      for d in $(grep '^/dev/loop' /proc/mounts | cut -d ' ' -f 2 | tac); do
          eval "umount -d -r -f $'$d'"
      done
      umount -d -r -f -a -O bind 
  }
  
  UMOUNTHDD() {
      # Unmounting all filesystems:
      for d in $(egrep -v '^[^/]|^/dev/root' /proc/mounts | cut -d ' ' -f 2 | tac); do
          eval "umount -d -r -f $'$d'"
      done
  }
  
  FINI() {
      sync; sync
      mount -n -o remount,ro / || { sleep 1; mount -n -o remount,ro /; }
  }
  
  
  PATH=/sbin:/usr/sbin:/bin:/usr/bin
  trap ':' INT QUIT TSTP
  . /etc/runit/lib.sh
  
  try CONSOLE
  startlog /var/log/shutdown
  try TERM
  try HWCLOCK
  try SERVICES
  try SOUND
  try RANDOMSEED
  try NETWORK
  try WTMP
  try KILL
  try UMOUNTPSEUDO
  try UMOUNTHDD
  stoplog
  try FINI