Pull to refresh

Делаем из Linux From Scratch свой универсальный дистрибутив

Reading time 9 min
Views 15K
Так уж случилось, что пару лет назад по долгу службы на команду разработчиков, к которой я отношусь, свалилась неожиданная задача — разработка системы управления оборудованием (в этом-то как-раз неожиданности нет, ибо направление разработок такое) с управляющим PC под Linux.
Разработки линуксовой части велись (да и ведутся) под Ubuntu, в среде Code::Blocks. Но, как показала практика, для качественной работы нужно что-то гораздо более легкое с гарантированным временем отклика. Для работы было достаточно консоли, так как задачи организации пользовательского интерфейса решались на подключаемом по TCP/IP удаленном компьютере.
Тогда и пришла идея использовать дистрибутив Linux собственной сборки, чем (сборкой дистрибутива), собственно, в свободное время я и занялся. Выбор пал на LFS. Про то что такое LFS уже неоднократно писали даже на Хабре, я же опишу решение нескольких дополнительных (кроме простенького Linux'а) задач, вставших передо мной в нашем конкретном случае.
Поначалу такая задача была одна — использовать real-time ядро.
Однако дальше, когда идея USB-флешки с дистрибутивом, пришлась всем по душе, то появились задачи размножения флешек и запуска системы на различных компьютерах (тестовых стендов много, имея свою флешку суешь в карман и иди к любому). Вот тут и появились проблемы — LFS не обладает 100% переносимостью с одного компьютера на другой. Для ее адаптации к конкретному компьютеру нужно править некоторые скрипты, что в условиях команды вчерашних Windows-кодеров проблематично (на виртуалку с Ubuntu некоторые пересели, но консоль и скрипты — это беда). Размножение системы также требует повторения некоторых манипуляций, проделываемых в процессе сборки (тот же GRUB установить).

Естественно, решение всех задач есть на просторах интернета, но, думаю, сбор некоторой информации в одном месте никому не помешает.
Итак, конкретные задачи были следующие…

1. Использование ядра Linux с real-time патчем

Это была одна из самых легких задач. Процесс сборки прошел по книге LFS с единственным исключением — вместо штатного для книги ядра было взято 2.6.33.9 и RT-патч для него. Везде, где происходили манипуляции с ядром (установка Linux Headers и ядра непосредственно), работаем с нашей пропатченной версией.
Также не лишним будет сказать, что дистрибутив собирался без подключения swap-раздела (2Гб ОЗУ это в нашем случае — выше крыши, наличие swap не желательно по причине его негативного влияния на гарантированное время отклика, да и для флешки он крайне губителен) и представлял собой один единственный раздел ext2fs.

2. Автоматический вход в систему (в условиях разработки безопасность нам не важна, да и систему пускаем под рутом по ряду причин)

Идея автоматического входа была взята отсюда.
Был создан файл autologin.c следующего содержания:
int main() 
{
execlp("login", "login", "-f", "root", 0);
}

Далее файл был скомпилирован командой:

gcc autologin.c -o /usr/local/sbin/autologin

Далее было решено, что двух консолей с автоматическим входом хватит (одна для запуска системы, другая для всего остального, если понадобится).
В файле /etc/inittab строчки:

1:2345:respawn:/sbin/agetty tty1 9600
2:2345:respawn:/sbin/agetty tty2 9600


были заменены на:

1:2345:respawn:/sbin/agetty -n -l /usr/local/sbin/autologin tty1 9600
2:2345:respawn:/sbin/agetty -n -l /usr/local/sbin/autologin tty2 9600


После этого никаких проблем с автоматическим входом не было, поэтому никаких дополнительных манипуляций со скриптами не было.

3. Отвязывание системы от порядка подключения дисков


У разных BIOS свои заморочки. Одни, например, считают, что первым является тот диск, с которого мы загружаемся. В этом случае наша флешка будет sda. Другие считают, что сначала должны идти жесткие диски, а потом другие устройства. В этом случае наша флешка будет иметь имя sdb, sdc и так далее.
В результате, то система не может загрузиться с диска, которого нет, то корневой каталог не может быть смонтирован по той причине, что в /etc/fstab указан не тот диск.
Все решается либо исправлением /boot/grub/grub.cfg и /etc/fstab под конкретную машину или использованием для загрузки и монтирования не имени диска (sda, sdb и т. д.), а UUID файловой системы, который для данной файловой системы на флешке будет уникален и, что самое главное — постоянен.
Проблема в том, что GRUB работать с UUID умеет, а ядро — нет, то есть напрямую монтировать корневую систему по UUID (не зная имени устройства) невозможно. Это не баг, а следствие идейных соображений Линуса Торвальдса, поэтому на такую возможность и в будущем надеяться не стоит. Тем не менее пути обхода есть — это initramfs.
Initramfs — временная файловая система, помогающая в загрузке и монтировании файловых систем настоящей системы.
В стандартную сборку LFS initramfs не входит, поэтому для ее построения воспользуемся рекомендациями из Gentoo Wiki и некоторыми собственными соображениями (вариант из Gentoo Wiki без изменений в моем случае проблему с именами дисков не решил, да и не заработал толком).
Для создания простейшей initramfs системы, монтирующей нашу основную по UUID нужна простейшая командная оболочка (shell) и скрипт init. Полный набор утилит командной строки достаточно громоздок для initramfs, поэтому часто для этой цели применяется busybox, который при скромных размерах и требованиях реализует некоторые, наиболее часто используемые утилиты.
Забираем последнюю версию busybox:

cd sources
wget http://busybox.net/downloads/busybox-1.18.4.tar.bz2


Распаковываем и конфигурируем:

tar jxf busybox-1.18.4.tar.bz2
cd busybox-1.18.4
make menuconfig


Конфигурирование производится при помощи меню (наподобие ядра Linux). В принципе, стандартной конфигурации хватает для наших нужд, но, на всякий случай, стоит проверить, что подключены следующие возможности:
Support for devfs — поддержка devfs для работы с /dev.
Build Busybox as a static library (no shared libraries) — статическая компоновка, чтобы не тянуть за собой кучу so-библиотек.
Support version 2.6.x Linux kernels — поддержка ядер линейки 2.6.
А также поддержка функциональности утилит: sh, cat, cut, findfs, mount, umount, sleep, echo, switch_root.

Компилируем:

make

Теперь собираем дерево каталогов нашей файловой системы:

mkdir /usr/src/initramfs
cd /usr/src/initramfs
mkdir -p bin lib dev etc mnt/root proc root sbin sys
cp -a /dev/{null,console} /usr/src/initramfs/dev/


Копируем busybox и создаем линки на утилиты:

cp /sources/busybox-1.18.4/busybox ./bin/
cd bin
ln -s busybox sh
ln -s busybox cat
ln -s busybox cut
ln -s busybox findfs
ln -s busybox mount
ln -s busybox umount
ln -s busybox sleep
ln -s busybox switch_root
cd ..


Осталось написать скрипт init:
#!/bin/sh

# Монтируем файловые системы proc и sysfs
mount -t proc none /proc
mount -t sysfs none /sys

# USB-устройства инициализируются ядром не сразу
# На всякий случай ждем 10 секунд, иначе корневой раздел не будет найден
sleep 10

# Монтируем файловую систему устройств
mount -t devtmpfs none /dev

# Ищем по UUID имя корневого диска и монтируем систему
for cmd in $(cat /proc/cmdline) ; do
	case $cmd in
	root=*)
		uuid=$(echo $cmd | cut -d= -f3)
		mount -o ro $(findfs UUID="$uuid") /mnt/root
	;;
	esac
