Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров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 в каталоге со скриптом и инсталлятор готов!
Теги:
Хабы:
+72
Комментарии51

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн