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 выглядит так:

  1. загрузка модуля ядра для работы с nilfs2;
  2. монтирование корневой файловой системы с nilfs2;
  3. переключение корня файловой системы на смонтированную 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.

В используемом окружении задействованы следующие адреса:

Учитывая размер файла 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

Последовательность действий:

  1. загрузка с 0 раздела mmc-устройства с файловой системой fat файла script.bin по адресу 0x43000000;
  2. загрузка файла uImage (ядро) по адресу 0x48000000;
  3. отключение watchdog;
  4. передача управления коду по адресу 0x48000000 (загруженное ядро).

В результате хочется получить такую:

  1. загрузка с 0 раздела mmc-устройства с файловой системой fat файла script.bin по адресу 0x43000000;
  2. загрузка файла uImage (ядро) по адресу 0x48000000;
  3. отключение watchdog;
  4. загрузка файла uInitrd (initramfs) по адресу 0x42000000;
  5. если загрузка uInitrd прошла успешно, передача управления коду по адресу 0x48000000 (загруженное ядро) с указанием адреса 0x42000000 (загруженный initramfs);
  6. если загрузка 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).