done

# Размонтируем все примонтированные файловые системы
umount /dev
umount /proc
umount /sys

# Загружаем основную систему
exec switch_root /mnt/root /sbin/init


Делаем наш скрипт исполняемым:

chmod +x /usr/src/initramfs/init

Собираем нашу временную файловую систему в архив:

cd /usr/src/initramfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > /boot/initrd.img-2.6.33-rt31


Обратим внимание на имя файла — оно должно соответствовать имени ядра (это нужно, чтобы GRUB его правильно подцепил). То есть, если ядро имеет имя vmlinux-2.6.33-rt31, то initramfs должен иметь имя initrd.img-2.6.33-rt31.

Теперь при выполнении grub-mkconfig GRUB обнаружит initramfs, а также включит в конфигурацию UUID корневой системы. Для проверки можно поправить /boot/grub/grub.cfg руками. Например конфигурацию:

menuentry "Linux 2.6.33-rt31" --class gnu-linux --class gnu --class os {
insmod ext2
set root='(hd0,1)'
search --no-floppy --fs-uuid --set 47029df8-8567-417d-b813-eedfe1ff8b0f
linux /boot/vmlinux-2.6.33-rt31 root=/dev/sda1 ro
}


исправим на:

menuentry "Linux 2.6.33-rt31" --class gnu-linux --class gnu --class os {
insmod ext2
set root='(hd0,1)'
search --no-floppy --fs-uuid --set 47029df8-8567-417d-b813-eedfe1ff8b0f
linux /boot/vmlinux-2.6.33-rt31 root=UUID=47029df8-8567-417d-b813-eedfe1ff8b0f ro
initrd /boot/initrd.img-2.6.33-rt31
}


