Cubieboard: Debian root на SATA SSD
Хотелось поставить на Cubieboard Debian с корневой файловой системой на подключенном по SATA SSD. Для усложнения задачи файловой системой была выбрана nilfs2.
Cubieboard – это одноплатный мини-компьютер на базе SOC Allwinner A10 с 1Ghz ядром ARM Cortex A8.
Готовый образ Debian можно взять по этому адресу: http://romanrm.ru/en/a10/debian. На момент написания статьи все действия делались с образом, датированным 20 сентября 2013.
Распаковываем и записывам образ на micro-SD:
bzcat a10-debian-server-2gb.2013-09-20.img.bz2 > /dev/mmc_device
Для загрузки используется загрузчик u-boot, который “готовится” отдельно для каждого типа устройства. В записанном выше образе уже встроена версия загрузчика, подходящая для большинства устройств на Allwinner A10, но для получения доступа к всей памяти (1Gb) Cubieboard необходимо использовать специфичную для устройства версию загрузчика. Из скачанного архива понадобятся следующие файлы:
cubieboard/bootloader/sunxi-spl.bin
cubieboard/bootloader/u-boot.bin
Их необходимо записать или в образ, или, если образ уже был записан на SD-карту (см. выше), обновить данные на карте:
dd if=sunxi-spl.bin of=/dev/mmc_device bs=1024 seek=8
dd if=u-boot.bin of=/dev/mmc_device bs=1024 seek=32
Теперь можно загрузить Cubieboard с полученной SD-карты и получить доступ к полноценному окружению Debian. Описываемые ниже действия выполняются именно в таком окружении.
Сборка initramfs
Загрузка системы с initramfs может понадобиться в тех случаях, когда необходимо как-либо изменить процесс ранней загрузки системы: загрузить дополнительные модули и т.п. В конечном счёте хотелось получить возможность загрузки системы с подключенного через SATA SSD с файловой системой nilfs2. В образе используется ядро, в которое вкомпилирована поддержка только vfat и ext4, для работы с другими ФС предполагается загружать отдельные модули, поэтому для доступа к nilfs2 в качестве корневой файловой системы есть следующие варианты: пересборка ядра с поддержкой nilfs2 или загрузка модуля из initramfs.
С пересборкой ядра связываться не хотелось и был выбран способ с initramfs. Напомню, что initramfs представляет архив файловой системы рабочего окружения linux, который распаковывается ядром на ранней стадии загрузки в ramdisk, после чего управление передаётся скрипту /init
(может быть переопределено параметром ядра rinit=
), в обязанности которого и входит выполнение необходимых действий и дальнейшая передача управления основной системе.
Последовательность действий, выполняемых в initramfs для загрузки основной системы с nilfs2 выглядит так:
- загрузка модуля ядра для работы с nilfs2;
- монтирование корневой файловой системы с nilfs2;
- переключение корня файловой системы на смонтированную nilfs2 и запуск init-процесса основной системы.
Для приготовления initramfs потребуется статическая сборка busybox, которую можно установить так:
apt-get install busybox-static
Все эти действия стоит делать на самом Cubieboard. Создаём минимальную файловую систему в каталоге ROOT:
mkdir -p ROOT/{bin,usr,proc,sys,dev,tmp,var,etc}
ln -s bin ROOT/sbin
ln -s ../bin ROOT/usr/bin
ln -s ../bin ROOT/usr/sbin
$(which busybox) --install ROOT/bin
Теперь у нас есть минимальная почти функциональная система в ROOT. Необходимо скопировать туда модуль для работы с nilfs2, путь к которому можно посмотреть в выводе команды modinfo nilfs2
:
cp /path/to/nilfs2.ko ROOT
Остаётся лишь создать init-скрипт, который будет выполнять загрузку модуля, монтирование файловой системы и переключение корня:
>ROOT/init
chmod +x ROOT/init
cat <<"EOF" >ROOT/init
#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin
NEWROOT=/newroot
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t tmpfs dev /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /bin/mdev > /proc/sys/kernel/hotplug
/bin/mdev -s
echo "inserting nilfs2 module"
insmod /nilfs2.ko && echo "nilfs2 module inserted successfully" || echo "failed to insert module"
echo "mounting fs"
mkdir -p $NEWROOT
mount -t nilfs2 -o ro,relatime,discard /dev/sda1 $NEWROOT
echo "disabling hotplug"
echo "" > /proc/sys/kernel/hotplug
echo "unmounting filesystems"
umount /sys
umount /dev/pts && umount /dev
umount /proc
echo "changing root"
exec /bin/switch_root $NEWROOT /sbin/init
EOF
В скрипте отсутствуют какие-либо проверки на ошибки, он создан для конкретной конфигурации, когда корневая файловая система на nilfs2 находится на /dev/sda1
(первый раздел на диске, подключенном через SATA).
Загрузка модуля выполняется через insmod filename.ko
– такой способ не учитывает наличие зависимостей (что делает modprobe
), поэтому, если планируется аналогичным образом загружать какие-то другие модули, необходимо выяснить через lsmod
их зависимости и обеспечить их предварительную загрузку.
Формируем initramfs:
(cd ROOTFS && find . | cpio -H newc -o ) | gzip > initramfs.cpio.gz
Полученный подобным образом initramfs в общем случае можно передавать ядру параметром initrd=initramfs.cpio.gz
. Конкретно в случае с cubieboard и загрузчиком u-boot используется другой метод, о котором будет рассказано ниже.
Загрузка с initramfs
Чуть-чуть о загрузчике u-boot: записанный на карту памяти загрузчик содержит небольшой скрипт, описывающий логику процесса загрузки; скрипт содержит установку различных переменных, загрузку файлов с карты памяти (ядро и т.п.) и, в конечном счёте, передачу управления загруженному ядру (команда bootm
). Параметры загрузки ядра можно задаются переменной bootargs
.
Из скрипта используемого u-boot загрузчика видно, что интерес представляют переменные setargs
и boot_mmc
– именно они содержат команды загрузки файлов с диска и передачу управления ядру. Для удобства код скрипта приведён ниже, его также можно посмотреть в коде:
baudrate=115200
scriptaddr=0x44000000
bootscr=boot.scr
bootenv=uEnv.txt
loadbootscr=fatload mmc 0 ${scriptaddr} ${bootscr} || ext2load mmc 0 ${scriptaddr} ${bootscr} || ext2load mmc 0 ${scriptaddr} boot/${bootscr}
loadbootenv=fatload mmc 0 ${scriptaddr} ${bootenv} || ext2load mmc 0 ${scriptaddr} ${bootenv} || ext2load mmc 0 ${scriptaddr} boot/${bootenv}
boot_mmc=fatload mmc 0 0x43000000 script.bin && fatload mmc 0 0x48000000 ${kernel} && watchdog 0 && bootm 0x48000000
bootcmd=if run loadbootenv; then \
echo Loaded environment from ${bootenv}; \
env import -t ${scriptaddr} ${filesize}; \
fi; \
if test -n ${uenvcmd}; then \
echo Running uenvcmd ...; \
run uenvcmd; \
fi; \
if run loadbootscr; then \
echo Jumping to ${bootscr}; \
source ${scriptaddr}; \
fi; \
run setargs boot_mmc;"
bootdelay=3
console=ttyS0,115200
kernel=uImage
loglevel=8
panicarg=panic=10
root=/dev/mmcblk0p2
setargs=setenv bootargs console=${console} root=${root} loglevel=${loglevel} ${panicarg} ${extraargs}
stderr=serial
stdin=serial
stdout=serial
В скрипте видно, что для кастомизации его выполнения могут использоваться файлы uEnv.txt
и boot.scr
. Первый представляет собой просто текстовый файл с указанием переменных в формате имя=значение
по одной на строку, в то время как второй может содержать отдельный скрипт работы u-boot, но должен быть сформирован в специфичном бинарном формате. Наиболее простым вариантом представляется использование файла uEnv.txt
, т.к. он не требует использования дополнительных инструментов.
Загрузчик u-boot использует команду bootm
для запуска ранее загруженного в память кода (в нашем случае это ядро linux), для передачи параметров ядру используется переменная bootargs
.
Трюк заключается в том, чтобы заставить ядро увидеть initramfs. Сделать это привычным образом (параметр initrd=
) не получится, поэтому необходимо использовать возможности команды bootm
: если при её вызове после адреса загруженного ядра указать адрес предварительно загруженного образа initrd/initramfs, он будет передан ядру. Следовательно, сперва необходимо загрузить initramfs в память. Сделать это можно командой fatload
.
В используемом окружении задействованы следующие адреса:
- 0x43000000: устройство-зависимый файл script.bin, конфигурирующий параметры оборудования;
- 0x44000000: загрузка скрипта кастомизации (uEnv.txt/boot.scr);
- 0x48000000: ядро.
Учитывая размер файла initramfs, для его загрузки был выбран адрес 0x42000000, так мы можем загрузить initramfs объёмом до 16Мб: (0x43000000-0x42000000)/1024/1024 = 16
.
Ещё одним нюансом является то, что u-boot ожидает увидеть файл initramfs/initrd в специфичном формате, если только u-boot не был собран с параметром CONFIG_SUPPORT_RAW_INITRD
. Официальная версия имеет такую поддержку, загруженная нами – нет, поэтому придётся конвертировать initramfs утилитой mkimage
в подходящий формат. К счастью, она есть в репозитории:
apt-get install uboot-mkimage
Далее конвертируем файл initramfs в специфичный формат u-boot:
mkimage -A arm -T ramdisk -C gzip -n uInitrd -d /path/to/ramfs.cpio.gz /boot/uInitrd
На этом этапе остаётся лишь сформировать правильный uEnv.txt
. В нём переопределяется переменная boot_mmc
, содержащая команды для загрузки файлов и передачи управления ядру.
Исходная последовательность (значения переменных раскрыты, переносы строк расставлены для наглядности):
fatload mmc 0 0x43000000 script.bin && \
fatload mmc 0 0x48000000 uImage && \
watchdog 0 && \
bootm 0x48000000
Последовательность действий:
- загрузка с 0 раздела mmc-устройства с файловой системой fat файла
script.bin
по адресу 0x43000000;
- загрузка файла
uImage
(ядро) по адресу 0x48000000;
- отключение watchdog;
- передача управления коду по адресу 0x48000000 (загруженное ядро).
В результате хочется получить такую:
- загрузка с 0 раздела mmc-устройства с файловой системой fat файла
script.bin
по адресу 0x43000000;
- загрузка файла
uImage
(ядро) по адресу 0x48000000;
- отключение watchdog;
- загрузка файла
uInitrd
(initramfs) по адресу 0x42000000;
- если загрузка
uInitrd
прошла успешно, передача управления коду по адресу 0x48000000 (загруженное ядро) с указанием адреса 0x42000000 (загруженный initramfs);
- если загрузка
uInitrd
прошла с ошибкой, передача управления коду по адресу 0x48000000 (загруженное ядро).
Результирующая последовательность в командах u-boot примет такой вид (для наглядности расставлены переносы строк):
fatload mmc 0 0x43000000 script.bin && \
fatload mmc 0 0x48000000 uImage && \
watchdog 0 && \
fatload mmc 0 0x42000000 uInitrd && \
bootm 0x48000000 0x42000000 || \
bootm 0x48000000
Для успешной загрузки initramfs понадобится установить ещё одну переменную: initrd_high
. В документации указано, что эта переменная указывается для ограничения области памяти, в которую допускается загружать initrd; при этом установка специального значения 0xffffffff
указывает на необходимость использования предварительно загруженного в память образа initrd (команда fatload
).
Собрав воедино всё вышеперечисленное, получаем результирующий файл /boot/uEnv.txt
подобного вида:
initrd_high=0xffffffff
boot_mmc=fatload mmc 0 0x43000000 script.bin && fatload mmc 0 0x48000000 uImage && watchdog 0 && fatload mmc 0 0x42000000 uInitrd && bootm 0x48000000 0x42000000 || bootm 0x48000000
Предлагаемый в начале статьи образ debian использует серверную версию ядра, которая отключает вывод видео для экономии оперативной памяти; в процессе отладки при отсутствии консольного кабеля удобнее наблюдать процесс загрузки на экране, для этого временно можно установить desktop версию ядра и использовать подобный uEnv.txt
:
setargs=setenv bootargs loglevel=6 panic=0 console=tty0 console=tty1 disp.screen0_output_mode=EDID:1280x720p60 root=/dev/mmcblk0p2 rootwait boot_delay=1
initrd_high=0xffffffff
boot_mmc=fatload mmc 0 0x43000000 script.bin && fatload mmc 0 0x48000000 uImage && watchdog 0 && fatload mmc 0 0x42000000 uInitrd && bootm 0x48000000 0x42000000 || bootm 0x48000000
Использование самосборного initramfs приводит к тому, что параметр ядра root=
перестаёт играть роль, т.к. выбор корневого устройства жёстко прописан в init-скрипте initramfs.
В заключение добавлю, что для успешной работы с nilfs2 лучше установить новую сборку ядра, в которой включена поддержка POSIX queues, необходимых для работы nilfs_cleanerd
(сборщик мусора для nilfs2).