UUID файловой системы можно узнать так (например, для /dev/sdb1):

blkid -p -o udev /dev/sdb1

Осталось поправить /etc/fstab, заменив строчку:

/dev/sda1 / ext2 defaults 1 1

на

UUID= 47029df8-8567-417d-b813-eedfe1ff8b0f / ext2 defaults 1 1

Также следует заметить, что для всех манипуляций выше необходимо, чтобы в ядре была включена поддержка devtmpfs (CONFIG_DEVTMPFS=y) и initramfs (CONFIG_BLK_DEV_INITRD=y).

4. Отвязывание системы от сетевой карты

Если в компьютере установлено более одной сетевой платы, то при параллельной загрузке модулей ядра не гарантируется постоянное назначение имен этим платам. Например, есть две платы. Плата_1 имеет имя интерфейса в системе eth0, Плата_2 — eth1. При очередной перезагрузке может получиться так, что Плата_1 станет eth1, а Плата_2 — eth0.
С этой целью в LFS производится привязка имени к конкретной плате. При загрузке на другом компьютере очень велика вероятность, что сеть не поднимется.
В моем конкретном случае плата на всех компьютерах одна и IP — статический (связь только с терминальным компьютером напрямую).
Поднятие сетевого интерфейса в LFS осуществляется скриптом /etc/rc.d/init.d/network. Допишем скрипт так, чтобы каждый раз при загрузке генерировался конфигурационный файл /etc/udev/rules.d/70-persistent-net.rules и при завершении работы этот файл удалялся. Подозреваю, что есть метод проще, но найденный метод заработал, а копаться в принципах работы Udev времени и особого желания на момент сборки системы не было.
В начало раздела start команды case добавляем:

for NIC in /sys/class/net/*
do
	INTERFACE=${NIC##*/} udevadm test --action=add $NIC
done


А в конец секции stop (непосредственно перед ;;) добавляем:

rm /etc/udev/rules.d/70-persistent-net.rules


Теперь при загрузке на любой системе имя сетевого интерфейса будет eth0 (кроме самых экзотических случаев) и сеть будет подниматься. Разумеется, каталог /etc/sysconfig/network-devices/ifconfig.eth0 с файлом ipv4 должен существовать. Содержимое этого файла описано в книге LFS.

5. Написание скрипта, производящего инсталляцию LFS на любую флешку

Осталось последнее — сделать архив системы и скрипт, который будет ее устанавливать на произвольный носитель.
Загружаемся в другой системе (не с флешки), монтируем флешку, например в /mnt/usb-os. Архивируем содержимое:

cd /mnt/usb-os
tar -cvjf ~/pack.tar.bz2 *


Пишем скрипт для установки install_usb-os.sh. В качестве параметра скрипт принимает имя устройства, на котором необходимо развернуть систему (например /dev/sdb). Скрипт сам создаст необходимый раздел и файловую систему (/dev/sdb1, если указано имя /dev/sdb), распакует архив и установит GRUB.

Запускаться он должен с правами root и, на самом деле, очень опасен.
В случае неверного указания имени устройства могут быть уничтожены все данные на рабочем диске!


#!/bin/sh
# Проверяем, существует ли параметр с именем устройства
if [ "x${1}" = "x" ] ; then
  echo "Usage: install_usb-os device_name"
  exit
fi

# Размонтируем раздел, если вдруг был примонтирован
umount ${1}1

# Создаем временный каталог для монтирования 
if [ ! -e /mnt/USBOSTmp ]; then
  mkdir /mnt/USBOSTmp
fi

# Создаем MBR, первичный раздел размером с диск
echo "Building partitions..."
parted -s ${1} mklabel msdos
parted -s ${1} unit % mkpart primary ext2 0 100

# Форматируем раздел и монтируем его в наш временный каталог
echo "Preparing filesystem..."
mkfs -t ext2 ${1}1
mount -t ext2 ${1}1 /mnt/USBOSTmp

# Распаковываем архив
echo "Unpacking distributive..."
tar -xvf ./pack.tar.bz2 -C /mnt/USBOSTmp

# Создаем правильные ноды console и null
# (те, что распаковываются из архива, почему-то оказываются неверными)
mknod -m 600 /mnt/USBOSTmp/dev/console c 5 1
mknod -m 666 /mnt/USBOSTmp/dev/null c 1 3

# Монтируем виртуальные файловые системы (устройств и т.д.)
echo "Mounting necessary file systems..."
mount -v --bind /dev /mnt/USBOSTmp/dev
mount -vt devpts devpts /mnt/USBOSTmp/dev/pts
mount -vt tmpfs shm /mnt/USBOSTmp/dev/shm
mount -vt proc proc /mnt/USBOSTmp/proc
mount -vt sysfs sysfs /mnt/USBOSTmp/sys

# Добываем UUID нашего раздела на флешке и записываем не нее
# файл fstab
uu=`blkid -p -o value ${1}1 | grep -`
echo "Writing fstab for uuid=$uu..."
cat > /mnt/USBOSTmp/tmp/fstab << "EOF"
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
devpts /dev/pts devpts gid=4,mode=620 0 0
tmpfs /dev/shm tmpfs defaults 0 0
EOF
echo "UUID=$uu / ext2 defaults 1 1" >> /mnt/USBOSTmp/tmp/fstab

# Записываем на флешку скрипт, который должен быть запущен
# когда корневым каталогом является корень флешки
# В этом скрипте происходит конфигурирование и установка GRUB
cat > /mnt/USBOSTmp/tmp/update.sh << "EOF"
echo "Starting internal update script for $H_DEV ..."
mv -v /tmp/fstab /etc/fstab
grub-mkconfig -o /boot/grub/grub.cfg
echo "Updating mbr $H_DEV2..."
grub-setup $H_DEV2
EOF

# Делаем его исполняемым
chmod +x /mnt/USBOSTmp/tmp/update.sh

# Запускаем скрипт в другом root-окружении,
# когда корневым каталогом является корень флешки
echo "Running update..."
chroot /mnt/AxiOMATmp/ /usr/bin/env -i HOME=/root TERM="$TERM" PS1='\u:\w\$ ' PATH=/bin:/usr/bin:/sbin:/usr/sbin H_DEV=${1}1 H_DEV2=${1} /tmp/update.sh

# Убираем все лишнее, размонтируем файловые системы
rm -v /mnt/USBOSTmp/tmp/update.sh
umount -v /mnt/USBOSTmp/dev/pts
umount -v /mnt/USBOSTmp/dev/shm
umount -v /mnt/USBOSTmp/dev
umount -v /mnt/USBOSTmp/proc
umount -v /mnt/USBOSTmp/sys
umount ${1}1
rm -rvf /mnt/USBOSTmp
echo "Installation finished!"


Теперь сделаем наш скрипт исполняемым:

chmod +x install_usb-os.sh

Размещаем архив pack.tar.bz2 в каталоге со скриптом и инсталлятор готов!
Tags:
Hubs:
+72
Comments 51
Comments Comments 51

Articles